From ce65935f0721e7fabfc198473f83181c0999cea7 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 7 Mar 2026 23:16:51 +0100 Subject: [PATCH 01/23] preallocate cutting geometries --- benchmarks/CMakeLists.txt | 43 ++++++++ benchmarks/README.md | 47 +++++++++ benchmarks/bench_cut_hex.cpp | 96 ++++++++++++++++++ benchmarks/bench_cut_mesh.cpp | 145 +++++++++++++++++++++++++++ benchmarks/bench_cut_tetrahedron.cpp | 93 +++++++++++++++++ benchmarks/bench_cut_triangle.cpp | 91 +++++++++++++++++ cpp/CMakeLists.txt | 1 + cpp/src/cut_hexahedron.cpp | 36 ++++--- cpp/src/cut_prism.cpp | 36 ++++--- cpp/src/cut_pyramid.cpp | 35 ++++--- cpp/src/cut_quadrilateral.cpp | 71 +++++++------ cpp/src/cut_tetrahedron.cpp | 44 +++++--- cpp/src/cut_triangle.cpp | 36 +++++-- cpp/src/span_math.h | 75 +++++++++++--- cpp/src/utils.h | 39 ++++--- 15 files changed, 764 insertions(+), 124 deletions(-) create mode 100644 benchmarks/CMakeLists.txt create mode 100644 benchmarks/README.md create mode 100644 benchmarks/bench_cut_hex.cpp create mode 100644 benchmarks/bench_cut_mesh.cpp create mode 100644 benchmarks/bench_cut_tetrahedron.cpp create mode 100644 benchmarks/bench_cut_triangle.cpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..a5d4d6e --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.21) + +if(CMAKE_CONFIGURATION_TYPES) + message(FATAL_ERROR "Benchmarks are supported only for single-config Release builds. Configure with -DCMAKE_BUILD_TYPE=Release.") +endif() + +if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") + message(FATAL_ERROR "Benchmarks compile only in Release mode. Set -DCMAKE_BUILD_TYPE=Release.") +endif() + +include(FetchContent) +find_package(benchmark QUIET) + +if(NOT benchmark_FOUND) + FetchContent_Declare( + benchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG v1.9.1 + ) + set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(benchmark) +endif() + +function(add_cutcells_benchmark target source_file) + add_executable(${target} ${source_file}) + target_link_libraries(${target} PRIVATE cutcells benchmark::benchmark) + target_compile_features(${target} PRIVATE cxx_std_20) +endfunction() + +add_cutcells_benchmark(bench_cut_triangle bench_cut_triangle.cpp) +add_cutcells_benchmark(bench_cut_tetrahedron bench_cut_tetrahedron.cpp) +add_cutcells_benchmark(bench_cut_hex bench_cut_hex.cpp) +add_cutcells_benchmark(bench_cut_mesh bench_cut_mesh.cpp) + +add_custom_target(run_benchmarks + COMMAND $ + COMMAND $ + COMMAND $ + COMMAND $ + DEPENDS bench_cut_triangle bench_cut_tetrahedron bench_cut_hex bench_cut_mesh + USES_TERMINAL + COMMENT "Build and run all CutCells benchmarks" +) diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..0eb92a6 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,47 @@ +# CutCells Benchmarks + +This folder contains Google Benchmark executables for: + +- `cut_triangle` +- `cut_tetrahedron` +- `cut_hexahedron` +- `cut_vtk_mesh` +- `create_cut_mesh` + +Each benchmark uses `benchmark::State` and performs `1e6` cut operations per state iteration. + +## Build (Release only) + +Benchmarks are intentionally enabled only in Release mode. + +```bash +cd /Users/sclaus/Workspace/FEniCSx0.10/CutCells/cpp +cmake -S . -B build-bench-release -DCMAKE_BUILD_TYPE=Release +cmake --build build-bench-release --target bench_cut_triangle bench_cut_tetrahedron bench_cut_hex bench_cut_mesh -j +``` + +## Run individual benchmarks + +```bash +./build-bench-release/benchmarks/bench_cut_triangle +./build-bench-release/benchmarks/bench_cut_tetrahedron +./build-bench-release/benchmarks/bench_cut_hex +./build-bench-release/benchmarks/bench_cut_mesh +``` + +## Run all benchmarks with one target + +Use the aggregate target: + +```bash +cmake --build build-bench-release --target run_benchmarks -j +``` + +## Useful Google Benchmark flags + +Examples: + +```bash +./build-bench-release/benchmarks/bench_cut_hex --benchmark_filter=CutHexahedron +./build-bench-release/benchmarks/bench_cut_mesh --benchmark_repetitions=5 --benchmark_report_aggregates_only=true +``` diff --git a/benchmarks/bench_cut_hex.cpp b/benchmarks/bench_cut_hex.cpp new file mode 100644 index 0000000..caed254 --- /dev/null +++ b/benchmarks/bench_cut_hex.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +#include +#include + +#include "cut_cell.h" +#include "cut_hexahedron.h" + +namespace +{ +constexpr int kRepeats = 1'000'000; + +std::array make_random_hexahedron(std::mt19937_64& rng) +{ + std::uniform_real_distribution shift(-10.0, 10.0); + std::uniform_real_distribution len(0.1, 2.0); + + const double x0 = shift(rng); + const double y0 = shift(rng); + const double z0 = shift(rng); + const double lx = len(rng); + const double ly = len(rng); + const double lz = len(rng); + + return { + x0, y0, z0, + x0 + lx, y0, z0, + x0 + lx, y0 + ly, z0, + x0, y0 + ly, z0, + x0, y0, z0 + lz, + x0 + lx, y0, z0 + lz, + x0 + lx, y0 + ly, z0 + lz, + x0, y0 + ly, z0 + lz, + }; +} + +std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) +{ + std::uniform_real_distribution w(-1.0, 1.0); + std::uniform_real_distribution d(-1.0, 1.0); + + std::array ls{}; + for (;;) + { + const double nx = w(rng); + const double ny = w(rng); + const double nz = w(rng); + const double shift = d(rng); + + bool has_pos = false; + bool has_neg = false; + bool near_zero = false; + + for (int i = 0; i < 8; ++i) + { + const double x = coords[i * 3 + 0]; + const double y = coords[i * 3 + 1]; + const double z = coords[i * 3 + 2]; + ls[i] = nx * x + ny * y + nz * z + shift; + + has_pos = has_pos || (ls[i] > 0.0); + has_neg = has_neg || (ls[i] < 0.0); + near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); + } + + if (has_pos && has_neg && !near_zero) + return ls; + } +} +} // namespace + +static void BM_CutHexahedron(benchmark::State& state) +{ + std::mt19937_64 rng(20260307); + + const auto coords = make_random_hexahedron(rng); + const auto ls = make_intersecting_ls(coords, rng); + + for (auto _ : state) + { + cutcells::cell::CutCell out; + for (int i = 0; i < kRepeats; ++i) + { + cutcells::cell::hexahedron::cut(coords, 3, ls, "phi=0", out, false); + benchmark::DoNotOptimize(out); + } + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); +} + +BENCHMARK(BM_CutHexahedron); +BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_mesh.cpp b/benchmarks/bench_cut_mesh.cpp new file mode 100644 index 0000000..412aaaf --- /dev/null +++ b/benchmarks/bench_cut_mesh.cpp @@ -0,0 +1,145 @@ +#include + +#include +#include +#include +#include +#include + +#include "cell_types.h" +#include "cut_hexahedron.h" +#include "cut_mesh.h" + +namespace +{ +constexpr int kRepeats = 1'000'000; + +struct RandomHexCellData +{ + std::array points; + std::array connectivity; + std::array offset; + std::array vtk_type; + std::array ls; +}; + +RandomHexCellData make_random_hex_cell_data(std::mt19937_64& rng) +{ + std::uniform_real_distribution shift(-10.0, 10.0); + std::uniform_real_distribution len(0.1, 2.0); + std::uniform_real_distribution w(-1.0, 1.0); + std::uniform_real_distribution d(-1.0, 1.0); + + RandomHexCellData data{ + .points = {}, + .connectivity = {0, 1, 2, 3, 4, 5, 6, 7}, + .offset = {0, 8}, + .vtk_type = {static_cast(cutcells::cell::vtk_types::VTK_HEXAHEDRON)}, + .ls = {}, + }; + + const double x0 = shift(rng); + const double y0 = shift(rng); + const double z0 = shift(rng); + const double lx = len(rng); + const double ly = len(rng); + const double lz = len(rng); + + data.points = { + x0, y0, z0, + x0 + lx, y0, z0, + x0 + lx, y0 + ly, z0, + x0, y0 + ly, z0, + x0, y0, z0 + lz, + x0 + lx, y0, z0 + lz, + x0 + lx, y0 + ly, z0 + lz, + x0, y0 + ly, z0 + lz, + }; + + for (;;) + { + const double nx = w(rng); + const double ny = w(rng); + const double nz = w(rng); + const double shift_plane = d(rng); + + bool has_pos = false; + bool has_neg = false; + bool near_zero = false; + + for (int i = 0; i < 8; ++i) + { + const double x = data.points[i * 3 + 0]; + const double y = data.points[i * 3 + 1]; + const double z = data.points[i * 3 + 2]; + data.ls[i] = nx * x + ny * y + nz * z + shift_plane; + + has_pos = has_pos || (data.ls[i] > 0.0); + has_neg = has_neg || (data.ls[i] < 0.0); + near_zero = near_zero || (std::abs(data.ls[i]) < std::numeric_limits::epsilon()); + } + + if (has_pos && has_neg && !near_zero) + return data; + } +} + +cutcells::mesh::CutCells make_random_cut_cells(std::mt19937_64& rng) +{ + const auto data = make_random_hex_cell_data(rng); + + cutcells::cell::CutCell cut_cell; + cutcells::cell::hexahedron::cut(data.points, 3, data.ls, "phi=0", cut_cell, false); + + cut_cell._parent_cell_type = cutcells::cell::type::hexahedron; + cut_cell._parent_vertex_ids = std::vector(data.connectivity.begin(), data.connectivity.end()); + + cutcells::mesh::CutCells cells; + cells._cut_cells = {std::move(cut_cell)}; + cells._parent_map = {0}; + cells._types = {cutcells::cell::type::hexahedron}; + return cells; +} +} // namespace + +static void BM_CutVtkMesh(benchmark::State& state) +{ + std::mt19937_64 rng(20260307); + const auto data = make_random_hex_cell_data(rng); + + for (auto _ : state) + { + cutcells::mesh::CutMesh out; + for (int i = 0; i < kRepeats; ++i) + { + out = cutcells::mesh::cut_vtk_mesh(data.ls, data.points, data.connectivity, data.offset, + data.vtk_type, "phi=0", false); + benchmark::DoNotOptimize(out); + } + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); +} + +static void BM_CreateCutMesh(benchmark::State& state) +{ + std::mt19937_64 rng(20260307); + + for (auto _ : state) + { + for (int i = 0; i < kRepeats; ++i) + { + auto cells = make_random_cut_cells(rng); + auto out = cutcells::mesh::create_cut_mesh(cells); + benchmark::DoNotOptimize(out); + } + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); +} + +BENCHMARK(BM_CutVtkMesh); +BENCHMARK(BM_CreateCutMesh); +BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_tetrahedron.cpp b/benchmarks/bench_cut_tetrahedron.cpp new file mode 100644 index 0000000..092c3b6 --- /dev/null +++ b/benchmarks/bench_cut_tetrahedron.cpp @@ -0,0 +1,93 @@ +#include + +#include +#include +#include +#include + +#include "cut_cell.h" +#include "cut_tetrahedron.h" + +namespace +{ +constexpr int kRepeats = 1'000'000; + +std::array make_random_tetrahedron(std::mt19937_64& rng) +{ + std::uniform_real_distribution shift(-10.0, 10.0); + std::uniform_real_distribution len(0.1, 2.0); + + const double x0 = shift(rng); + const double y0 = shift(rng); + const double z0 = shift(rng); + + const double lx = len(rng); + const double ly = len(rng); + const double lz = len(rng); + + return { + x0, y0, z0, + x0 + lx, y0, z0, + x0, y0 + ly, z0, + x0, y0, z0 + lz, + }; +} + +std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) +{ + std::uniform_real_distribution w(-1.0, 1.0); + std::uniform_real_distribution d(-1.0, 1.0); + + std::array ls{}; + for (;;) + { + const double nx = w(rng); + const double ny = w(rng); + const double nz = w(rng); + const double shift = d(rng); + + bool has_pos = false; + bool has_neg = false; + bool near_zero = false; + + for (int i = 0; i < 4; ++i) + { + const double x = coords[i * 3 + 0]; + const double y = coords[i * 3 + 1]; + const double z = coords[i * 3 + 2]; + ls[i] = nx * x + ny * y + nz * z + shift; + + has_pos = has_pos || (ls[i] > 0.0); + has_neg = has_neg || (ls[i] < 0.0); + near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); + } + + if (has_pos && has_neg && !near_zero) + return ls; + } +} +} // namespace + +static void BM_CutTetrahedron(benchmark::State& state) +{ + std::mt19937_64 rng(20260307); + + const auto coords = make_random_tetrahedron(rng); + const auto ls = make_intersecting_ls(coords, rng); + + for (auto _ : state) + { + cutcells::cell::CutCell out; + for (int i = 0; i < kRepeats; ++i) + { + cutcells::cell::tetrahedron::cut(coords, 3, ls, "phi=0", out, false); + benchmark::DoNotOptimize(out); + } + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); +} + +BENCHMARK(BM_CutTetrahedron); +BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_triangle.cpp b/benchmarks/bench_cut_triangle.cpp new file mode 100644 index 0000000..db0bafd --- /dev/null +++ b/benchmarks/bench_cut_triangle.cpp @@ -0,0 +1,91 @@ +#include + +#include +#include +#include +#include + +#include "cut_cell.h" +#include "cut_triangle.h" + +namespace +{ +constexpr int kRepeats = 1'000'000; + +std::array make_random_triangle(std::mt19937_64& rng) +{ + std::uniform_real_distribution shift(-10.0, 10.0); + std::uniform_real_distribution len(0.1, 2.0); + + const double x0 = shift(rng); + const double y0 = shift(rng); + const double z0 = shift(rng); + + const double lx = len(rng); + const double ly = len(rng); + + return { + x0, y0, z0, + x0 + lx, y0, z0, + x0, y0 + ly, z0, + }; +} + +std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) +{ + std::uniform_real_distribution w(-1.0, 1.0); + std::uniform_real_distribution d(-1.0, 1.0); + + std::array ls{}; + for (;;) + { + const double nx = w(rng); + const double ny = w(rng); + const double nz = w(rng); + const double shift = d(rng); + + bool has_pos = false; + bool has_neg = false; + bool near_zero = false; + + for (int i = 0; i < 3; ++i) + { + const double x = coords[i * 3 + 0]; + const double y = coords[i * 3 + 1]; + const double z = coords[i * 3 + 2]; + ls[i] = nx * x + ny * y + nz * z + shift; + + has_pos = has_pos || (ls[i] > 0.0); + has_neg = has_neg || (ls[i] < 0.0); + near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); + } + + if (has_pos && has_neg && !near_zero) + return ls; + } +} +} // namespace + +static void BM_CutTriangle(benchmark::State& state) +{ + std::mt19937_64 rng(20260307); + + const auto coords = make_random_triangle(rng); + const auto ls = make_intersecting_ls(coords, rng); + + for (auto _ : state) + { + cutcells::cell::CutCell out; + for (int i = 0; i < kRepeats; ++i) + { + cutcells::cell::triangle::cut(coords, 3, ls, "phi=0", out, false); + benchmark::DoNotOptimize(out); + } + benchmark::ClobberMemory(); + } + + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); +} + +BENCHMARK(BM_CutTriangle); +BENCHMARK_MAIN(); diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2ee2af4..37b4f51 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -17,6 +17,7 @@ include(FeatureSummary) # Source files add_subdirectory(src) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../benchmarks ${CMAKE_CURRENT_BINARY_DIR}/benchmarks) set_target_properties(cutcells PROPERTIES PRIVATE_HEADER "${HEADERS}") target_include_directories(cutcells PUBLIC diff --git a/cpp/src/cut_hexahedron.cpp b/cpp/src/cut_hexahedron.cpp index 8ba5c20..015b6bb 100644 --- a/cpp/src/cut_hexahedron.cpp +++ b/cpp/src/cut_hexahedron.cpp @@ -17,38 +17,42 @@ #include #include #include -#include #include namespace cutcells::cell::hexahedron { namespace { + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 40; + constexpr int reserve_connectivity = 64; + constexpr int reserve_types = 64; + [[noreturn]] void throw_missing_token(const int flag, const int token, const char* where) { throw std::runtime_error(std::string(where) + ": missing token=" + std::to_string(token) + " for flag=" + std::to_string(flag)); } - int lookup_token_or_throw(const std::unordered_map& vertex_case_map, + int lookup_token_or_throw(const VertexCaseMap& vertex_case_map, const int flag, const int token, const char* where) { - const auto it = vertex_case_map.find(token); - if (it == vertex_case_map.end()) + if (token < 0 || token >= static_cast(vertex_case_map.size()) || vertex_case_map[token] < 0) throw_missing_token(flag, token, where); - return it->second; + return vertex_case_map[token]; } template void compute_intersection_points(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { intersection_points.clear(); - vertex_case_map.clear(); + intersection_points.reserve(12 * gdim); + vertex_case_map.fill(-1); std::vector v0(gdim); std::vector v1(gdim); @@ -86,9 +90,9 @@ namespace cutcells::cell::hexahedron void ensure_vertex_token(const std::span vertex_coordinates, const int gdim, const int token, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; const int vid = token - 100; @@ -106,9 +110,9 @@ namespace cutcells::cell::hexahedron const int* special_point_offset, const int* special_point_data, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; const int sp_id = token - 200; @@ -179,7 +183,7 @@ namespace cutcells::cell::hexahedron const int* special_point_data, bool triangulate, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { const int cell_begin = case_offsets[flag]; const int cell_end = case_offsets[flag + 1]; @@ -261,14 +265,18 @@ namespace cutcells::cell::hexahedron // Compute intersections (shared for all parts) std::vector intersection_points; - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, - intersection_points, vertex_case_map); + intersection_points, vertex_case_map); cut_cell._gdim = gdim; cut_cell._vertex_coords = std::move(intersection_points); cut_cell._types.clear(); cut_cell._connectivity.clear(); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_prism.cpp b/cpp/src/cut_prism.cpp index acc9faf..0220bed 100644 --- a/cpp/src/cut_prism.cpp +++ b/cpp/src/cut_prism.cpp @@ -17,13 +17,17 @@ #include #include #include -#include #include namespace cutcells::cell::prism { namespace { + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 32; + constexpr int reserve_connectivity = 48; + constexpr int reserve_types = 48; + // VTK_WEDGE / CutCells prism vertex ordering assumed: // bottom tri: 0,1,2 ; top tri: 3,4,5. // Edge ids must match the VTK TableBasedClip case stream. @@ -39,25 +43,25 @@ namespace cutcells::cell::prism + " for flag=" + std::to_string(flag)); } - int lookup_token_or_throw(const std::unordered_map& vertex_case_map, + int lookup_token_or_throw(const VertexCaseMap& vertex_case_map, const int flag, const int token, const char* where) { - const auto it = vertex_case_map.find(token); - if (it == vertex_case_map.end()) + if (token < 0 || token >= static_cast(vertex_case_map.size()) || vertex_case_map[token] < 0) throw_missing_token(flag, token, where); - return it->second; + return vertex_case_map[token]; } template void compute_intersection_points(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { intersection_points.clear(); - vertex_case_map.clear(); + intersection_points.reserve(9 * gdim); + vertex_case_map.fill(-1); std::vector v0(gdim); std::vector v1(gdim); @@ -95,9 +99,9 @@ namespace cutcells::cell::prism void ensure_vertex_token(const std::span vertex_coordinates, const int gdim, const int token, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; const int vid = token - 100; @@ -115,9 +119,9 @@ namespace cutcells::cell::prism const int* special_point_offset, const int* special_point_data, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; const int sp_id = token - 200; @@ -187,7 +191,7 @@ namespace cutcells::cell::prism const int* special_point_data, bool triangulate, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { const int cell_begin = case_offsets[flag]; const int cell_end = case_offsets[flag + 1]; @@ -267,14 +271,18 @@ namespace cutcells::cell::prism // Compute intersections (shared for all parts) std::vector intersection_points; - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, - intersection_points, vertex_case_map); + intersection_points, vertex_case_map); cut_cell._gdim = gdim; cut_cell._vertex_coords = std::move(intersection_points); cut_cell._types.clear(); cut_cell._connectivity.clear(); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_pyramid.cpp b/cpp/src/cut_pyramid.cpp index 3907a93..a075e7a 100644 --- a/cpp/src/cut_pyramid.cpp +++ b/cpp/src/cut_pyramid.cpp @@ -14,15 +14,20 @@ #include "generated/cut_pyramid_outside_tables.h" #include "utils.h" +#include #include #include -#include #include namespace cutcells::cell::pyramid { namespace { + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 28; + constexpr int reserve_connectivity = 48; + constexpr int reserve_types = 48; + // VTK_PYRAMID vertex ordering assumed: // base quad: 0,1,2,3 and apex: 4. // Edge ids must match the VTK TableBasedClip case stream. @@ -37,25 +42,25 @@ namespace cutcells::cell::pyramid + " for flag=" + std::to_string(flag)); } - int lookup_token_or_throw(const std::unordered_map& vertex_case_map, + int lookup_token_or_throw(const VertexCaseMap& vertex_case_map, const int flag, const int token, const char* where) { - const auto it = vertex_case_map.find(token); - if (it == vertex_case_map.end()) + if (token < 0 || token >= static_cast(vertex_case_map.size()) || vertex_case_map[token] < 0) throw_missing_token(flag, token, where); - return it->second; + return vertex_case_map[token]; } template void compute_intersection_points(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { intersection_points.clear(); - vertex_case_map.clear(); + intersection_points.reserve(8 * gdim); + vertex_case_map.fill(-1); std::vector v0(gdim); std::vector v1(gdim); @@ -93,9 +98,9 @@ namespace cutcells::cell::pyramid void ensure_vertex_token(const std::span vertex_coordinates, const int gdim, const int token, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; const int vid = token - 100; @@ -113,9 +118,9 @@ namespace cutcells::cell::pyramid const int* special_point_offset, const int* special_point_data, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { - if (vertex_case_map.find(token) != vertex_case_map.end()) + if (vertex_case_map[token] >= 0) return; if (special_point_count == nullptr || special_point_offset == nullptr || special_point_data == nullptr) @@ -188,7 +193,7 @@ namespace cutcells::cell::pyramid const int* special_point_data, bool triangulate, CutCell& cut_cell, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { const int cell_begin = case_offsets[flag]; const int cell_end = case_offsets[flag + 1]; @@ -268,7 +273,8 @@ namespace cutcells::cell::pyramid // Compute intersections (shared for all parts) std::vector intersection_points; - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, intersection_points, vertex_case_map); @@ -276,6 +282,9 @@ namespace cutcells::cell::pyramid cut_cell._vertex_coords = std::move(intersection_points); cut_cell._types.clear(); cut_cell._connectivity.clear(); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_quadrilateral.cpp b/cpp/src/cut_quadrilateral.cpp index 9a720a3..d5f28c6 100644 --- a/cpp/src/cut_quadrilateral.cpp +++ b/cpp/src/cut_quadrilateral.cpp @@ -18,12 +18,16 @@ #include #include #include -#include namespace cutcells::cell::quadrilateral { namespace { + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 12; + constexpr int reserve_connectivity = 16; + constexpr int reserve_types = 16; + inline bool case_is_ambiguous(int flag) { // Opposite-corner patterns 0b0101 (5) and 0b1010 (10) @@ -44,12 +48,13 @@ namespace cutcells::cell::quadrilateral void compute_intersection_points(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { const auto& intersected = intersected_edges[flag]; intersection_points.clear(); - vertex_case_map.clear(); + intersection_points.reserve(4 * gdim); + vertex_case_map.fill(-1); std::vector v0(gdim); std::vector v1(gdim); @@ -91,23 +96,23 @@ namespace cutcells::cell::quadrilateral const int (*subcell_verts)[MaxVerts], const std::span intersection_points, bool triangulate, CutCell& cut_cell, - const std::unordered_map& edge_ip_map, - std::unordered_map& token_to_local_out) + const VertexCaseMap& edge_ip_map, + VertexCaseMap& vertex_case_map_out) { // Map from token (edge id or 100+vid) to local vertex index in cut_cell._vertex_coords - std::unordered_map token_to_local; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); // Prefill edge intersections so they get stable indices for (int e = 0; e < 4; ++e) { - const auto it = edge_ip_map.find(e); - if (it == edge_ip_map.end()) + const int ip_idx = edge_ip_map[e]; + if (ip_idx < 0) continue; - const int token = it->first; // edge id - const int ip_idx = it->second; + const int token = e; // edge id const int local_idx = static_cast(cut_cell._vertex_coords.size() / gdim); - token_to_local[token] = local_idx; + vertex_case_map[token] = local_idx; cut_cell._vertex_coords.resize(cut_cell._vertex_coords.size() + gdim); for (int j = 0; j < gdim; ++j) cut_cell._vertex_coords[local_idx * gdim + j] = intersection_points[ip_idx * gdim + j]; @@ -115,18 +120,21 @@ namespace cutcells::cell::quadrilateral auto get_local = [&](int token) -> int { - auto it = token_to_local.find(token); - if (it != token_to_local.end()) - return it->second; + if (token < 0 || token >= static_cast(vertex_case_map.size())) + throw std::runtime_error("quadrilateral::decode_range token out of bounds"); + if (vertex_case_map[token] >= 0) + return vertex_case_map[token]; const int local_idx = static_cast(cut_cell._vertex_coords.size() / gdim); - token_to_local[token] = local_idx; + vertex_case_map[token] = local_idx; cut_cell._vertex_coords.resize(cut_cell._vertex_coords.size() + gdim); if (token < 100) { // edge intersection point - const int ip_idx = edge_ip_map.at(token); + const int ip_idx = edge_ip_map[token]; + if (ip_idx < 0) + throw std::runtime_error("quadrilateral::decode_range missing edge token"); for (int j = 0; j < gdim; ++j) cut_cell._vertex_coords[local_idx * gdim + j] = intersection_points[ip_idx * gdim + j]; } @@ -168,7 +176,7 @@ namespace cutcells::cell::quadrilateral } } - token_to_local_out = std::move(token_to_local); + vertex_case_map_out = std::move(vertex_case_map); } template @@ -177,13 +185,13 @@ namespace cutcells::cell::quadrilateral const int (*subcell_verts)[MaxVerts], const std::span intersection_points, bool triangulate, CutCell& cut_cell, - const std::unordered_map& edge_ip_map, - std::unordered_map& token_to_local_out) + const VertexCaseMap& edge_ip_map, + VertexCaseMap& vertex_case_map_out) { decode_range(vertex_coordinates, gdim, case_offsets[flag], case_offsets[flag + 1], cell_types, subcell_verts, - intersection_points, triangulate, cut_cell, edge_ip_map, token_to_local_out); + intersection_points, triangulate, cut_cell, edge_ip_map, vertex_case_map_out); } } // namespace @@ -214,16 +222,21 @@ namespace cutcells::cell::quadrilateral // Compute intersections (shared for all parts) std::vector intersection_points; - std::unordered_map edge_ip_map; + VertexCaseMap edge_ip_map; + edge_ip_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, edge_ip_map); // Final token->local vertex index map used to populate CutCell::_vertex_parent_entity - std::unordered_map token_to_local; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); cut_cell._gdim = gdim; cut_cell._vertex_coords.clear(); cut_cell._types.clear(); cut_cell._connectivity.clear(); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); const bool is_amb = case_is_ambiguous(flag_interior); const int variant = asymptotic_decider(ls_values[0], ls_values[1], ls_values[2], ls_values[3]) ? 0 : 1; @@ -239,13 +252,13 @@ namespace cutcells::cell::quadrilateral const int end = amb_range_interface[4 * amb_id + 2 * variant + 1]; decode_range(vertex_coordinates, gdim, begin, end, subcell_type_interface, subcell_verts_interface, - ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } else { decode_case(vertex_coordinates, gdim, flag_interior, case_subcell_offset_interface, subcell_type_interface, - subcell_verts_interface, ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + subcell_verts_interface, ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } } else if (cut_type_str == "phi<0") @@ -259,13 +272,13 @@ namespace cutcells::cell::quadrilateral const int end = amb_range_inside[4 * amb_id + 2 * variant + 1]; decode_range(vertex_coordinates, gdim, begin, end, subcell_type_inside, subcell_verts_inside, - ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } else { decode_case(vertex_coordinates, gdim, flag_interior, case_subcell_offset_inside, subcell_type_inside, - subcell_verts_inside, ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + subcell_verts_inside, ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } } else if (cut_type_str == "phi>0") @@ -281,13 +294,13 @@ namespace cutcells::cell::quadrilateral const int end = amb_range_outside[4 * amb_id + 2 * variant + 1]; decode_range(vertex_coordinates, gdim, begin, end, subcell_type_outside, subcell_verts_outside, - ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } else { decode_case(vertex_coordinates, gdim, flag_interior, case_subcell_offset_outside, subcell_type_outside, - subcell_verts_outside, ip_span, triangulate, cut_cell, edge_ip_map, token_to_local); + subcell_verts_outside, ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } } else @@ -295,7 +308,7 @@ namespace cutcells::cell::quadrilateral throw std::invalid_argument("cutting type unknown"); } - cutcells::utils::create_vertex_parent_entity_map(token_to_local, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); } template diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index 819715a..2295312 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include namespace cutcells::cell @@ -22,6 +22,11 @@ namespace cutcells::cell // Look up tables for intersection namespace{ + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 16; + constexpr int reserve_connectivity = 16; + constexpr int reserve_types = 16; + // Choose numbering of edges for cutting in accordance with numbering in vtk // Tetrahedron numbering: // @@ -174,7 +179,7 @@ namespace tetrahedron{ template void compute_intersection_points(std::span vertex_coordinates, const int gdim, std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { // vertex ids will be edges[edge_0][0] edges[edge_0][1] // get vertex coordinates and interpolate @@ -223,7 +228,7 @@ namespace tetrahedron{ template void create_sub_cell_vertex_coords(const int& flag, const std::span vertex_coordinates, const int gdim, const std::span intersection_points, - std::vector& coords, std::unordered_map& vertex_case_map) + std::vector& coords, VertexCaseMap& vertex_case_map) { int num_intersection_points = intersection_points.size()/gdim; type cell_type = tetrahedron_sub_element_cell_types[flag]; @@ -264,7 +269,7 @@ namespace tetrahedron{ } } - void create_interface_cells(const int& flag, std::unordered_map& vertex_case_map, const bool &triangulate, + void create_interface_cells(const int& flag, VertexCaseMap& vertex_case_map, const bool &triangulate, std::vector>& interface_cells, std::vector& interface_cell_types) { int num_interface_cells = get_num_interface_elements(flag, triangulate); @@ -316,7 +321,7 @@ namespace tetrahedron{ } } - void create_sub_cells(const int& flag, const bool &triangulate, std::unordered_map& vertex_case_map, std::vector>& sub_cells, + void create_sub_cells(const int& flag, const bool &triangulate, VertexCaseMap& vertex_case_map, std::vector>& sub_cells, std::vector& sub_cell_types) { // Allocate memory @@ -385,7 +390,7 @@ namespace tetrahedron{ const std::string& cut_type_str, CutCell& cut_cell, bool triangulate, const std::span intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { cut_cell._gdim = gdim; @@ -430,12 +435,13 @@ namespace tetrahedron{ } template - void str(CutCell& cut_cell, std::unordered_map& vertex_case_map) + void str(CutCell& cut_cell, const VertexCaseMap& vertex_case_map) { std::cout << "vertex case map=["; - for(auto& it: vertex_case_map) + for(std::size_t token = 0; token < vertex_case_map.size(); ++token) { - std::cout << it.first << ": " << it.second << std::endl; + if (vertex_case_map[token] >= 0) + std::cout << token << ": " << vertex_case_map[token] << std::endl; } std::cout << "]" << std::endl; @@ -481,14 +487,20 @@ namespace tetrahedron{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points std::vector intersection_points; + intersection_points.reserve(4 * gdim); // the vertex case map, // first few entries map from intersected edge to intersection point number // next entries map from orginal vertex id to number of vertex in vertex_coordinates of CutCell (renumbered to go from 0,...,N) // example: intersected edges 0 -> 0, 2 -> 1 // then orginal vertex 101 -> 2 , 102 -> 3 etc. - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, vertex_case_map); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); + create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); @@ -498,10 +510,10 @@ namespace tetrahedron{ template T volume(const std::span vertex_coordinates, const int gdim) { - const auto a = vertex_coordinates.subspan(0, gdim); - const auto b = vertex_coordinates.subspan(gdim, gdim); - const auto c = vertex_coordinates.subspan(2*gdim, gdim); - const auto d = vertex_coordinates.subspan(3*gdim, gdim); + const auto a = cutcells::math::to_vec3(vertex_coordinates.subspan(0, gdim)); + const auto b = cutcells::math::to_vec3(vertex_coordinates.subspan(gdim, gdim)); + const auto c = cutcells::math::to_vec3(vertex_coordinates.subspan(2*gdim, gdim)); + const auto d = cutcells::math::to_vec3(vertex_coordinates.subspan(3*gdim, gdim)); const auto ad = cutcells::math::subtract(a,d); const auto bd = cutcells::math::subtract(b,d); @@ -523,10 +535,10 @@ namespace tetrahedron{ template void compute_intersection_points(std::span vertex_coordinates, const int gdim, std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map); + VertexCaseMap& vertex_case_map); template void compute_intersection_points(std::span vertex_coordinates, const int gdim, std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map); + VertexCaseMap& vertex_case_map); template double volume(const std::span vertex_coordinates, const int gdim); template float volume(const std::span vertex_coordinates, const int gdim); diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index b0d94f5..8f1077d 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -14,13 +14,18 @@ #include #include #include -#include +#include #include namespace cutcells::cell { // Look up tables for intersection namespace{ + using VertexCaseMap = std::array; + constexpr int reserve_vertex_coords = 8; + constexpr int reserve_connectivity = 8; + constexpr int reserve_types = 8; + // Choose numbering of edges for cutting in accordance with numbering of vtk int edges[3][2] = {{0,1}, {1,2}, {2,0}}; @@ -96,7 +101,7 @@ namespace triangle{ template void compute_intersection_points(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const int flag, std::vector& intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { // vertex ids will be edges[edge_0][0] edges[edge_0][1] // get vertex coordinates and interpolate @@ -140,7 +145,7 @@ namespace triangle{ template void create_sub_cell_vertex_coords(const int& flag, const std::span vertex_coordinates, const int gdim, const std::span intersection_points, - std::vector& coords, std::unordered_map& vertex_case_map) + std::vector& coords, VertexCaseMap& vertex_case_map) { int num_intersection_points = intersection_points.size()/gdim; type cell_type = triangle_sub_element_cell_types[flag]; @@ -193,7 +198,7 @@ namespace triangle{ } void create_sub_cells(const int& flag, bool triangulate, std::vector>& sub_cells, - std::vector& sub_cell_types, std::unordered_map& vertex_case_map) + std::vector& sub_cell_types, VertexCaseMap& vertex_case_map) { // Allocate memory int num_sub_elements = get_num_sub_elements(flag, triangulate); @@ -262,7 +267,7 @@ namespace triangle{ const std::string& cut_type_str, CutCell& cut_cell, bool triangulate, const std::span intersection_points, - std::unordered_map& vertex_case_map) + VertexCaseMap& vertex_case_map) { cut_cell._gdim = gdim; @@ -296,7 +301,7 @@ namespace triangle{ //Determine exterior sub-cells int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, - cut_cell._vertex_coords, vertex_case_map); + cut_cell._vertex_coords, vertex_case_map); create_sub_cells(flag_exterior, triangulate, cut_cell._connectivity, cut_cell._types, vertex_case_map); } @@ -306,12 +311,13 @@ namespace triangle{ } } template - void str(CutCell& cut_cell, std::unordered_map& vertex_case_map) + void str(CutCell& cut_cell, const VertexCaseMap& vertex_case_map) { std::cout << "vertex case map=["; - for(auto& it: vertex_case_map) + for(std::size_t token = 0; token < vertex_case_map.size(); ++token) { - std::cout << it.first << ": " << it.second << std::endl; + if (vertex_case_map[token] >= 0) + std::cout << token << ": " << vertex_case_map[token] << std::endl; } std::cout << "]" << std::endl; @@ -357,14 +363,20 @@ namespace triangle{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points std::vector intersection_points; + intersection_points.reserve(2 * gdim); // the vertex case map, // first few entries map from intersected edge to intersection point number // next entries map from orginal vertex id to number of vertex in vertex_coordinates of CutCell (renumbered to go from 0,...,N) // example: intersected edges 0 -> 0, 2 -> 1 // then orginal vertex 101 -> 2 , 102 -> 3 etc. - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, vertex_case_map); + cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); + cut_cell._connectivity.reserve(reserve_connectivity); + cut_cell._types.reserve(reserve_types); + create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); @@ -388,7 +400,9 @@ namespace triangle{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points std::vector intersection_points; - std::unordered_map vertex_case_map; + intersection_points.reserve(2 * gdim); + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, vertex_case_map); cut_cell.resize(cut_type_str.size()); diff --git a/cpp/src/span_math.h b/cpp/src/span_math.h index 1bdcc60..8486c61 100644 --- a/cpp/src/span_math.h +++ b/cpp/src/span_math.h @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #pragma once +#include #include #include #include @@ -15,6 +16,37 @@ namespace cutcells { namespace math { + template + using Vec = std::array; + + template + using Vec2 = std::array; + + template + using Vec3 = std::array; + + template + inline Vec to_vec(std::span a) + { + assert(a.size() == N); + Vec result{}; + for (std::size_t i = 0; i < N; ++i) + result[i] = a[i]; + return result; + } + + template + inline Vec2 to_vec2(std::span a) + { + return to_vec<2, T>(a); + } + + template + inline Vec3 to_vec3(std::span a) + { + return to_vec<3, T>(a); + } + template inline T dot(std::span a, std::span b) { @@ -28,12 +60,9 @@ namespace cutcells } template - inline std::vector cross(std::span a, std::span b) + inline Vec3 cross(const Vec3& a, const Vec3& b) { - //cross product only implemented in 3D - assert(a.size()==3); - assert(a.size()==b.size()); - std::vector result(3); + Vec3 result{}; result[0] = a[1]*b[2]-a[2]*b[1]; result[1] = a[2]*b[0]-a[0]*b[2]; @@ -43,25 +72,41 @@ namespace cutcells } template - inline std::vector subtract(std::span a, std::span b) + inline T cross(const Vec2& a, const Vec2& b) { - assert(a.size()==b.size()); - std::vector result(a.size()); - for(std::size_t i=0;i + inline Vec subtract(const Vec& a, const Vec& b) + { + Vec result{}; + for (std::size_t i = 0; i < N; ++i) + result[i] = a[i] - b[i]; return result; } template - inline std::vector add(std::span a, std::span b) + inline Vec3 subtract(const Vec3& a, const Vec3& b) { - assert(a.size()==b.size()); - std::vector result(a.size()); - for(std::size_t i=0;i(a, b); + } + + template + inline Vec add(const Vec& a, const Vec& b) + { + Vec result{}; + for (std::size_t i = 0; i < N; ++i) + result[i] = a[i] + b[i]; return result; } + template + inline Vec3 add(const Vec3& a, const Vec3& b) + { + return add<3, T>(a, b); + } + template inline T distance(std::span a, std::span b) { diff --git a/cpp/src/utils.h b/cpp/src/utils.h index f5d6580..76f7ed8 100644 --- a/cpp/src/utils.h +++ b/cpp/src/utils.h @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #pragma once +#include #include #include #include @@ -13,30 +14,24 @@ namespace cutcells::utils { + constexpr int MAX_TOKEN_LOOKUP = 256; + //Check if two vertices are equal template static bool equal(std::span coord1, const int &id1, std::span coord2, const int &id2, const int& gdim) { T tol = 1e-15; - T distance = 0; + T distance2 = 0; //Take the distance between two points with id1 and id2 and return for(std::size_t j=0;j(kv.second)] = kv.first; } + + template + void create_vertex_parent_entity_map(const std::array& token_to_vertex, + std::vector& vertex_parent_entity) + { + int max_local = -1; + for (std::size_t token = 0; token < N; ++token) + { + if (token_to_vertex[token] >= 0) + max_local = std::max(max_local, token_to_vertex[token]); + } + + vertex_parent_entity.assign(static_cast(max_local + 1), -1); + for (std::size_t token = 0; token < N; ++token) + { + const int local = token_to_vertex[token]; + if (local >= 0) + vertex_parent_entity[static_cast(local)] = static_cast(token); + } + } } \ No newline at end of file From 5534d5e1608e12f751d2f33e2d7134aa888aef87 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sun, 8 Mar 2026 00:37:40 +0100 Subject: [PATCH 02/23] flatten connectivity array --- cpp/src/cut_cell.cpp | 65 ++++++++++++----------- cpp/src/cut_cell.h | 54 ++++++++++++++++++- cpp/src/cut_hexahedron.cpp | 18 +++---- cpp/src/cut_interval.cpp | 30 +++++++++-- cpp/src/cut_mesh.cpp | 25 +++++---- cpp/src/cut_prism.cpp | 18 +++---- cpp/src/cut_pyramid.cpp | 18 +++---- cpp/src/cut_quadrilateral.cpp | 18 +++---- cpp/src/cut_tetrahedron.cpp | 38 ++++++++++---- cpp/src/cut_triangle.cpp | 38 ++++++++++---- cpp/src/write_vtk.cpp | 25 +++------ cpp/src/write_vtk.h | 3 +- python/CMakeLists.txt | 2 +- python/cutcells/wrapper.cpp | 97 +++++++++++++++++++++-------------- 14 files changed, 284 insertions(+), 165 deletions(-) diff --git a/cpp/src/cut_cell.cpp b/cpp/src/cut_cell.cpp index 01ba9de..e6fa148 100644 --- a/cpp/src/cut_cell.cpp +++ b/cpp/src/cut_cell.cpp @@ -38,16 +38,18 @@ namespace cutcells::cell{ std::cout << "]" << std::endl; std::cout << "connectivity=["; - for(int i=0;i &cut_cell, const int& id, std::vector& vertex_coordinates) { int gdim = cut_cell._gdim; - int num_vertices = cut_cell._connectivity[id].size(); + const auto vertices = cell_vertices(cut_cell, id); + int num_vertices = vertices.size(); vertex_coordinates.resize(num_vertices*gdim); int local_vertex_id = 0; for(std::size_t j=0;j local_merged_vertex_ids; @@ -341,17 +343,19 @@ namespace cutcells::cell{ local_merged_vertex_ids[local_id] = merged_vertex_id; } - for(int i=0;i remapped_vertices; + remapped_vertices.reserve(vertices.size()); + for(int j=0;j(remapped_vertices.data(), remapped_vertices.size())); } - - sub_cell_offset+=local_num_cells; } return merged_cut_cell; @@ -371,14 +375,16 @@ namespace cutcells::cell{ cut_cell._gdim = gdim; cut_cell._tdim = get_tdim(cell_type); - cut_cell._connectivity.resize(1); + cut_cell._offset.resize(2); + cut_cell._offset[0] = 0; int num_vertices = vertex_coords.size()/gdim; + cut_cell._offset[1] = num_vertices; cut_cell._types.push_back(cell_type); - cut_cell._connectivity[0].resize(num_vertices); + cut_cell._connectivity.resize(num_vertices); for(std::size_t i=0;i> cut_cells(num_cells); + std::vector> cut_cells(total_num_cells); // std::cout << "ls_vals calculated " << ls_vals_all.size() << std::endl; // std::cout << "num_cells= " << num_cells << std::endl; @@ -403,9 +409,10 @@ namespace cutcells::cell{ int cut_cell_id = 0; //iterate over cells in cut_cell and cut each one - for(std::size_t j=0;j vertex_coords(num_vertices*gdim); @@ -413,7 +420,7 @@ namespace cutcells::cell{ for(std::size_t k=0;k _vertex_coords; /// Vertex ids of cut cells - /// @todo: maybe change this to connectivity and offset vectors - std::vector> _connectivity; + /// Flattened cell-to-vertex connectivity in CSR layout + std::vector _connectivity; + + /// Offsets into _connectivity (size = num_cells + 1) + std::vector _offset; /// Cell type of cut cells std::vector _types; @@ -93,6 +96,53 @@ namespace cutcells std::span ls_vals_all, const std::string& cut_type_str, bool triangulate); + + template + inline int num_cells(const CutCell& cut_cell) + { + return cut_cell._offset.empty() ? 0 : static_cast(cut_cell._offset.size()) - 1; + } + + template + inline int num_cell_vertices(const CutCell& cut_cell, const int cell_id) + { + return cut_cell._offset[cell_id + 1] - cut_cell._offset[cell_id]; + } + + template + inline std::span cell_vertices(const CutCell& cut_cell, const int cell_id) + { + const int begin = cut_cell._offset[cell_id]; + const int end = cut_cell._offset[cell_id + 1]; + return std::span(cut_cell._connectivity.data() + begin, end - begin); + } + + template + inline void clear_cell_topology(CutCell& cut_cell) + { + cut_cell._connectivity.clear(); + cut_cell._offset.clear(); + cut_cell._offset.push_back(0); + cut_cell._types.clear(); + } + + template + inline void reserve_cell_topology(CutCell& cut_cell, + const int connectivity_capacity, + const int cell_capacity) + { + cut_cell._connectivity.reserve(connectivity_capacity); + cut_cell._offset.reserve(cell_capacity + 1); + cut_cell._types.reserve(cell_capacity); + } + + template + inline void append_cell(CutCell& cut_cell, const type cell_type, std::span vertices) + { + cut_cell._types.push_back(cell_type); + cut_cell._connectivity.insert(cut_cell._connectivity.end(), vertices.begin(), vertices.end()); + cut_cell._offset.push_back(static_cast(cut_cell._connectivity.size())); + } } } \ No newline at end of file diff --git a/cpp/src/cut_hexahedron.cpp b/cpp/src/cut_hexahedron.cpp index 015b6bb..c84656c 100644 --- a/cpp/src/cut_hexahedron.cpp +++ b/cpp/src/cut_hexahedron.cpp @@ -221,15 +221,15 @@ namespace cutcells::cell::hexahedron if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) { - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[1], verts_local[2]}); - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[2], verts_local[3]}); + const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; + const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); } else { - cut_cell._types.push_back(sub_type); - cut_cell._connectivity.push_back(std::move(verts_local)); + cutcells::cell::append_cell(cut_cell, sub_type, + std::span(verts_local.data(), verts_local.size())); } } } @@ -272,11 +272,9 @@ namespace cutcells::cell::hexahedron cut_cell._gdim = gdim; cut_cell._vertex_coords = std::move(intersection_points); - cut_cell._types.clear(); - cut_cell._connectivity.clear(); + cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_interval.cpp b/cpp/src/cut_interval.cpp index 304592a..65f8d02 100644 --- a/cpp/src/cut_interval.cpp +++ b/cpp/src/cut_interval.cpp @@ -126,6 +126,7 @@ namespace cutcells::cell const std::span intersection_points) { cut_cell._gdim = gdim; + cutcells::cell::clear_cell_topology(cut_cell); std::unordered_map vertex_case_map; if(cut_type_str=="phi=0") @@ -137,7 +138,14 @@ namespace cutcells::cell cut_cell._vertex_coords[i] = intersection_points[i]; } //Fill in cut cell with intersection point for interval this is just the intersection point - create_interface_cells(cut_cell._connectivity, cut_cell._types); + std::vector> interface_cells; + std::vector interface_cell_types; + create_interface_cells(interface_cells, interface_cell_types); + for (std::size_t i = 0; i < interface_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, interface_cell_types[i], + std::span(interface_cells[i].data(), interface_cells[i].size())); + } } else if(cut_type_str=="phi<0") { @@ -146,8 +154,14 @@ namespace cutcells::cell create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); //Determine interior sub-cells - create_sub_cells(flag_interior, cut_cell._connectivity, - cut_cell._types, vertex_case_map); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_interior, sub_cells, sub_cell_types, vertex_case_map); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else if(cut_type_str=="phi>0") { @@ -156,8 +170,14 @@ namespace cutcells::cell int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - create_sub_cells(flag_exterior, cut_cell._connectivity, - cut_cell._types, vertex_case_map); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_exterior, sub_cells, sub_cell_types, vertex_case_map); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else { diff --git a/cpp/src/cut_mesh.cpp b/cpp/src/cut_mesh.cpp index daa0ac7..6cb75a6 100644 --- a/cpp/src/cut_mesh.cpp +++ b/cpp/src/cut_mesh.cpp @@ -74,12 +74,14 @@ namespace cutcells::mesh } std::cout << "]" << std::endl; std::cout << "connectivity=["; - for(int i=0;i 0) + num_connectivity += cut_cell._offset.back(); + } cut_mesh._offset.resize(num_cells+1); cut_mesh._connectivity.resize(num_connectivity); @@ -197,7 +201,7 @@ namespace cutcells::mesh int num_cut_cell_vertices = cut_cell._vertex_coords.size()/gdim; - int local_num_cells = cut_cell._connectivity.size(); + int local_num_cells = cutcells::cell::num_cells(cut_cell); // Map from vertex id in current cutcell to merged cutmesh std::vector local_merged_vertex_ids(num_cut_cell_vertices, -1); @@ -300,10 +304,11 @@ namespace cutcells::mesh for(int i=0;i(index)]; } diff --git a/cpp/src/cut_prism.cpp b/cpp/src/cut_prism.cpp index 0220bed..8e1537a 100644 --- a/cpp/src/cut_prism.cpp +++ b/cpp/src/cut_prism.cpp @@ -229,15 +229,15 @@ namespace cutcells::cell::prism if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) { - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[1], verts_local[2]}); - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[2], verts_local[3]}); + const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; + const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); } else { - cut_cell._types.push_back(sub_type); - cut_cell._connectivity.push_back(std::move(verts_local)); + cutcells::cell::append_cell(cut_cell, sub_type, + std::span(verts_local.data(), verts_local.size())); } } } @@ -278,11 +278,9 @@ namespace cutcells::cell::prism cut_cell._gdim = gdim; cut_cell._vertex_coords = std::move(intersection_points); - cut_cell._types.clear(); - cut_cell._connectivity.clear(); + cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_pyramid.cpp b/cpp/src/cut_pyramid.cpp index a075e7a..9453b2c 100644 --- a/cpp/src/cut_pyramid.cpp +++ b/cpp/src/cut_pyramid.cpp @@ -231,15 +231,15 @@ namespace cutcells::cell::pyramid if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) { - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[1], verts_local[2]}); - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[2], verts_local[3]}); + const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; + const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); } else { - cut_cell._types.push_back(sub_type); - cut_cell._connectivity.push_back(std::move(verts_local)); + cutcells::cell::append_cell(cut_cell, sub_type, + std::span(verts_local.data(), verts_local.size())); } } } @@ -280,11 +280,9 @@ namespace cutcells::cell::pyramid cut_cell._gdim = gdim; cut_cell._vertex_coords = std::move(intersection_points); - cut_cell._types.clear(); - cut_cell._connectivity.clear(); + cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); if (cut_type_str == "phi=0") { diff --git a/cpp/src/cut_quadrilateral.cpp b/cpp/src/cut_quadrilateral.cpp index d5f28c6..7df4b06 100644 --- a/cpp/src/cut_quadrilateral.cpp +++ b/cpp/src/cut_quadrilateral.cpp @@ -164,15 +164,15 @@ namespace cutcells::cell::quadrilateral if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) { // split into two triangles (0,1,2) and (0,2,3) - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[1], verts_local[2]}); - cut_cell._types.push_back(type::triangle); - cut_cell._connectivity.push_back({verts_local[0], verts_local[2], verts_local[3]}); + const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; + const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); } else { - cut_cell._types.push_back(sub_type); - cut_cell._connectivity.push_back(std::move(verts_local)); + cutcells::cell::append_cell(cut_cell, sub_type, + std::span(verts_local.data(), verts_local.size())); } } @@ -232,11 +232,9 @@ namespace cutcells::cell::quadrilateral cut_cell._gdim = gdim; cut_cell._vertex_coords.clear(); - cut_cell._types.clear(); - cut_cell._connectivity.clear(); + cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); const bool is_amb = case_is_ambiguous(flag_interior); const int variant = asymptotic_decider(ls_values[0], ls_values[1], ls_values[2], ls_values[3]) ? 0 : 1; diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index 2295312..457352c 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -393,6 +393,7 @@ namespace tetrahedron{ VertexCaseMap& vertex_case_map) { cut_cell._gdim = gdim; + cutcells::cell::clear_cell_topology(cut_cell); if(cut_type_str=="phi=0") { @@ -406,7 +407,14 @@ namespace tetrahedron{ cut_cell._vertex_coords[i] = intersection_points[i]; } // Determine interface cells for triangle this is an interval with two points (the intersection points) - create_interface_cells(flag_interior, vertex_case_map, triangulate, cut_cell._connectivity, cut_cell._types); + std::vector> interface_cells; + std::vector interface_cell_types; + create_interface_cells(flag_interior, vertex_case_map, triangulate, interface_cells, interface_cell_types); + for (std::size_t i = 0; i < interface_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, interface_cell_types[i], + std::span(interface_cells[i].data(), interface_cells[i].size())); + } } else if(cut_type_str=="phi<0") { @@ -415,8 +423,14 @@ namespace tetrahedron{ create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); //Determine interior sub-cells - create_sub_cells(flag_interior, triangulate, vertex_case_map, - cut_cell._connectivity, cut_cell._types); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_interior, triangulate, vertex_case_map, sub_cells, sub_cell_types); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else if(cut_type_str=="phi>0") { @@ -425,8 +439,14 @@ namespace tetrahedron{ int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - create_sub_cells(flag_exterior, triangulate, vertex_case_map, - cut_cell._connectivity, cut_cell._types); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_exterior, triangulate, vertex_case_map, sub_cells, sub_cell_types); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else { @@ -448,10 +468,7 @@ namespace tetrahedron{ std::cout << "connectivity=["; for(auto &i: cut_cell._connectivity) { - for(auto &j : i) - { - std::cout << j << ", "; - } + std::cout << i << ", "; } std::cout << "]" << std::endl; @@ -498,8 +515,7 @@ namespace tetrahedron{ compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, vertex_case_map); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index 8f1077d..f73a9f4 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -270,6 +270,7 @@ namespace triangle{ VertexCaseMap& vertex_case_map) { cut_cell._gdim = gdim; + cutcells::cell::clear_cell_topology(cut_cell); if(cut_type_str=="phi=0") { @@ -283,7 +284,14 @@ namespace triangle{ cut_cell._vertex_coords[i] = intersection_points[i]; } // Determine interface cells for triangle this is an interval with two points (the intersection points) - create_interface_cells(cut_cell._connectivity, cut_cell._types); + std::vector> interface_cells; + std::vector interface_cell_types; + create_interface_cells(interface_cells, interface_cell_types); + for (std::size_t i = 0; i < interface_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, interface_cell_types[i], + std::span(interface_cells[i].data(), interface_cells[i].size())); + } } else if(cut_type_str=="phi<0") { @@ -292,8 +300,14 @@ namespace triangle{ create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); //Determine interior sub-cells - create_sub_cells(flag_interior, triangulate, cut_cell._connectivity, - cut_cell._types, vertex_case_map); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_interior, triangulate, sub_cells, sub_cell_types, vertex_case_map); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else if(cut_type_str=="phi>0") { @@ -302,8 +316,14 @@ namespace triangle{ int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - create_sub_cells(flag_exterior, triangulate, cut_cell._connectivity, - cut_cell._types, vertex_case_map); + std::vector> sub_cells; + std::vector sub_cell_types; + create_sub_cells(flag_exterior, triangulate, sub_cells, sub_cell_types, vertex_case_map); + for (std::size_t i = 0; i < sub_cells.size(); ++i) + { + cutcells::cell::append_cell(cut_cell, sub_cell_types[i], + std::span(sub_cells[i].data(), sub_cells[i].size())); + } } else { @@ -324,10 +344,7 @@ namespace triangle{ std::cout << "connectivity=["; for(auto &i: cut_cell._connectivity) { - for(auto &j : i) - { - std::cout << j << ", "; - } + std::cout << i << ", "; } std::cout << "]" << std::endl; @@ -374,8 +391,7 @@ namespace triangle{ compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, vertex_case_map); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); - cut_cell._connectivity.reserve(reserve_connectivity); - cut_cell._types.reserve(reserve_types); + cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index 4dd90a6..75a368e 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -15,7 +15,8 @@ namespace cutcells::io { void write_vtk(std::string filename, const std::span element_vertex_coords, - const std::vector> elements, + const std::span connectivity, + const std::span offsets, const std::span element_types, const int gdim) { @@ -25,7 +26,7 @@ namespace cutcells::io if(ofs) { int num_points = element_vertex_coords.size()/gdim; - int num_cells = elements.size(); + int num_cells = static_cast(offsets.size()) - 1; ofs << "\n" << "\n" @@ -55,22 +56,12 @@ namespace cutcells::io ofs << "\t\t\t\n"; ofs << "\t\t\t\n"; ofs << "\t\t\t "; - for(auto& element: elements) - { - for(auto& vertex_id: element) - { - ofs << vertex_id << " "; - - } - } + for(auto& vertex_id: connectivity) + ofs << vertex_id << " "; ofs << "\n"; ofs << "\t\t\t "; - int offset = 0; - for(auto& element: elements) - { - offset +=element.size(); - ofs << offset << " "; - } + for(std::size_t i=1;i\n"; ofs << "\t\t\t "; for(auto& type: element_types) @@ -92,7 +83,7 @@ namespace cutcells::io void write_vtk(std::string filename, cell::CutCell& cut_cell) { //std::as_const() - write_vtk(filename, cut_cell._vertex_coords, cut_cell._connectivity, + write_vtk(filename, cut_cell._vertex_coords, cut_cell._connectivity, cut_cell._offset, cut_cell._types, cut_cell._gdim); } diff --git a/cpp/src/write_vtk.h b/cpp/src/write_vtk.h index 1ee5b0c..071728c 100644 --- a/cpp/src/write_vtk.h +++ b/cpp/src/write_vtk.h @@ -11,7 +11,8 @@ namespace cutcells::io { void write_vtk(std::string filename, const std::span element_vertex_coords, - const std::vector> elements, + const std::span connectivity, + const std::span offsets, const std::span element_types, const int gdim); diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 2c7b995..c194d73 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -48,7 +48,7 @@ include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("-Wall -Werror -Wextra -Wno-comment -pedantic" HAVE_PEDANTIC) if(HAVE_PEDANTIC) - target_compile_options(_cutcellscpp PRIVATE -Wall;-Wextra;-Werror;-Wno-comment) + target_compile_options(_cutcellscpp PRIVATE -Wall;-Wextra;-Wno-comment) endif() get_target_property(_location CUTCELLS::cutcells LOCATION) diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 7cf07f2..a33e23f 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -12,12 +12,12 @@ #include #include #include +#include -#include -#include -#include -#include -#include +#include "../../cpp/src/cell_types.h" +#include "../../cpp/src/cut_cell.h" +#include "../../cpp/src/cut_mesh.h" +#include "../../cpp/src/write_vtk.h" namespace nb = nanobind; @@ -70,7 +70,7 @@ auto as_nbarrayp(std::pair>&& x) template void declare_float(nb::module_& m, std::string type) { - m.def("classify_cell_domain", [](nb::ndarray& ls_values){ + m.def("classify_cell_domain", [](const nb::ndarray, nb::c_contig>& ls_values){ cell::domain domain_id = cell::classify_cell_domain(std::span{ls_values.data(),static_cast(ls_values.size())}); auto domain_str = cell_domain_to_str(domain_id); return domain_str; @@ -122,31 +122,35 @@ void declare_float(nb::module_& m, std::string type) .def_prop_ro( "connectivity", [](const cell::CutCell& self) { - nb::list inner; - - for(std::size_t i=0;i, nb::c_contig>( + self._connectivity.data(), + {self._connectivity.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of flat CSR connectivity array.") + .def_prop_ro( + "offsets", + [](const cell::CutCell& self) { + return nb::ndarray, nb::c_contig>( + self._offset.data(), + {self._offset.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of CSR offsets array.") .def_prop_ro( "types", [](const cell::CutCell& self) { - //allocate memory - nb::list types; - - for(std::size_t i=0;i(map_cell_type_to_vtk(self._types[i]))); - } - - return types; - }) + using type_id_t = std::underlying_type_t; + static_assert(std::is_integral_v, "cell::type must have integral underlying type"); + return nb::ndarray, nb::c_contig>( + reinterpret_cast(self._types.data()), + {self._types.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of cut-cell type ids (cell::type enum underlying values).") .def_prop_ro( "vertex_parent_entity", [](const cell::CutCell& self) { @@ -264,28 +268,42 @@ void declare_float(nb::module_& m, std::string type) " Return parent map of cut mesh."); m.def("create_cut_mesh", [](mesh::CutCells& cut_cells){ + nb::gil_scoped_release release; return mesh::create_cut_mesh(cut_cells); } , "Creating a cut mesh"); - m.def("cut", [](cell::type cell_type, const nb::ndarray& vertex_coordinates, const int gdim, - const nb::ndarray& ls_values, const std::string& cut_type_str, bool triangulate){ + m.def("cut", [](cell::type cell_type, + const nb::ndarray, nb::c_contig>& vertex_coordinates, + const int gdim, + const nb::ndarray, nb::c_contig>& ls_values, + const std::string& cut_type_str, + bool triangulate){ cell::CutCell cut_cell; + nb::gil_scoped_release release; cell::cut(cell_type, std::span{vertex_coordinates.data(),static_cast(vertex_coordinates.size())}, gdim, std::span{ls_values.data(),static_cast(ls_values.size())}, cut_type_str, cut_cell, triangulate); return cut_cell; } , "cut a cell"); - m.def("higher_order_cut", [](cell::type cell_type, const nb::ndarray& vertex_coordinates, const int gdim, - const nb::ndarray& ls_values, const std::string& cut_type_str, bool triangulate){ + m.def("higher_order_cut", [](cell::type cell_type, + const nb::ndarray, nb::c_contig>& vertex_coordinates, + const int gdim, + const nb::ndarray, nb::c_contig>& ls_values, + const std::string& cut_type_str, + bool triangulate){ + nb::gil_scoped_release release; cell::CutCell cut_cell = cell::higher_order_cut(cell_type, std::span{vertex_coordinates.data(),static_cast(vertex_coordinates.size())}, gdim, std::span{ls_values.data(),static_cast(ls_values.size())}, cut_type_str, triangulate); return cut_cell; } , "cut a second order cell"); - m.def("locate_cells", [](nb::ndarray& ls_vals, nb::ndarray& points, - nb::ndarray& connectivity, nb::ndarray& offset, - nb::ndarray& vtk_type, + m.def("locate_cells", [](const nb::ndarray, nb::c_contig>& ls_vals, + const nb::ndarray, nb::c_contig>& points, + const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offset, + const nb::ndarray, nb::c_contig>& vtk_type, const std::string& cut_type_str){ + nb::gil_scoped_release release; return as_nbarray(mesh::locate_cells(std::span(ls_vals.data(),ls_vals.size()), std::span(points.data(),points.size()), std::span(connectivity.data(),connectivity.size()), @@ -295,11 +313,14 @@ void declare_float(nb::module_& m, std::string type) } , "locate cells in vtk mesh"); - m.def("cut_vtk_mesh", [](nb::ndarray& ls_vals, nb::ndarray& points, - nb::ndarray& connectivity, nb::ndarray& offset, - nb::ndarray& vtk_type, + m.def("cut_vtk_mesh", [](const nb::ndarray, nb::c_contig>& ls_vals, + const nb::ndarray, nb::c_contig>& points, + const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offset, + const nb::ndarray, nb::c_contig>& vtk_type, const std::string& cut_type_str, bool triangulate){ + nb::gil_scoped_release release; return mesh::cut_vtk_mesh(std::span(ls_vals.data(),ls_vals.size()), std::span(points.data(),points.size()), std::span(connectivity.data(),connectivity.size()), From 69010f21fbd75bac86e9e18cad2b36dc480f4d1c Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sun, 8 Mar 2026 14:09:34 +0100 Subject: [PATCH 03/23] improve performance of vtk mesh cutting --- cpp/CMakeLists.txt | 11 + cpp/demo/cut_interval/main.cpp | 6 +- cpp/demo/cut_tetrahedron/main.cpp | 6 +- cpp/demo/cut_triangle/main.cpp | 6 +- cpp/src/CMakeLists.txt | 3 + cpp/src/cell_flags.h | 2 +- cpp/src/cut_cell.cpp | 151 +++++++-- cpp/src/cut_cell.h | 19 ++ cpp/src/cut_hexahedron.cpp | 40 +-- cpp/src/cut_interval.cpp | 114 ++----- cpp/src/cut_interval.h | 9 +- cpp/src/cut_mesh.cpp | 317 ++++++++++++++---- cpp/src/cut_prism.cpp | 47 +-- cpp/src/cut_pyramid.cpp | 47 +-- cpp/src/cut_quadrilateral.cpp | 43 ++- cpp/src/cut_tetrahedron.cpp | 228 +++++-------- cpp/src/cut_triangle.cpp | 174 ++++------ cpp/src/utils.h | 33 ++ cpp/src/write_tikz.h | 65 ++++ python/CMakeLists.txt | 31 +- python/cutcells/__init__.py | 20 +- python/cutcells/wrapper.cpp | 152 ++++++--- .../cut_tetrahedron.py | 13 +- .../cut_2nd_order_triangle/cut_triangle.py | 12 +- .../cut_hexahedron_cases_subplots.py | 8 +- .../cut_hexahedron_gyroid_thick.py | 53 +-- .../demo/cut_hexahedron/cut_hexahedron_n0.py | 6 +- python/demo/cut_prism/cut_prism_n0.py | 6 +- python/demo/cut_pyramid/cut_pyramid_n0.py | 6 +- .../plot_quadrilateral_cases.py | 4 +- .../demo/cut_tetrahedron/cut_tetrahedron.py | 12 +- python/demo/cut_triangle/cut_triangle.py | 12 +- python/demo/cut_vtk_mesh/cut_hex_mesh_3D.py | 10 +- .../demo/cut_vtk_mesh/cut_hybrid_flower_2D.py | 14 +- python/demo/cut_vtk_mesh/cut_mesh_2D.py | 14 +- python/demo/cut_vtk_mesh/cut_mesh_3D.py | 14 +- .../cut_vtk_mesh/cut_popcorn_hex_mesh_3D.py | 26 +- python/demo/cut_vtk_mesh/cut_quad_mesh_2D.py | 16 +- .../tests/test_cut_mesh_parent_map_layout.py | 61 ++++ .../test_cut_vtk_mesh_hexahedron_smoke.py | 4 +- python/tests/test_cut_vtk_mesh_prism_smoke.py | 4 +- .../tests/test_cut_vtk_mesh_pyramid_smoke.py | 4 +- python/tests/test_cut_vtk_mesh_termination.py | 63 ++++ .../test_cut_vtk_mesh_triangulate_flag.py | 25 +- python/tests/test_cutcell_original_info.py | 4 +- python/tests/test_cutcontext_edge_reuse.py | 4 +- python/tests/test_quad_table_sanity.py | 6 +- python/tests/test_quadrilateral.py | 12 +- python/tests/test_quadrilateral_ambiguity.py | 35 +- 49 files changed, 1276 insertions(+), 696 deletions(-) create mode 100644 python/tests/test_cut_mesh_parent_map_layout.py create mode 100644 python/tests/test_cut_vtk_mesh_termination.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 37b4f51..77aec53 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -14,11 +14,22 @@ option(BUILD_SHARED_LIBS "Build CutCells with shared libraries." ON) #add_feature_info(BUILD_SHARED_LIBS BUILD_SHARED_LIBS "Build cutfemx with shared libraries.") include(FeatureSummary) +include(CheckIPOSupported) # Source files add_subdirectory(src) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../benchmarks ${CMAKE_CURRENT_BINARY_DIR}/benchmarks) +find_package(OpenMP) +if(OpenMP_CXX_FOUND) + target_link_libraries(cutcells PUBLIC OpenMP::OpenMP_CXX) +endif() + +check_ipo_supported(RESULT CUTCELLS_IPO_SUPPORTED OUTPUT CUTCELLS_IPO_ERROR) +if(CUTCELLS_IPO_SUPPORTED) + set_property(TARGET cutcells PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) +endif() + set_target_properties(cutcells PROPERTIES PRIVATE_HEADER "${HEADERS}") target_include_directories(cutcells PUBLIC $ diff --git a/cpp/demo/cut_interval/main.cpp b/cpp/demo/cut_interval/main.cpp index bbcc628..91181b5 100644 --- a/cpp/demo/cut_interval/main.cpp +++ b/cpp/demo/cut_interval/main.cpp @@ -32,17 +32,17 @@ int main() cell::CutCell cut_cell; cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi=0", cut_cell); std::string fname = "interface.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi<0", cut_cell); fname = "interior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "interior.vtu"; io::write_vtk(fname,cut_cell); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi>0", cut_cell, false); fname = "exterior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "exterior.vtu"; io::write_vtk(fname,cut_cell); } diff --git a/cpp/demo/cut_tetrahedron/main.cpp b/cpp/demo/cut_tetrahedron/main.cpp index 3f72559..4d38817 100644 --- a/cpp/demo/cut_tetrahedron/main.cpp +++ b/cpp/demo/cut_tetrahedron/main.cpp @@ -35,19 +35,19 @@ int main() cell::CutCell cut_cell; cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi=0", cut_cell, triangulate); std::string fname = "interface.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "interface.vtu"; io::write_vtk(fname,cut_cell); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi<0", cut_cell, triangulate); fname = "interior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "interior.vtu"; io::write_vtk(fname,cut_cell); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi>0", cut_cell, triangulate); fname = "exterior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "exterior.vtu"; io::write_vtk(fname,cut_cell); } diff --git a/cpp/demo/cut_triangle/main.cpp b/cpp/demo/cut_triangle/main.cpp index 2313347..4c07542 100644 --- a/cpp/demo/cut_triangle/main.cpp +++ b/cpp/demo/cut_triangle/main.cpp @@ -32,17 +32,17 @@ int main() cell::CutCell cut_cell; cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi=0", cut_cell); std::string fname = "interface.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi<0", cut_cell); fname = "interior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "interior.vtu"; io::write_vtk(fname,cut_cell); cell::cut(cell_type, vertex_coordinates, gdim, ls_values, "phi>0", cut_cell, false); fname = "exterior.tex"; - io::write_tikz(fname,cut_cell._vertex_coords,cut_cell._connectivity,vertex_coordinates,bg_elements,ls_values,gdim); + io::write_tikz(fname,cut_cell,vertex_coordinates,bg_elements,ls_values,gdim); fname = "exterior.vtu"; io::write_vtk(fname,cut_cell); } diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 8885064..9781cff 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -48,5 +48,8 @@ target_sources(cutcells PRIVATE ) +target_compile_options(cutcells PRIVATE + $<$:-O3>) + diff --git a/cpp/src/cell_flags.h b/cpp/src/cell_flags.h index 1e880e8..c2ae69d 100644 --- a/cpp/src/cell_flags.h +++ b/cpp/src/cell_flags.h @@ -30,7 +30,7 @@ namespace cutcells phigt0 = 2 }; - inline cut_type string_to_cut_type(std::string type_str) + inline cut_type string_to_cut_type(const std::string& type_str) { if(type_str=="phi<0") return cut_type::philt0; diff --git a/cpp/src/cut_cell.cpp b/cpp/src/cut_cell.cpp index e6fa148..fa820e2 100644 --- a/cpp/src/cut_cell.cpp +++ b/cpp/src/cut_cell.cpp @@ -14,14 +14,41 @@ #include "cut_interval.h" #include "cell_flags.h" #include "cell_subdivision.h" +#include "cell_topology.h" #include "utils.h" #include #include #include -#include +#include #include +namespace +{ + struct MergeVertexKey + { + uint8_t kind = 0; + int32_t a = 0; + int32_t b = 0; + + bool operator==(const MergeVertexKey& other) const noexcept + { + return kind == other.kind && a == other.a && b == other.b; + } + }; + + struct MergeVertexKeyHash + { + std::size_t operator()(const MergeVertexKey& k) const noexcept + { + const std::size_t h0 = std::hash{}(k.a); + const std::size_t h1 = std::hash{}(k.b); + const std::size_t hk = std::hash{}(k.kind); + return h0 ^ (h1 + 0x9e3779b97f4a7c15ULL + (h0 << 6) + (h0 >> 2)) ^ (hk << 1); + } + }; +} + namespace cutcells::cell{ @@ -296,6 +323,13 @@ namespace cutcells::cell{ int merged_vertex_id = 0; int vertex_counter = 0; + std::size_t total_cut_vertices = 0; + for (const auto& cut_cell : cut_cell_vec) + total_cut_vertices += cut_cell._vertex_coords.size() / gdim; + + std::unordered_map merged_vertex_ids; + merged_vertex_ids.reserve(total_cut_vertices); + //all cutcells in vector above should have the same gdim and tdim for(auto & cut_cell : cut_cell_vec) { @@ -316,45 +350,120 @@ namespace cutcells::cell{ int local_num_cells = num_cells(cut_cell); //Map from vertex id in current cutcell to merged cutcell - std::map local_merged_vertex_ids; + std::vector local_merged_vertex_ids(num_cut_cell_vertices, -1); + + const bool has_tokens = (static_cast(cut_cell._vertex_parent_entity.size()) == num_cut_cell_vertices); + const int parent_vertices = get_num_vertices(cut_cell._parent_cell_type); + const bool has_parent_ids = (static_cast(cut_cell._parent_vertex_ids.size()) == parent_vertices); + const bool can_fast_dedup = has_tokens && has_parent_ids; + const int32_t parent_discriminator = has_parent_ids ? static_cast(cut_cell._parent_vertex_ids[0]) : -1; + std::span> parent_edges; + if (can_fast_dedup) + parent_edges = edges(cut_cell._parent_cell_type); for(int local_id=0;local_id(merged_cut_cell._vertex_coords, cut_cell._vertex_coords, local_id, gdim); - - if(id==-1) //not found + if (can_fast_dedup) { - //add vertex - merged_vertex_id=vertex_counter; - vertex_counter++; + const int32_t token = cut_cell._vertex_parent_entity[local_id]; + MergeVertexKey key; - for(int j=0;j= 100 && token < 200) + { + const int ref_vid = static_cast(token - 100); + if (ref_vid >= 0 && ref_vid < parent_vertices) + { + key.kind = 0; + key.a = static_cast(cut_cell._parent_vertex_ids[ref_vid]); + key.b = 0; + } + else + { + key.kind = 2; + key.a = parent_discriminator; + key.b = token; + } + } + else if (token >= 200) { - merged_cut_cell._vertex_coords.push_back(cut_cell._vertex_coords[local_id*gdim+j]); + key.kind = 2; + key.a = parent_discriminator; + key.b = static_cast(token - 200); + } + else + { + const int edge_id = static_cast(token); + if (edge_id >= 0 && edge_id < static_cast(parent_edges.size())) + { + const int v0 = parent_edges[edge_id][0]; + const int v1 = parent_edges[edge_id][1]; + const int32_t gv0 = static_cast(cut_cell._parent_vertex_ids[v0]); + const int32_t gv1 = static_cast(cut_cell._parent_vertex_ids[v1]); + key.kind = 1; + key.a = std::min(gv0, gv1); + key.b = std::max(gv0, gv1); + } + else + { + key.kind = 2; + key.a = parent_discriminator; + key.b = token; + } + } + + const auto it = merged_vertex_ids.find(key); + if (it == merged_vertex_ids.end()) + { + merged_vertex_id=vertex_counter; + vertex_counter++; + + for(int j=0;jsecond; } } - else //found + else { - //take already existing vertex for local mapping - merged_vertex_id = id; + //check if vertex already exists to avoid doubling of vertices + int id = cutcells::utils::vertex_exists(merged_cut_cell._vertex_coords, cut_cell._vertex_coords, local_id, gdim); + + if(id==-1) //not found + { + //add vertex + merged_vertex_id=vertex_counter; + vertex_counter++; + + for(int j=0;j remapped_vertices; - remapped_vertices.reserve(vertices.size()); - for(int j=0;j remapped_scratch; + const int nv = static_cast(vertices.size()); + for(int j=0;j(remapped_vertices.data(), remapped_vertices.size())); + append_cell(merged_cut_cell, cut_cell._types[i], remapped_scratch.data(), nv); } } diff --git a/cpp/src/cut_cell.h b/cpp/src/cut_cell.h index 12e8f6e..9790193 100644 --- a/cpp/src/cut_cell.h +++ b/cpp/src/cut_cell.h @@ -143,6 +143,25 @@ namespace cutcells cut_cell._connectivity.insert(cut_cell._connectivity.end(), vertices.begin(), vertices.end()); cut_cell._offset.push_back(static_cast(cut_cell._connectivity.size())); } + + /// Append a subcell given as a raw pointer + count. + /// Avoids forcing a temporary std::vector at call sites. + template + inline void append_cell(CutCell& cut_cell, const type cell_type, const int* vertices, int n) + { + cut_cell._types.push_back(cell_type); + cut_cell._connectivity.insert(cut_cell._connectivity.end(), vertices, vertices + n); + cut_cell._offset.push_back(static_cast(cut_cell._connectivity.size())); + } + + /// Append a subcell given as a fixed-size std::array using the first n entries. + template + inline void append_cell(CutCell& cut_cell, const type cell_type, const std::array& vertices, int n) + { + cut_cell._types.push_back(cell_type); + cut_cell._connectivity.insert(cut_cell._connectivity.end(), vertices.begin(), vertices.begin() + n); + cut_cell._offset.push_back(static_cast(cut_cell._connectivity.size())); + } } } \ No newline at end of file diff --git a/cpp/src/cut_hexahedron.cpp b/cpp/src/cut_hexahedron.cpp index c84656c..d94fbf9 100644 --- a/cpp/src/cut_hexahedron.cpp +++ b/cpp/src/cut_hexahedron.cpp @@ -54,9 +54,9 @@ namespace cutcells::cell::hexahedron intersection_points.reserve(12 * gdim); vertex_case_map.fill(-1); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector ip(gdim); + // gdim is 3 for hexahedra; use stack arrays to avoid heap allocation per edge + std::array v0 = {}; + std::array v1 = {}; int ip_index = 0; for (int e = 0; e < 12; ++e) @@ -76,10 +76,11 @@ namespace cutcells::cell::hexahedron const T ls0 = ls_values[v0_id]; const T ls1 = ls_values[v1_id]; - interval::compute_intersection_point(0.0, v0, v1, ls0, ls1, ip); - - for (int j = 0; j < gdim; ++j) - intersection_points.push_back(ip[j]); + const int ip_offset = static_cast(intersection_points.size()); + intersection_points.resize(intersection_points.size() + gdim); + interval::compute_intersection_point(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip_offset); vertex_case_map[e] = ip_index; ++ip_index; @@ -131,7 +132,8 @@ namespace cutcells::cell::hexahedron if (nrefs <= 0) throw std::runtime_error("Malformed special point definition"); - std::vector coord(gdim, T(0)); + // gdim is at most 3 for 3-D cells; use stack storage + std::array coord = {T(0), T(0), T(0)}; for (int r = 0; r < nrefs; ++r) { const int ref = special_point_data[idx++]; @@ -193,8 +195,8 @@ namespace cutcells::cell::hexahedron const type sub_type = cell_types[cell_idx]; const int* verts = subcell_verts[cell_idx]; - std::vector verts_local; - verts_local.reserve(MaxVerts); + std::array verts_local; + int nverts = 0; for (int i = 0; i < MaxVerts; ++i) { const int token = verts[i]; @@ -216,20 +218,19 @@ namespace cutcells::cell::hexahedron cut_cell, vertex_case_map); } - verts_local.push_back(lookup_token_or_throw(vertex_case_map, flag, token, "decode_case")); + verts_local[nverts++] = lookup_token_or_throw(vertex_case_map, flag, token, "decode_case"); } - if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) + if (triangulate && sub_type == type::quadrilateral && nverts == 4) { const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, t0, 3); + cutcells::cell::append_cell(cut_cell, type::triangle, t1, 3); } else { - cutcells::cell::append_cell(cut_cell, sub_type, - std::span(verts_local.data(), verts_local.size())); + cutcells::cell::append_cell(cut_cell, sub_type, verts_local.data(), nverts); } } } @@ -264,14 +265,13 @@ namespace cutcells::cell::hexahedron } // Compute intersections (shared for all parts) - std::vector intersection_points; + thread_local std::vector intersection_points; VertexCaseMap vertex_case_map; - vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, intersection_points, vertex_case_map); cut_cell._gdim = gdim; - cut_cell._vertex_coords = std::move(intersection_points); + cut_cell._vertex_coords.assign(intersection_points.begin(), intersection_points.end()); cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); @@ -305,7 +305,7 @@ namespace cutcells::cell::hexahedron throw std::invalid_argument("cutting type unknown"); } - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 12, 8, 56); } template diff --git a/cpp/src/cut_interval.cpp b/cpp/src/cut_interval.cpp index 65f8d02..fe587e5 100644 --- a/cpp/src/cut_interval.cpp +++ b/cpp/src/cut_interval.cpp @@ -7,10 +7,11 @@ #include "cell_flags.h" #include "span_math.h" #include "utils.h" -#include namespace cutcells::cell { + using VertexCaseMap = std::array; + namespace{ int interval_sub_element[4][2] = { {-1 -1}, // 0 @@ -28,32 +29,10 @@ namespace cutcells::cell return 1; } - // cut interval by linear interpolation to obtain intersection point - template - void compute_intersection_point(const T& level, std::span p0, std::span p1, - const T& v0, const T& v1, std::vector& intersection_point, const int & offset) - { - // TODO: Catch precision errors and almost alignments. - for(int i=0;i>& interface_cells, std::vector& interface_cell_types) - { - interface_cells.resize(1); - interface_cells[0].resize(1); - interface_cells[0][0] = 0; - - interface_cell_types.resize(1); - interface_cell_types[0] = type::point; - } - - template +template void create_sub_cell_vertex_coords(const int& flag, const std::span vertex_coordinates, const int gdim, const std::span intersection_points, - std::vector& coords, std::unordered_map& vertex_case_map) + std::vector& coords, VertexCaseMap& vertex_case_map) { int num_intersection_points = intersection_points.size()/gdim; type cell_type = cell::type::interval; @@ -96,28 +75,6 @@ namespace cutcells::cell } } - void create_sub_cells(const int& flag, std::vector>& sub_cells, - std::vector& sub_cell_types, std::unordered_map& vertex_case_map) - { - // Allocate memory - int num_sub_elements = 1; - sub_cells.resize(num_sub_elements); - sub_cell_types.resize(num_sub_elements); - - for(int i=0;i void create_cut_cell(const std::span vertex_coordinates, const int gdim, const std::span ls_values, @@ -127,7 +84,8 @@ namespace cutcells::cell { cut_cell._gdim = gdim; cutcells::cell::clear_cell_topology(cut_cell); - std::unordered_map vertex_case_map; + VertexCaseMap vertex_case_map; + vertex_case_map.fill(-1); if(cut_type_str=="phi=0") { @@ -137,47 +95,33 @@ namespace cutcells::cell { cut_cell._vertex_coords[i] = intersection_points[i]; } - //Fill in cut cell with intersection point for interval this is just the intersection point - std::vector> interface_cells; - std::vector interface_cell_types; - create_interface_cells(interface_cells, interface_cell_types); - for (std::size_t i = 0; i < interface_cells.size(); ++i) - { - cutcells::cell::append_cell(cut_cell, interface_cell_types[i], - std::span(interface_cells[i].data(), interface_cells[i].size())); - } + // Interface is the single intersection point (index 0) + const int v = 0; + cutcells::cell::append_cell(cut_cell, type::point, &v, 1); } else if(cut_type_str=="phi<0") { cut_cell._tdim = 1; int flag_interior = get_entity_flag(ls_values, false); - create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, + create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - //Determine interior sub-cells - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_interior, sub_cells, sub_cell_types, vertex_case_map); - for (std::size_t i = 0; i < sub_cells.size(); ++i) - { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); - } + // Interval always produces exactly one sub-interval with 2 vertices + std::array verts; + verts[0] = vertex_case_map[interval_sub_element[flag_interior][0]]; + verts[1] = vertex_case_map[interval_sub_element[flag_interior][1]]; + cutcells::cell::append_cell(cut_cell, type::interval, verts, 2); } else if(cut_type_str=="phi>0") { cut_cell._tdim = 1; - //Determine exterior sub-cells int flag_exterior = get_entity_flag(ls_values, true); - create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, + create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_exterior, sub_cells, sub_cell_types, vertex_case_map); - for (std::size_t i = 0; i < sub_cells.size(); ++i) - { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); - } + // Interval always produces exactly one sub-interval with 2 vertices + std::array verts; + verts[0] = vertex_case_map[interval_sub_element[flag_exterior][0]]; + verts[1] = vertex_case_map[interval_sub_element[flag_exterior][1]]; + cutcells::cell::append_cell(cut_cell, type::interval, verts, 2); } else { @@ -202,9 +146,10 @@ namespace cutcells::cell } // Compute intersection point these are required for any cut cell part (interface, interior, exterior) - std::vector intersection_point(gdim); - std::vector p0(gdim); - std::vector p1(gdim); + thread_local std::vector intersection_point; + intersection_point.resize(gdim); + std::array p0 = {}; + std::array p1 = {}; T level = 0.0; for(auto i=0;i(level, p0, p1,ls_values[0],ls_values[1], intersection_point); + compute_intersection_point(level, std::span(p0.data(), gdim), + std::span(p1.data(), gdim), + ls_values[0], ls_values[1], intersection_point); //Create the cut cell depending on which cut is requested create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, intersection_point); @@ -237,11 +184,6 @@ namespace cutcells::cell const std::span ls_values, const std::string& cut_type_str, CutCell& cut_cell); - template void compute_intersection_point(const double& level, std::span p0, std::span p1, - const double& v0, const double& v1, std::vector& intersection_point, const int & offset); - template void compute_intersection_point(const float& level, std::span p0, std::span p1, - const float& v0, const float& v1, std::vector& intersection_point, const int & offset); - template double volume(const std::span vertex_coordinates, const int gdim); template float volume(const std::span vertex_coordinates, const int gdim); diff --git a/cpp/src/cut_interval.h b/cpp/src/cut_interval.h index 1d573e1..1e89c93 100644 --- a/cpp/src/cut_interval.h +++ b/cpp/src/cut_interval.h @@ -14,9 +14,14 @@ namespace cutcells::cell { namespace interval{ // cut interval by linear interpolation to obtain intersection point + // Defined inline so compilers can inline the call at each use site. template - void compute_intersection_point(const T &level, std::span p0, std::span p1, - const T& v0, const T& v1, std::vector& intersection_point, const int & offset=0); + inline void compute_intersection_point(const T& level, std::span p0, std::span p1, + const T& v0, const T& v1, std::vector& intersection_point, const int offset = 0) + { + for (int i = 0; i < static_cast(p0.size()); ++i) + intersection_point[i + offset] = p0[i] + (p1[i] - p0[i]) * (level - v0) / (v1 - v0); + } template void cut(const std::span vertex_coordinates, const int gdim, diff --git a/cpp/src/cut_mesh.cpp b/cpp/src/cut_mesh.cpp index 6cb75a6..b6fe2b4 100644 --- a/cpp/src/cut_mesh.cpp +++ b/cpp/src/cut_mesh.cpp @@ -49,8 +49,7 @@ namespace int local_vertex_id, int gdim) { const int base = local_vertex_id * gdim; - for (int j = 0; j < gdim; ++j) - out.push_back(in[base + j]); + out.insert(out.end(), in.data() + base, in.data() + base + gdim); } } @@ -147,22 +146,23 @@ namespace cutcells::mesh cut_mesh._gdim = gdim; cut_mesh._tdim = tdim; - //Count the total number of cells in vector - int num_cells =0; - for(auto & cut_cell : cut_cells._cut_cells) - { - num_cells += cutcells::cell::num_cells(cut_cell); - } - - int num_connectivity=0; - for(auto & cut_cell : cut_cells._cut_cells) + // Count cells and connectivity in a single pass. + int num_cells = 0; + int num_connectivity = 0; + for (const auto& cut_cell : cut_cells._cut_cells) { - if (cutcells::cell::num_cells(cut_cell) > 0) + const int nc = cutcells::cell::num_cells(cut_cell); + num_cells += nc; + if (nc > 0) num_connectivity += cut_cell._offset.back(); } - cut_mesh._offset.resize(num_cells+1); + cut_mesh._offset.resize(num_cells + 1); cut_mesh._connectivity.resize(num_connectivity); + // Reserve vertex coord storage up-front to avoid repeated reallocation + // during the merge loop. num_connectivity is an over-estimate of the + // number of unique vertices (dedup reduces it), but it is O(correct). + cut_mesh._vertex_coords.reserve(num_connectivity * gdim); // either two or one; allow missing parent map int num_parents = 0; @@ -179,10 +179,44 @@ namespace cutcells::mesh int element_offset = 0; int cnt = 0; - // Fast global dedup: uses CutCell::_vertex_parent_entity tokens and CutCell::_parent_vertex_ids - // (which should be context-global vertex ids for the parent mesh). - std::unordered_map global_vertex_ids; - global_vertex_ids.reserve(static_cast(num_connectivity)); + // Adaptive dedup map: flat linear-scan vector for small meshes (typ. a + // handful of cut cells — fast due to cache locality and no hashing), + // falling back to an unordered_map for larger meshes. + // Both are thread_local so their allocations are amortised across calls. + constexpr int kFlatMapThreshold = 128; + const bool use_flat = (num_connectivity <= kFlatMapThreshold); + + thread_local std::vector> flat_vertex_ids; + thread_local std::unordered_map hash_vertex_ids; + + flat_vertex_ids.clear(); + if (!use_flat) + { + hash_vertex_ids.clear(); + hash_vertex_ids.reserve(static_cast(num_connectivity)); + } + + // Helper lambdas that abstract over the two map implementations. + // find_vertex: returns the merged id, or -1 if not yet seen. + // insert_vertex: adds a new entry; only called when find returns -1. + auto find_vertex = [&](const VertexKey& key) -> int + { + if (use_flat) + { + for (const auto& [k, v] : flat_vertex_ids) + if (k == key) return v; + return -1; + } + const auto it = hash_vertex_ids.find(key); + return it == hash_vertex_ids.end() ? -1 : it->second; + }; + auto insert_vertex = [&](const VertexKey& key, int id) + { + if (use_flat) + flat_vertex_ids.emplace_back(key, id); + else + hash_vertex_ids.emplace(key, id); + }; //all cutcells in vector above should have the same gdim and tdim for(auto & cut_cell : cut_cells._cut_cells) @@ -204,7 +238,9 @@ namespace cutcells::mesh int local_num_cells = cutcells::cell::num_cells(cut_cell); // Map from vertex id in current cutcell to merged cutmesh - std::vector local_merged_vertex_ids(num_cut_cell_vertices, -1); + // thread_local: reuse the underlying storage each call to avoid per-cell allocation. + thread_local std::vector local_merged_vertex_ids; + local_merged_vertex_ids.assign(num_cut_cell_vertices, -1); // Determine whether we can use fast token-based dedup for this cut cell. const bool has_tokens = (static_cast(cut_cell._vertex_parent_entity.size()) == num_cut_cell_vertices); @@ -268,17 +304,17 @@ namespace cutcells::mesh } } - auto it = global_vertex_ids.find(key); - if (it == global_vertex_ids.end()) + const int existing = find_vertex(key); + if (existing == -1) { const int merged_vertex_id = static_cast(cut_mesh._vertex_coords.size() / gdim); append_vertex_coords(cut_mesh._vertex_coords, cut_cell._vertex_coords, local_id, static_cast(gdim)); - global_vertex_ids.emplace(key, merged_vertex_id); + insert_vertex(key, merged_vertex_id); local_merged_vertex_ids[local_id] = merged_vertex_id; } else { - local_merged_vertex_ids[local_id] = it->second; + local_merged_vertex_ids[local_id] = existing; } } } @@ -318,7 +354,11 @@ namespace cutcells::mesh cut_mesh._types[sub_cell_offset+i]=cut_cell._types[i]; for(int j=0;j cells; + std::vector level_set_values; int num_cells = vtk_type.size(); for(std::size_t i=0;i level_set_values(num_vertices); + if (static_cast(level_set_values.size()) != num_vertices) + level_set_values.resize(num_vertices); + for(std::size_t j=0;j cut_cells; + // ----------------------------------------------------------------------- + // Fused single-pass implementation. + // + // All scratch buffers are thread_local so their heap allocations are + // amortised across calls (vectors keep their capacity; the hash map keeps + // its bucket array). No intermediate CutCells accumulator is created, + // and no CutCell is ever copy-assigned — the cut writes directly into a + // reusable scratch object that is immediately merged into the output. + // ----------------------------------------------------------------------- + + // --- thread-local scratch (reused across invocations) ------------------ + thread_local std::vector tl_intersected; + thread_local std::vector tl_vtx_buf; + thread_local std::vector tl_ls_buf; + thread_local cell::CutCell tl_scratch; + thread_local std::vector tl_local_merged; + thread_local std::unordered_map tl_dedup; + + // --- Pass 1: locate intersected cells ---------------------------------- + tl_intersected.clear(); + { + const int ncells = static_cast(vtk_type.size()); + for (int i = 0; i < ncells; ++i) + { + const int coff = offset[i]; + const cell::type ctype = cell::map_vtk_type_to_cell_type( + static_cast(vtk_type[i])); + const int nv = cell::get_num_vertices(ctype); + tl_ls_buf.resize(nv); + for (int j = 0; j < nv; ++j) + tl_ls_buf[j] = ls_vals[connectivity[coff + j]]; + if (cell::classify_cell_domain(std::span(tl_ls_buf.data(), nv)) + == cell::domain::intersected) + tl_intersected.push_back(i); + } + } - auto intersected_cells = locate_cells(ls_vals, points, - connectivity, offset, - vtk_type, - cell::cut_type::phieq0); + if (tl_intersected.empty()) + return CutMesh{}; + + const int n_cut = static_cast(tl_intersected.size()); + + // --- Prepare output mesh ----------------------------------------------- + // Heuristic reserves: each intersected cell produces ~8 sub-cells, + // ~4 vertices each (tet / triangle). + CutMesh cm; + cm._gdim = 3; // VTK meshes always use 3-D coordinates + cm._tdim = 0; // filled from first non-empty cut result + cm._num_cells = 0; + cm._num_vertices = 0; + cm._types.reserve(n_cut * 8); + cm._offset.reserve(n_cut * 8 + 1); + cm._connectivity.reserve(n_cut * 8 * 4); + cm._parent_map.reserve(n_cut * 8); + cm._vertex_coords.reserve(n_cut * 12); + cm._offset.push_back(0); + + // --- Clear dedup map (bucket array is retained across calls) ----------- + tl_dedup.clear(); + tl_dedup.reserve(static_cast(n_cut * 10)); + + int total_conn = 0; + int total_cells = 0; + + // --- Pass 2: cut each cell and immediately merge into cm --------------- + for (int i = 0; i < n_cut; ++i) + { + const int ci = tl_intersected[i]; + const int coff = offset[ci]; + const cell::type ctype = cell::map_vtk_type_to_cell_type( + static_cast(vtk_type[ci])); + const int nv = cell::get_num_vertices(ctype); + + // Gather vertex coords and level-set values + tl_vtx_buf.resize(nv * 3); + tl_ls_buf.resize(nv); + for (int j = 0; j < nv; ++j) + { + const int vid = connectivity[coff + j]; + tl_vtx_buf[j * 3 + 0] = points[vid * 3 + 0]; + tl_vtx_buf[j * 3 + 1] = points[vid * 3 + 1]; + tl_vtx_buf[j * 3 + 2] = points[vid * 3 + 2]; + tl_ls_buf[j] = ls_vals[vid]; + } - cut_cells._cut_cells.resize(intersected_cells.size()); - cut_cells._parent_map.resize(intersected_cells.size()); + // Cut into scratch — all CutCell fields are reset by the cutter + cell::cut(ctype, tl_vtx_buf, 3, tl_ls_buf, cut_type_str, tl_scratch, triangulate); - for(std::size_t i=0;i(vtk_type[cell_index]); - cell::type cell_type = cell::map_vtk_type_to_cell_type(cell_vtk_type); - int num_vertices = cell::get_num_vertices(cell_type); + // Override parent vertex IDs with context-global mesh indices + // (the individual cutters default them to local 0..nv-1) + tl_scratch._parent_cell_type = ctype; + tl_scratch._parent_vertex_ids.resize(nv); + for (int j = 0; j < nv; ++j) + tl_scratch._parent_vertex_ids[j] = connectivity[coff + j]; - std::vector vertex_coords(num_vertices*3); - std::vector level_set_values(num_vertices); + const int n_local_verts = static_cast(tl_scratch._vertex_coords.size()) + / tl_scratch._gdim; + const int n_local_cells = cell::num_cells(tl_scratch); + if (n_local_cells == 0) + continue; - for(std::size_t j=0;j cut_cell; + const bool has_tokens = (static_cast(tl_scratch._vertex_parent_entity.size()) + == n_local_verts); - //cut the cell - cell::cut(cell_type, vertex_coords,3,level_set_values,cut_type_str,cut_cell,triangulate); + if (has_tokens) + { + // Fast token-based dedup: shared vertices between neighbouring cut + // cells (parent vertices and edge intersections) are identified via + // their VertexKey and mapped to a single global vertex. + const auto parent_edges = cell::edges(ctype); + + for (int lv = 0; lv < n_local_verts; ++lv) + { + const int32_t token = tl_scratch._vertex_parent_entity[lv]; + VertexKey key; + + if (token >= 100 && token < 200) + { + const int ref = token - 100; + if (ref >= 0 && ref < nv) + { + key.kind = 0; + key.a = static_cast(tl_scratch._parent_vertex_ids[ref]); + key.b = 0; + } + else { key.kind = 2; key.a = ci; key.b = token; } + } + else if (token >= 200) + { + key.kind = 2; + key.a = ci; + key.b = static_cast(token - 200); + } + else + { + const int eid = static_cast(token); + if (eid >= 0 && eid < static_cast(parent_edges.size())) + { + const int32_t gv0 = static_cast( + tl_scratch._parent_vertex_ids[parent_edges[eid][0]]); + const int32_t gv1 = static_cast( + tl_scratch._parent_vertex_ids[parent_edges[eid][1]]); + key.kind = 1; + key.a = std::min(gv0, gv1); + key.b = std::max(gv0, gv1); + } + else { key.kind = 2; key.a = ci; key.b = token; } + } - // Populate context-global parent vertex IDs to enable fast merging in create_cut_mesh. - // The cutters themselves default these IDs to 0..V-1. - cut_cell._parent_cell_type = cell_type; - cut_cell._parent_vertex_ids.resize(num_vertices); - for (int j = 0; j < num_vertices; ++j) + const int next_id = static_cast(cm._vertex_coords.size()) / cm._gdim; + auto [it, inserted] = tl_dedup.emplace(key, next_id); + tl_local_merged[lv] = it->second; + if (inserted) + { + const int base = lv * tl_scratch._gdim; + cm._vertex_coords.insert(cm._vertex_coords.end(), + tl_scratch._vertex_coords.data() + base, + tl_scratch._vertex_coords.data() + base + tl_scratch._gdim); + } + } + } + else { - const int vertex_id = connectivity[cell_offset + j]; - cut_cell._parent_vertex_ids[j] = vertex_id; + // Fallback: tokens not available — emit all local vertices without + // inter-cell dedup (unusual path; occurs only without token support) + for (int lv = 0; lv < n_local_verts; ++lv) + { + tl_local_merged[lv] = static_cast(cm._vertex_coords.size()) / cm._gdim; + const int base = lv * tl_scratch._gdim; + cm._vertex_coords.insert(cm._vertex_coords.end(), + tl_scratch._vertex_coords.data() + base, + tl_scratch._vertex_coords.data() + base + tl_scratch._gdim); + } } - cut_cells._cut_cells[i] = cut_cell; - cut_cells._parent_map[i] = cell_index; - - if (std::find(cut_cells._types.begin(), cut_cells._types.end(), cell_type) == cut_cells._types.end()) - cut_cells._types.push_back(cell_type); + // Emit sub-cells + for (int lc = 0; lc < n_local_cells; ++lc) + { + const auto verts = cell::cell_vertices(tl_scratch, lc); + const int vcnt = static_cast(verts.size()); + for (int v = 0; v < vcnt; ++v) + cm._connectivity.push_back(tl_local_merged[verts[v]]); + total_conn += vcnt; + cm._offset.push_back(total_conn); + cm._types.push_back(tl_scratch._types[lc]); + cm._parent_map.push_back(ci); + ++total_cells; + } } - return create_cut_mesh(cut_cells); + cm._num_cells = total_cells; + cm._num_vertices = static_cast(cm._vertex_coords.size()) / cm._gdim; + + return cm; } //----------------------------------------------------------------------------- diff --git a/cpp/src/cut_prism.cpp b/cpp/src/cut_prism.cpp index 8e1537a..b8c1d8e 100644 --- a/cpp/src/cut_prism.cpp +++ b/cpp/src/cut_prism.cpp @@ -63,9 +63,9 @@ namespace cutcells::cell::prism intersection_points.reserve(9 * gdim); vertex_case_map.fill(-1); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector ip(gdim); + // gdim is 3 for prisms; use stack arrays to avoid heap allocation per edge + std::array v0 = {}; + std::array v1 = {}; int ip_index = 0; for (int e = 0; e < 9; ++e) @@ -85,10 +85,11 @@ namespace cutcells::cell::prism const T ls0 = ls_values[v0_id]; const T ls1 = ls_values[v1_id]; - interval::compute_intersection_point(0.0, v0, v1, ls0, ls1, ip); - - for (int j = 0; j < gdim; ++j) - intersection_points.push_back(ip[j]); + const int ip_offset = static_cast(intersection_points.size()); + intersection_points.resize(intersection_points.size() + gdim); + interval::compute_intersection_point(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip_offset); vertex_case_map[e] = ip_index; ++ip_index; @@ -140,7 +141,8 @@ namespace cutcells::cell::prism if (nrefs <= 0) throw std::runtime_error("Malformed special point definition"); - std::vector coord(gdim, T(0)); + // gdim is at most 3 for 3-D cells; use stack storage + std::array coord = {T(0), T(0), T(0)}; for (int r = 0; r < nrefs; ++r) { const int ref = special_point_data[idx++]; @@ -201,8 +203,8 @@ namespace cutcells::cell::prism const type sub_type = cell_types[cell_idx]; const int* verts = subcell_verts[cell_idx]; - std::vector verts_local; - verts_local.reserve(MaxVerts); + std::array verts_local; + int nverts = 0; for (int i = 0; i < MaxVerts; ++i) { const int token = verts[i]; @@ -224,20 +226,19 @@ namespace cutcells::cell::prism cut_cell, vertex_case_map); } - verts_local.push_back(lookup_token_or_throw(vertex_case_map, flag, token, "decode_case")); + verts_local[nverts++] = lookup_token_or_throw(vertex_case_map, flag, token, "decode_case"); } - if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) + if (triangulate && sub_type == type::quadrilateral && nverts == 4) { const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, t0, 3); + cutcells::cell::append_cell(cut_cell, type::triangle, t1, 3); } else { - cutcells::cell::append_cell(cut_cell, sub_type, - std::span(verts_local.data(), verts_local.size())); + cutcells::cell::append_cell(cut_cell, sub_type, verts_local.data(), nverts); } } } @@ -270,19 +271,19 @@ namespace cutcells::cell::prism } // Compute intersections (shared for all parts) - std::vector intersection_points; + thread_local std::vector intersection_points; VertexCaseMap vertex_case_map; - vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, intersection_points, vertex_case_map); cut_cell._gdim = gdim; - cut_cell._vertex_coords = std::move(intersection_points); + cut_cell._vertex_coords.assign(intersection_points.begin(), intersection_points.end()); cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); + const cut_type cut_kind = string_to_cut_type(cut_type_str); - if (cut_type_str == "phi=0") + if (cut_kind == cut_type::phieq0) { cut_cell._tdim = 2; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -290,7 +291,7 @@ namespace cutcells::cell::prism special_point_count_interface, special_point_offset_interface, special_point_data_interface, triangulate, cut_cell, vertex_case_map); } - else if (cut_type_str == "phi<0") + else if (cut_kind == cut_type::philt0) { cut_cell._tdim = 3; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -298,7 +299,7 @@ namespace cutcells::cell::prism special_point_count_inside, special_point_offset_inside, special_point_data_inside, triangulate, cut_cell, vertex_case_map); } - else if (cut_type_str == "phi>0") + else if (cut_kind == cut_type::phigt0) { cut_cell._tdim = 3; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -311,7 +312,7 @@ namespace cutcells::cell::prism throw std::invalid_argument("cutting type unknown"); } - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 9, 6, 44); } template diff --git a/cpp/src/cut_pyramid.cpp b/cpp/src/cut_pyramid.cpp index 9453b2c..a9370f9 100644 --- a/cpp/src/cut_pyramid.cpp +++ b/cpp/src/cut_pyramid.cpp @@ -62,9 +62,9 @@ namespace cutcells::cell::pyramid intersection_points.reserve(8 * gdim); vertex_case_map.fill(-1); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector ip(gdim); + // gdim is 3 for pyramids; use stack arrays to avoid heap allocation per edge + std::array v0 = {}; + std::array v1 = {}; int ip_index = 0; for (int e = 0; e < 8; ++e) @@ -84,10 +84,11 @@ namespace cutcells::cell::pyramid const T ls0 = ls_values[v0_id]; const T ls1 = ls_values[v1_id]; - interval::compute_intersection_point(0.0, v0, v1, ls0, ls1, ip); - - for (int j = 0; j < gdim; ++j) - intersection_points.push_back(ip[j]); + const int ip_offset = static_cast(intersection_points.size()); + intersection_points.resize(intersection_points.size() + gdim); + interval::compute_intersection_point(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip_offset); vertex_case_map[e] = ip_index; ++ip_index; @@ -142,7 +143,8 @@ namespace cutcells::cell::pyramid if (nrefs <= 0) throw std::runtime_error("Malformed special point definition"); - std::vector coord(gdim, T(0)); + // gdim is at most 3 for 3-D cells; use stack storage + std::array coord = {T(0), T(0), T(0)}; for (int r = 0; r < nrefs; ++r) { const int ref = special_point_data[idx++]; @@ -203,8 +205,8 @@ namespace cutcells::cell::pyramid const type sub_type = cell_types[cell_idx]; const int* verts = subcell_verts[cell_idx]; - std::vector verts_local; - verts_local.reserve(MaxVerts); + std::array verts_local; + int nverts = 0; for (int i = 0; i < MaxVerts; ++i) { const int token = verts[i]; @@ -226,20 +228,19 @@ namespace cutcells::cell::pyramid cut_cell, vertex_case_map); } - verts_local.push_back(lookup_token_or_throw(vertex_case_map, flag, token, "decode_case")); + verts_local[nverts++] = lookup_token_or_throw(vertex_case_map, flag, token, "decode_case"); } - if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) + if (triangulate && sub_type == type::quadrilateral && nverts == 4) { const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, t0, 3); + cutcells::cell::append_cell(cut_cell, type::triangle, t1, 3); } else { - cutcells::cell::append_cell(cut_cell, sub_type, - std::span(verts_local.data(), verts_local.size())); + cutcells::cell::append_cell(cut_cell, sub_type, verts_local.data(), nverts); } } } @@ -272,19 +273,19 @@ namespace cutcells::cell::pyramid } // Compute intersections (shared for all parts) - std::vector intersection_points; + thread_local std::vector intersection_points; VertexCaseMap vertex_case_map; - vertex_case_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_lt0, intersection_points, vertex_case_map); cut_cell._gdim = gdim; - cut_cell._vertex_coords = std::move(intersection_points); + cut_cell._vertex_coords.assign(intersection_points.begin(), intersection_points.end()); cutcells::cell::clear_cell_topology(cut_cell); cut_cell._vertex_coords.reserve(reserve_vertex_coords * gdim); cutcells::cell::reserve_cell_topology(cut_cell, reserve_connectivity, reserve_types); + const cut_type cut_kind = string_to_cut_type(cut_type_str); - if (cut_type_str == "phi=0") + if (cut_kind == cut_type::phieq0) { cut_cell._tdim = 2; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -292,7 +293,7 @@ namespace cutcells::cell::pyramid nullptr, nullptr, nullptr, triangulate, cut_cell, vertex_case_map); } - else if (cut_type_str == "phi<0") + else if (cut_kind == cut_type::philt0) { cut_cell._tdim = 3; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -300,7 +301,7 @@ namespace cutcells::cell::pyramid special_point_count_inside, special_point_offset_inside, special_point_data_inside, triangulate, cut_cell, vertex_case_map); } - else if (cut_type_str == "phi>0") + else if (cut_kind == cut_type::phigt0) { cut_cell._tdim = 3; decode_case(vertex_coordinates, gdim, flag_lt0, @@ -313,7 +314,7 @@ namespace cutcells::cell::pyramid throw std::invalid_argument("cutting type unknown"); } - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 8, 5, 1); } template diff --git a/cpp/src/cut_quadrilateral.cpp b/cpp/src/cut_quadrilateral.cpp index 7df4b06..943b49c 100644 --- a/cpp/src/cut_quadrilateral.cpp +++ b/cpp/src/cut_quadrilateral.cpp @@ -56,9 +56,9 @@ namespace cutcells::cell::quadrilateral intersection_points.reserve(4 * gdim); vertex_case_map.fill(-1); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector ip(gdim); + // gdim is at most 3; use stack arrays to avoid heap allocations + std::array v0 = {}; + std::array v1 = {}; int ip_index = 0; for (int e = 0; e < 4; ++e) @@ -78,12 +78,11 @@ namespace cutcells::cell::quadrilateral const T ls0 = ls_values[v0_id]; const T ls1 = ls_values[v1_id]; - interval::compute_intersection_point(0.0, v0, v1, ls0, ls1, ip); - - for (int j = 0; j < gdim; ++j) - { - intersection_points.push_back(ip[j]); - } + const int ip_offset = static_cast(intersection_points.size()); + intersection_points.resize(intersection_points.size() + gdim); + interval::compute_intersection_point(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip_offset); vertex_case_map[e] = ip_index; ++ip_index; @@ -152,27 +151,27 @@ namespace cutcells::cell::quadrilateral const type sub_type = cell_types[cell_idx]; const int* verts = subcell_verts[cell_idx]; - std::vector verts_local; + std::array verts_local; + int nverts = 0; for (int i = 0; i < MaxVerts; ++i) { const int token = verts[i]; if (token == -1) break; - verts_local.push_back(get_local(token)); + verts_local[nverts++] = get_local(token); } - if (triangulate && sub_type == type::quadrilateral && verts_local.size() == 4) + if (triangulate && sub_type == type::quadrilateral && nverts == 4) { // split into two triangles (0,1,2) and (0,2,3) const std::array t0 = {verts_local[0], verts_local[1], verts_local[2]}; const std::array t1 = {verts_local[0], verts_local[2], verts_local[3]}; - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t0.data(), t0.size())); - cutcells::cell::append_cell(cut_cell, type::triangle, std::span(t1.data(), t1.size())); + cutcells::cell::append_cell(cut_cell, type::triangle, t0, 3); + cutcells::cell::append_cell(cut_cell, type::triangle, t1, 3); } else { - cutcells::cell::append_cell(cut_cell, sub_type, - std::span(verts_local.data(), verts_local.size())); + cutcells::cell::append_cell(cut_cell, sub_type, verts_local.data(), nverts); } } @@ -221,9 +220,8 @@ namespace cutcells::cell::quadrilateral } // Compute intersections (shared for all parts) - std::vector intersection_points; + thread_local std::vector intersection_points; VertexCaseMap edge_ip_map; - edge_ip_map.fill(-1); compute_intersection_points(vertex_coordinates, gdim, ls_values, flag_interior, intersection_points, edge_ip_map); // Final token->local vertex index map used to populate CutCell::_vertex_parent_entity @@ -238,8 +236,9 @@ namespace cutcells::cell::quadrilateral const bool is_amb = case_is_ambiguous(flag_interior); const int variant = asymptotic_decider(ls_values[0], ls_values[1], ls_values[2], ls_values[3]) ? 0 : 1; + const cut_type cut_kind = string_to_cut_type(cut_type_str); - if (cut_type_str == "phi=0") + if (cut_kind == cut_type::phieq0) { cut_cell._tdim = 1; std::span ip_span(intersection_points.data(), intersection_points.size()); @@ -259,7 +258,7 @@ namespace cutcells::cell::quadrilateral subcell_verts_interface, ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } } - else if (cut_type_str == "phi<0") + else if (cut_kind == cut_type::philt0) { cut_cell._tdim = 2; std::span ip_span(intersection_points.data(), intersection_points.size()); @@ -279,7 +278,7 @@ namespace cutcells::cell::quadrilateral subcell_verts_inside, ip_span, triangulate, cut_cell, edge_ip_map, vertex_case_map); } } - else if (cut_type_str == "phi>0") + else if (cut_kind == cut_type::phigt0) { cut_cell._tdim = 2; std::span ip_span(intersection_points.data(), intersection_points.size()); @@ -306,7 +305,7 @@ namespace cutcells::cell::quadrilateral throw std::invalid_argument("cutting type unknown"); } - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 4, 4); } template diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index 457352c..1d1036d 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -187,9 +187,9 @@ namespace tetrahedron{ intersection_points.resize(num_intersection_points*gdim); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector intersection_point(gdim); + // gdim is at most 3; use stack arrays to avoid heap allocation per intersection point + std::array v0 = {}; + std::array v1 = {}; for(int ip=0; ip(0.0, v0, v1,ls0, ls1, intersection_point); - - for(int j=0;j(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip * gdim); } int cnt = 0; @@ -269,121 +266,6 @@ namespace tetrahedron{ } } - void create_interface_cells(const int& flag, VertexCaseMap& vertex_case_map, const bool &triangulate, - std::vector>& interface_cells, std::vector& interface_cell_types) - { - int num_interface_cells = get_num_interface_elements(flag, triangulate); - interface_cells.resize(num_interface_cells); - interface_cell_types.resize(num_interface_cells); - type sub_cell_type = interface_sub_element_cell_types[flag]; - - // initialize cell types - for(std::size_t i =0; i> triangles; - triangulation(sub_cell_type, tetrahedron_intersected_edges[flag], triangles); - - for(int i=0;i>& sub_cells, - std::vector& sub_cell_types) - { - // Allocate memory - int num_sub_elements = get_num_sub_elements(flag, triangulate); - type sub_cell_type = tetrahedron_sub_element_cell_types[flag]; - sub_cells.resize(num_sub_elements); - sub_cell_types.resize(num_sub_elements); - - for(int i=0;i> triangles; - triangulation(sub_cell_type, tetrahedron_sub_element[flag], triangles); - - for(int i=0;i void create_cut_cell(const std::span vertex_coordinates, const int gdim, const std::span ls_values, @@ -394,8 +276,9 @@ namespace tetrahedron{ { cut_cell._gdim = gdim; cutcells::cell::clear_cell_topology(cut_cell); + const cut_type cut_kind = string_to_cut_type(cut_type_str); - if(cut_type_str=="phi=0") + if(cut_kind == cut_type::phieq0) { cut_cell._tdim = 2; int flag_interior = get_entity_flag(ls_values, false); @@ -406,46 +289,90 @@ namespace tetrahedron{ { cut_cell._vertex_coords[i] = intersection_points[i]; } - // Determine interface cells for triangle this is an interval with two points (the intersection points) - std::vector> interface_cells; - std::vector interface_cell_types; - create_interface_cells(flag_interior, vertex_case_map, triangulate, interface_cells, interface_cell_types); - for (std::size_t i = 0; i < interface_cells.size(); ++i) + // Append interface cells directly + type iface_type = interface_sub_element_cell_types[flag_interior]; + if(iface_type == type::quadrilateral && triangulate) + { + std::vector> triangles; + triangulation(iface_type, tetrahedron_intersected_edges[flag_interior], triangles); + for(int i = 0; i < static_cast(triangles.size()); ++i) + { + std::array t; + t[0] = vertex_case_map[triangles[i][0]]; + t[1] = vertex_case_map[triangles[i][1]]; + t[2] = vertex_case_map[triangles[i][2]]; + cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); + } + } + else { - cutcells::cell::append_cell(cut_cell, interface_cell_types[i], - std::span(interface_cells[i].data(), interface_cells[i].size())); + int num_vertices = get_num_vertices(iface_type); + std::array verts; + for(int j = 0; j < num_vertices; ++j) + verts[j] = vertex_case_map[tetrahedron_intersected_edges[flag_interior][j]]; + cutcells::cell::append_cell(cut_cell, iface_type, verts, num_vertices); } } - else if(cut_type_str=="phi<0") + else if(cut_kind == cut_type::philt0) { cut_cell._tdim = 3; int flag_interior = get_entity_flag(ls_values, false); create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - //Determine interior sub-cells - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_interior, triangulate, vertex_case_map, sub_cells, sub_cell_types); - for (std::size_t i = 0; i < sub_cells.size(); ++i) + // Append sub-cells directly + type sub_cell_type = tetrahedron_sub_element_cell_types[flag_interior]; + if(sub_cell_type == type::prism && triangulate) { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); + std::vector> tets; + triangulation(sub_cell_type, tetrahedron_sub_element[flag_interior], tets); + for(int i = 0; i < static_cast(tets.size()); ++i) + { + std::array t; + t[0] = vertex_case_map[tets[i][0]]; + t[1] = vertex_case_map[tets[i][1]]; + t[2] = vertex_case_map[tets[i][2]]; + t[3] = vertex_case_map[tets[i][3]]; + cutcells::cell::append_cell(cut_cell, type::tetrahedron, t, 4); + } + } + else + { + int num_vertices = get_num_vertices(sub_cell_type); + std::array verts; + for(int j = 0; j < num_vertices; ++j) + verts[j] = vertex_case_map[tetrahedron_sub_element[flag_interior][j]]; + cutcells::cell::append_cell(cut_cell, sub_cell_type, verts, num_vertices); } } - else if(cut_type_str=="phi>0") + else if(cut_kind == cut_type::phigt0) { cut_cell._tdim = 3; - //Determine exterior sub-cells int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_exterior, triangulate, vertex_case_map, sub_cells, sub_cell_types); - for (std::size_t i = 0; i < sub_cells.size(); ++i) + // Append sub-cells directly + type sub_cell_type = tetrahedron_sub_element_cell_types[flag_exterior]; + if(sub_cell_type == type::prism && triangulate) { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); + std::vector> tets; + triangulation(sub_cell_type, tetrahedron_sub_element[flag_exterior], tets); + for(int i = 0; i < static_cast(tets.size()); ++i) + { + std::array t; + t[0] = vertex_case_map[tets[i][0]]; + t[1] = vertex_case_map[tets[i][1]]; + t[2] = vertex_case_map[tets[i][2]]; + t[3] = vertex_case_map[tets[i][3]]; + cutcells::cell::append_cell(cut_cell, type::tetrahedron, t, 4); + } + } + else + { + int num_vertices = get_num_vertices(sub_cell_type); + std::array verts; + for(int j = 0; j < num_vertices; ++j) + verts[j] = vertex_case_map[tetrahedron_sub_element[flag_exterior][j]]; + cutcells::cell::append_cell(cut_cell, sub_cell_type, verts, num_vertices); } } else @@ -503,7 +430,8 @@ namespace tetrahedron{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points - std::vector intersection_points; + thread_local std::vector intersection_points; + intersection_points.clear(); intersection_points.reserve(4 * gdim); // the vertex case map, // first few entries map from intersected edge to intersection point number @@ -520,7 +448,7 @@ namespace tetrahedron{ create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 6, 4); } template diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index f73a9f4..49f8ba4 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -109,9 +109,9 @@ namespace triangle{ intersection_points.resize(num_intersection_points*gdim); - std::vector v0(gdim); - std::vector v1(gdim); - std::vector intersection_point(gdim); + // gdim is at most 3; use stack arrays to avoid heap allocation per intersection point + std::array v0 = {}; + std::array v1 = {}; for(int ip=0; ip(0.0, v0, v1,ls0, ls1, intersection_point); - - for(int j=0;j(T(0), std::span(v0.data(), gdim), + std::span(v1.data(), gdim), + ls0, ls1, intersection_points, ip * gdim); } vertex_case_map[triangle_intersected_edges[flag][0]] = 0; @@ -186,81 +183,6 @@ namespace triangle{ } } - void create_interface_cells(std::vector>& interface_cells, std::vector& interface_cell_types) - { - interface_cells.resize(1); - interface_cells[0].resize(2); - interface_cells[0][0] = 0; - interface_cells[0][1] = 1; - - interface_cell_types.resize(1); - interface_cell_types[0] = type::interval; - } - - void create_sub_cells(const int& flag, bool triangulate, std::vector>& sub_cells, - std::vector& sub_cell_types, VertexCaseMap& vertex_case_map) - { - // Allocate memory - int num_sub_elements = get_num_sub_elements(flag, triangulate); - sub_cells.resize(num_sub_elements); - sub_cell_types.resize(num_sub_elements); - - for(int i=0;i> triangles; - triangulation(sub_cell_type, triangle_sub_element[flag], triangles); - - for(int i=0;i void create_cut_cell(const std::span vertex_coordinates, const int gdim, const std::span ls_values, @@ -271,8 +193,9 @@ namespace triangle{ { cut_cell._gdim = gdim; cutcells::cell::clear_cell_topology(cut_cell); + const cut_type cut_kind = string_to_cut_type(cut_type_str); - if(cut_type_str=="phi=0") + if(cut_kind == cut_type::phieq0) { cut_cell._tdim = 1; int flag_interior = get_entity_flag(ls_values, false); @@ -283,46 +206,69 @@ namespace triangle{ { cut_cell._vertex_coords[i] = intersection_points[i]; } - // Determine interface cells for triangle this is an interval with two points (the intersection points) - std::vector> interface_cells; - std::vector interface_cell_types; - create_interface_cells(interface_cells, interface_cell_types); - for (std::size_t i = 0; i < interface_cells.size(); ++i) - { - cutcells::cell::append_cell(cut_cell, interface_cell_types[i], - std::span(interface_cells[i].data(), interface_cells[i].size())); - } + // Interface is a single interval spanning the two intersection points + const std::array iface_verts = {0, 1}; + cutcells::cell::append_cell(cut_cell, type::interval, iface_verts, 2); } - else if(cut_type_str=="phi<0") + else if(cut_kind == cut_type::philt0) { cut_cell._tdim = 2; int flag_interior = get_entity_flag(ls_values, false); create_sub_cell_vertex_coords(flag_interior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - //Determine interior sub-cells - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_interior, triangulate, sub_cells, sub_cell_types, vertex_case_map); - for (std::size_t i = 0; i < sub_cells.size(); ++i) + // Append sub-cells directly into flat storage without staging + type sub_cell_type = triangle_sub_element_cell_types[flag_interior]; + if(sub_cell_type == type::quadrilateral && triangulate) + { + // Triangulate the quad: produce two triangles via triangulation helper + std::vector> triangles; + triangulation(sub_cell_type, triangle_sub_element[flag_interior], triangles); + for(int i = 0; i < static_cast(triangles.size()); ++i) + { + std::array t; + t[0] = vertex_case_map[triangles[i][0]]; + t[1] = vertex_case_map[triangles[i][1]]; + t[2] = vertex_case_map[triangles[i][2]]; + cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); + } + } + else { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); + int num_vertices = get_num_vertices(sub_cell_type); + std::array verts; + for(int j = 0; j < num_vertices; ++j) + verts[j] = vertex_case_map[triangle_sub_element[flag_interior][j]]; + cutcells::cell::append_cell(cut_cell, sub_cell_type, verts, num_vertices); } } - else if(cut_type_str=="phi>0") + else if(cut_kind == cut_type::phigt0) { cut_cell._tdim = 2; - //Determine exterior sub-cells int flag_exterior = get_entity_flag(ls_values, true); create_sub_cell_vertex_coords(flag_exterior, vertex_coordinates, gdim, intersection_points, cut_cell._vertex_coords, vertex_case_map); - std::vector> sub_cells; - std::vector sub_cell_types; - create_sub_cells(flag_exterior, triangulate, sub_cells, sub_cell_types, vertex_case_map); - for (std::size_t i = 0; i < sub_cells.size(); ++i) + // Append sub-cells directly into flat storage without staging + type sub_cell_type = triangle_sub_element_cell_types[flag_exterior]; + if(sub_cell_type == type::quadrilateral && triangulate) + { + std::vector> triangles; + triangulation(sub_cell_type, triangle_sub_element[flag_exterior], triangles); + for(int i = 0; i < static_cast(triangles.size()); ++i) + { + std::array t; + t[0] = vertex_case_map[triangles[i][0]]; + t[1] = vertex_case_map[triangles[i][1]]; + t[2] = vertex_case_map[triangles[i][2]]; + cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); + } + } + else { - cutcells::cell::append_cell(cut_cell, sub_cell_types[i], - std::span(sub_cells[i].data(), sub_cells[i].size())); + int num_vertices = get_num_vertices(sub_cell_type); + std::array verts; + for(int j = 0; j < num_vertices; ++j) + verts[j] = vertex_case_map[triangle_sub_element[flag_exterior][j]]; + cutcells::cell::append_cell(cut_cell, sub_cell_type, verts, num_vertices); } } else @@ -379,7 +325,8 @@ namespace triangle{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points - std::vector intersection_points; + thread_local std::vector intersection_points; + intersection_points.clear(); intersection_points.reserve(2 * gdim); // the vertex case map, // first few entries map from intersected edge to intersection point number @@ -396,7 +343,7 @@ namespace triangle{ create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity); + cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 3, 3); }; // cut triangle version with vector of string in case multiple parts of the cut-cell are needed (very common) @@ -415,7 +362,8 @@ namespace triangle{ // Compute intersection points these are required for any cut cell part (interface, interior, exterior) // get the number of intersection points - std::vector intersection_points; + thread_local std::vector intersection_points; + intersection_points.clear(); intersection_points.reserve(2 * gdim); VertexCaseMap vertex_case_map; vertex_case_map.fill(-1); diff --git a/cpp/src/utils.h b/cpp/src/utils.h index 76f7ed8..f565c16 100644 --- a/cpp/src/utils.h +++ b/cpp/src/utils.h @@ -88,4 +88,37 @@ namespace cutcells::utils vertex_parent_entity[static_cast(local)] = static_cast(token); } } + + // Targeted variant: only scans the token ranges actually used by the cell type. + // Much faster than scanning all 256 entries: e.g. triangle only has 6 valid tokens. + // n_edges: number of edge tokens in [0, n_edges) + // n_vertices: number of vertex tokens in [100, 100+n_vertices) + // n_special: number of special tokens in [200, 200+n_special) + template + void create_vertex_parent_entity_map(const std::array& token_to_vertex, + std::vector& vertex_parent_entity, + int n_edges, int n_vertices, int n_special = 0) + { + int max_local = -1; + for (int t = 0; t < n_edges; ++t) + if (token_to_vertex[t] >= 0) + max_local = std::max(max_local, token_to_vertex[t]); + for (int t = 100, e = 100 + n_vertices; t < e; ++t) + if (token_to_vertex[t] >= 0) + max_local = std::max(max_local, token_to_vertex[t]); + for (int t = 200, e = 200 + n_special; t < e; ++t) + if (token_to_vertex[t] >= 0) + max_local = std::max(max_local, token_to_vertex[t]); + + vertex_parent_entity.assign(static_cast(max_local + 1), -1); + for (int t = 0; t < n_edges; ++t) + if (token_to_vertex[t] >= 0) + vertex_parent_entity[static_cast(token_to_vertex[t])] = static_cast(t); + for (int t = 100, e = 100 + n_vertices; t < e; ++t) + if (token_to_vertex[t] >= 0) + vertex_parent_entity[static_cast(token_to_vertex[t])] = static_cast(t); + for (int t = 200, e = 200 + n_special; t < e; ++t) + if (token_to_vertex[t] >= 0) + vertex_parent_entity[static_cast(token_to_vertex[t])] = static_cast(t); + } } \ No newline at end of file diff --git a/cpp/src/write_tikz.h b/cpp/src/write_tikz.h index 968e346..d70b9db 100644 --- a/cpp/src/write_tikz.h +++ b/cpp/src/write_tikz.h @@ -9,6 +9,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include + +#include "cut_cell.h" namespace cutcells::io { @@ -145,4 +153,61 @@ namespace cutcells::io } else std:: cout << "Unable to open file " << filename << " to write" << std::endl; }; + + inline void write_tikz(std::string filename, + const std::span element_vertex_coords, + const std::span connectivity, + const std::span offsets, + const std::span bg_vertex_coords, + const std::vector> bg_elements, + const std::span ls_values, + const int gdim) + { + if (offsets.empty()) + { + write_tikz(filename, element_vertex_coords, std::vector>{}, + bg_vertex_coords, bg_elements, ls_values, gdim); + return; + } + + const bool has_terminal_offset = offsets.back() == static_cast(connectivity.size()); + const int num_cells = has_terminal_offset + ? static_cast(offsets.size()) - 1 + : static_cast(offsets.size()); + + std::vector> elements; + elements.reserve(std::max(num_cells, 0)); + + for (int i = 0; i < num_cells; ++i) + { + const int begin = offsets[i]; + const int end = (i + 1 < static_cast(offsets.size())) + ? offsets[i + 1] + : static_cast(connectivity.size()); + + if (begin < 0 || end < begin || end > static_cast(connectivity.size())) + throw std::runtime_error("Invalid CSR layout in write_tikz"); + + elements.emplace_back(connectivity.begin() + begin, connectivity.begin() + end); + } + + write_tikz(filename, element_vertex_coords, elements, bg_vertex_coords, bg_elements, ls_values, gdim); + } + + inline void write_tikz(std::string filename, + const cell::CutCell& cut_cell, + const std::span bg_vertex_coords, + const std::vector> bg_elements, + const std::span ls_values, + const int gdim) + { + write_tikz(filename, + std::span(cut_cell._vertex_coords.data(), cut_cell._vertex_coords.size()), + std::span(cut_cell._connectivity.data(), cut_cell._connectivity.size()), + std::span(cut_cell._offset.data(), cut_cell._offset.size()), + bg_vertex_coords, + bg_elements, + ls_values, + gdim); + } } \ No newline at end of file diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index c194d73..4b88d15 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -7,6 +7,8 @@ endif() project(cutcells_nanobind VERSION "0.2.0" LANGUAGES CXX) +find_package(OpenMP) + # Set C++ standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -16,12 +18,31 @@ if (TARGET cutcells) add_library(CUTCELLS::cutcells ALIAS cutcells) else() # Find CutCells (C++) - # Prefer the in-repo build if present, so `pip install -e python` uses local C++ changes. + # Prefer in-repo CutCells configs over any globally/conda-installed libcutcells. + # This avoids ABI mismatches between wrapper headers (from workspace) and an older + # runtime library discovered on the default prefix path. if (NOT DEFINED CutCells_DIR) - set(_local_cutcells_prefix "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir/prefix") - if (EXISTS "${_local_cutcells_prefix}/lib/cmake/cutcells/CutCellsConfig.cmake") - list(PREPEND CMAKE_PREFIX_PATH "${_local_cutcells_prefix}") - endif() + set(_local_cutcells_candidates + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build/prefix/lib/cmake/cutcells" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir/prefix/lib/cmake/cutcells" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir") + + foreach(_candidate IN LISTS _local_cutcells_candidates) + if (EXISTS "${_candidate}") + if (IS_DIRECTORY "${_candidate}") + set(_candidate_dir "${_candidate}") + else() + get_filename_component(_candidate_dir "${_candidate}" DIRECTORY) + endif() + + if (EXISTS "${_candidate_dir}/CutCellsConfig.cmake" AND EXISTS "${_candidate_dir}/CutCellsTargets.cmake") + set(CutCells_DIR "${_candidate_dir}") + break() + endif() + endif() + endforeach() endif() find_package(CutCells REQUIRED) endif() diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 1902fdb..4966e6d 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -3,7 +3,19 @@ # This file is part of CutCells # # SPDX-License-Identifier: MIT -from ._cutcellscpp import (classify_cell_domain, CellType, CutCell_float32, CutCell_float64, CutCells_float32, - CutCells_float64, CutMesh_float32, CutMesh_float64, cut, higher_order_cut, create_cut_mesh, - locate_cells, cut_vtk_mesh) - +from ._cutcellscpp import ( + classify_cell_domain, + CellType, + CutCell_float32, + CutCell_float64, + CutCells_float32, + CutCells_float64, + CutMesh_float32, + CutMesh_float64, + cut, + higher_order_cut, + create_cut_mesh, + locate_cells, + cut_vtk_mesh, + csr_to_vtk_cells, +) diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index a33e23f..2d681e7 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "../../cpp/src/cell_types.h" #include "../../cpp/src/cut_cell.h" @@ -67,6 +68,40 @@ auto as_nbarrayp(std::pair>&& x) return as_nbarray(std::move(x.first), x.second.size(), x.second.data()); } +inline std::vector csr_to_vtk_cells_impl(std::span connectivity, + std::span offsets) +{ + if (offsets.empty()) + return {}; + + const int conn_size = static_cast(connectivity.size()); + const bool has_terminal_offset = (offsets.back() == conn_size); + const int num_cells = has_terminal_offset ? static_cast(offsets.size()) - 1 + : static_cast(offsets.size()); + + if (num_cells < 0) + throw std::runtime_error("Invalid CSR offsets: negative cell count"); + + std::vector cells; + cells.reserve(connectivity.size() + static_cast(num_cells)); + + for (int i = 0; i < num_cells; ++i) + { + const int begin = offsets[i]; + const int end = (i + 1 < static_cast(offsets.size())) ? offsets[i + 1] : conn_size; + + if (begin < 0 || end < begin || end > conn_size) + throw std::runtime_error("Invalid CSR offsets/connectivity bounds"); + + const int nverts = end - begin; + cells.push_back(static_cast(nverts)); + for (int j = begin; j < end; ++j) + cells.push_back(static_cast(connectivity[j])); + } + + return cells; +} + template void declare_float(nb::module_& m, std::string type) { @@ -139,6 +174,15 @@ void declare_float(nb::module_& m, std::string type) }, nb::rv_policy::reference_internal, "Zero-copy view of CSR offsets array.") + .def_prop_ro( + "cells", + [](const cell::CutCell& self) { + return as_nbarray(csr_to_vtk_cells_impl( + std::span(self._connectivity.data(), self._connectivity.size()), + std::span(self._offset.data(), self._offset.size()))); + }, + nb::rv_policy::move, + "Packed VTK cells array [n0, v0..., n1, v1..., ...] built from connectivity+offsets.") .def_prop_ro( "types", [](const cell::CutCell& self) { @@ -151,6 +195,17 @@ void declare_float(nb::module_& m, std::string type) }, nb::rv_policy::reference_internal, "Zero-copy view of cut-cell type ids (cell::type enum underlying values).") + .def_prop_ro( + "vtk_types", + [](const cell::CutCell& self) { + std::vector vtk; + vtk.reserve(self._types.size()); + for (const auto t : self._types) + vtk.push_back(static_cast(cell::map_cell_type_to_vtk(t))); + return as_nbarray(std::move(vtk)); + }, + nb::rv_policy::move, + "VTK type IDs for each sub-cell (uint8), suitable for pv.UnstructuredGrid.") .def_prop_ro( "vertex_parent_entity", [](const cell::CutCell& self) { @@ -180,16 +235,15 @@ void declare_float(nb::module_& m, std::string type) .def_prop_ro( "types", [](const mesh::CutCells& self) { - //allocate memory - nb::list types; - - for(std::size_t i=0;i(map_cell_type_to_vtk(self._types[i]))); - } - - return types; - }) + using type_id_t = std::underlying_type_t; + static_assert(std::is_integral_v, "cell::type must have integral underlying type"); + return nb::ndarray, nb::c_contig>( + reinterpret_cast(self._types.data()), + {self._types.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of cut-cell type ids (cell::type enum underlying values).") .def_prop_ro( "parent_map", [](const mesh::CutCells& self) { @@ -233,32 +287,36 @@ void declare_float(nb::module_& m, std::string type) .def_prop_ro( "types", [](const mesh::CutMesh& self) { - //allocate memory - nb::list types; - for(std::size_t i=0;i(map_cell_type_to_vtk(self._types[i]))); - } - return types; - }) + using type_id_t = std::underlying_type_t; + static_assert(std::is_integral_v, "cell::type must have integral underlying type"); + return nb::ndarray, nb::c_contig>( + reinterpret_cast(self._types.data()), + {self._types.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of cut-mesh type ids (cell::type enum underlying values).") + .def_prop_ro( + "vtk_types", + [](const mesh::CutMesh& self) { + std::vector vtk; + vtk.reserve(self._types.size()); + for (const auto t : self._types) + vtk.push_back(static_cast(cell::map_cell_type_to_vtk(t))); + return as_nbarray(std::move(vtk)); + }, + nb::rv_policy::move, + "VTK type IDs for each sub-cell (uint8), suitable for pv.UnstructuredGrid.") .def_prop_ro( "cells", [](const mesh::CutMesh& self) { - std::size_t num_cells = self._offset.size()-1; - std::vector cells; - - for(std::size_t i=0;i(self._connectivity.data(), self._connectivity.size()), + std::span(self._offset.data(), self._offset.size()))); }, - " Return cells.") + nb::rv_policy::move, + "Convenience packed cells view [n, v0, ...] for VTK-style consumers (allocates)." + " For high-performance workflows, prefer zero-copy 'connectivity' + 'offset'.") .def_prop_ro( "parent_map", [](const mesh::CutMesh& self) { @@ -303,13 +361,17 @@ void declare_float(nb::module_& m, std::string type) const nb::ndarray, nb::c_contig>& offset, const nb::ndarray, nb::c_contig>& vtk_type, const std::string& cut_type_str){ - nb::gil_scoped_release release; - return as_nbarray(mesh::locate_cells(std::span(ls_vals.data(),ls_vals.size()), - std::span(points.data(),points.size()), - std::span(connectivity.data(),connectivity.size()), - std::span(offset.data(),offset.size()), - std::span(vtk_type.data(),vtk_type.size()), - cell::string_to_cut_type(cut_type_str))); + std::vector located_cells; + { + nb::gil_scoped_release release; + located_cells = mesh::locate_cells(std::span(ls_vals.data(),ls_vals.size()), + std::span(points.data(),points.size()), + std::span(connectivity.data(),connectivity.size()), + std::span(offset.data(),offset.size()), + std::span(vtk_type.data(),vtk_type.size()), + cell::string_to_cut_type(cut_type_str)); + } + return as_nbarray(std::move(located_cells)); } , "locate cells in vtk mesh"); @@ -353,4 +415,16 @@ NB_MODULE(_cutcellscpp, m) declare_float(m, "float32"); declare_float(m, "float64"); + + m.def("csr_to_vtk_cells", + [](const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offsets) + { + return as_nbarray(csr_to_vtk_cells_impl( + std::span(connectivity.data(), connectivity.size()), + std::span(offsets.data(), offsets.size()))); + }, + nb::arg("connectivity"), + nb::arg("offsets"), + "Pack CSR connectivity/offsets to VTK cells layout [n0, v0..., n1, v1..., ...]."); } diff --git a/python/demo/cut_2nd_order_tetrahedron/cut_tetrahedron.py b/python/demo/cut_2nd_order_tetrahedron/cut_tetrahedron.py index 068fe07..effcca4 100644 --- a/python/demo/cut_2nd_order_tetrahedron/cut_tetrahedron.py +++ b/python/demo/cut_2nd_order_tetrahedron/cut_tetrahedron.py @@ -2,6 +2,7 @@ import numpy as np import pyvista as pv + subdivision = np.array([[2, 3], [1, 3], [1, 2], [0, 3], [0, 2], [0, 1]]) # e01 : 4 , e12: 5, e02: 6, e03: 7, e13: 8, e23: 9 @@ -64,14 +65,14 @@ def level_set(x, c, r): # pv.start_xvfb() grid_int = pv.UnstructuredGrid( - cut_cell_int.connectivity, - cut_cell_int.types, - cut_cell_int.vertex_coords, + cut_cell_int.cells, + cut_cell_int.vtk_types, + np.asarray(cut_cell_int.vertex_coords), ) grid_ext = pv.UnstructuredGrid( - cut_cell_ext.connectivity, - cut_cell_ext.types, - cut_cell_ext.vertex_coords, + cut_cell_ext.cells, + cut_cell_ext.vtk_types, + np.asarray(cut_cell_ext.vertex_coords), ) plotter = pv.Plotter() # off_screen=True diff --git a/python/demo/cut_2nd_order_triangle/cut_triangle.py b/python/demo/cut_2nd_order_triangle/cut_triangle.py index 95be98c..c7e42bf 100644 --- a/python/demo/cut_2nd_order_triangle/cut_triangle.py +++ b/python/demo/cut_2nd_order_triangle/cut_triangle.py @@ -39,8 +39,16 @@ def cut_cell_type_to_pyvista(cut_cell_type): if pts_ext.shape[1] == 2: pts_ext = np.c_[pts_ext, np.zeros((pts_ext.shape[0],), dtype=pts_ext.dtype)] -grid_int = pv.UnstructuredGrid(cut_cell_int.connectivity, cut_cell_int.types, pts_int) -grid_ext = pv.UnstructuredGrid(cut_cell_ext.connectivity, cut_cell_ext.types, pts_ext) +grid_int = pv.UnstructuredGrid( + cut_cell_int.cells, + cut_cell_int.vtk_types, + pts_int, +) +grid_ext = pv.UnstructuredGrid( + cut_cell_ext.cells, + cut_cell_ext.vtk_types, + pts_ext, +) plotter = pv.Plotter() # off_screen=True plotter.set_background("white", top="white") diff --git a/python/demo/cut_hexahedron/cut_hexahedron_cases_subplots.py b/python/demo/cut_hexahedron/cut_hexahedron_cases_subplots.py index 95afb9d..e28046e 100644 --- a/python/demo/cut_hexahedron/cut_hexahedron_cases_subplots.py +++ b/python/demo/cut_hexahedron/cut_hexahedron_cases_subplots.py @@ -121,7 +121,9 @@ def render_batch(batch_index: int) -> None: ) inside_points = np.asarray(inside_cell.vertex_coords) inside_grid = pv.UnstructuredGrid( - inside_cell.connectivity, inside_cell.types, inside_points + inside_cell.cells, + inside_cell.vtk_types, + inside_points, ) # Interface surface (phi=0) @@ -136,7 +138,9 @@ def render_batch(batch_index: int) -> None: points = np.asarray(cut_cell.vertex_coords) grid = pv.UnstructuredGrid( - cut_cell.connectivity, cut_cell.types, points + cut_cell.cells, + cut_cell.vtk_types, + points, ) edge_mask = np.array([on_unit_cube_edge(p) for p in points], dtype=bool) diff --git a/python/demo/cut_hexahedron/cut_hexahedron_gyroid_thick.py b/python/demo/cut_hexahedron/cut_hexahedron_gyroid_thick.py index a97183c..1c9f699 100644 --- a/python/demo/cut_hexahedron/cut_hexahedron_gyroid_thick.py +++ b/python/demo/cut_hexahedron/cut_hexahedron_gyroid_thick.py @@ -36,7 +36,11 @@ def create_structured_hex_mesh( def create_cut_mesh( grid: pv.UnstructuredGrid, k: float, thickness: float, triangulate: bool ) -> pv.UnstructuredGrid: - points = grid.points + points = np.asarray(grid.points, dtype=np.float64) + points_flat = points.reshape(-1) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offset = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) ls_values = np.zeros(len(points), dtype=float) for j, point in enumerate(points): @@ -44,27 +48,27 @@ def create_cut_mesh( cut_mesh = cutcells.cut_vtk_mesh( ls_values, - points, - grid.cell_connectivity, - grid.offset, - grid.celltypes, + points_flat, + connectivity, + offset, + celltypes, "phi<0", triangulate, ) inside_cells = cutcells.locate_cells( ls_values, - points, - grid.cell_connectivity, - grid.offset, - grid.celltypes, + points_flat, + connectivity, + offset, + celltypes, "phi<0", ) pv_cut = pv.UnstructuredGrid( cut_mesh.cells, - cut_mesh.types, - cut_mesh.vertex_coords, + cut_mesh.vtk_types, + np.asarray(cut_mesh.vertex_coords), ) extract = grid.extract_cells(inside_cells) @@ -151,24 +155,27 @@ def main() -> None: # Optionally write interface, inside and outside VTK files if args.write_prefix: - pts = grid.points + pts = np.asarray(grid.points, dtype=np.float64) + pts_flat = pts.reshape(-1) ls_values = np.zeros(len(pts), dtype=float) for j, point in enumerate(pts): ls_values[j] = level_set_thick_gyroid( point, k=k, thickness=float(args.thickness) ) - conn = grid.cell_connectivity - off = grid.offset - ctypes = grid.celltypes + conn = np.asarray(grid.cell_connectivity, dtype=np.int32) + off = np.asarray(grid.offset, dtype=np.int32) + ctypes = np.asarray(grid.celltypes, dtype=np.int32) # Interface (phi=0) try: iface = cutcells.cut_vtk_mesh( - ls_values, pts, conn, off, ctypes, "phi=0", args.triangulate + ls_values, pts_flat, conn, off, ctypes, "phi=0", args.triangulate ) iface_pv = pv.UnstructuredGrid( - iface.cells, iface.types, iface.vertex_coords + iface.cells, + iface.vtk_types, + np.asarray(iface.vertex_coords), ) iface_file = f"{args.write_prefix}_interface.vtu" iface_pv.save(iface_file) @@ -179,10 +186,12 @@ def main() -> None: # Inside (phi<0) try: inside = cutcells.cut_vtk_mesh( - ls_values, pts, conn, off, ctypes, "phi<0", args.triangulate + ls_values, pts_flat, conn, off, ctypes, "phi<0", args.triangulate ) inside_pv = pv.UnstructuredGrid( - inside.cells, inside.types, inside.vertex_coords + inside.cells, + inside.vtk_types, + np.asarray(inside.vertex_coords), ) inside_file = f"{args.write_prefix}_inside.vtu" inside_pv.save(inside_file) @@ -193,10 +202,12 @@ def main() -> None: # Outside (phi>0) try: outside = cutcells.cut_vtk_mesh( - ls_values, pts, conn, off, ctypes, "phi>0", args.triangulate + ls_values, pts_flat, conn, off, ctypes, "phi>0", args.triangulate ) outside_pv = pv.UnstructuredGrid( - outside.cells, outside.types, outside.vertex_coords + outside.cells, + outside.vtk_types, + np.asarray(outside.vertex_coords), ) outside_file = f"{args.write_prefix}_outside.vtu" outside_pv.save(outside_file) diff --git a/python/demo/cut_hexahedron/cut_hexahedron_n0.py b/python/demo/cut_hexahedron/cut_hexahedron_n0.py index 4574519..22ecca6 100644 --- a/python/demo/cut_hexahedron/cut_hexahedron_n0.py +++ b/python/demo/cut_hexahedron/cut_hexahedron_n0.py @@ -73,7 +73,11 @@ def main() -> None: ) points = np.asarray(cut_cell.vertex_coords) - grid = pv.UnstructuredGrid(cut_cell.connectivity, cut_cell.types, points) + grid = pv.UnstructuredGrid( + cut_cell.cells, + cut_cell.vtk_types, + points, + ) edge_mask = np.array([on_unit_cube_edge(p) for p in points], dtype=bool) interior_points = points[~edge_mask] diff --git a/python/demo/cut_prism/cut_prism_n0.py b/python/demo/cut_prism/cut_prism_n0.py index 88c04b8..716e7c7 100644 --- a/python/demo/cut_prism/cut_prism_n0.py +++ b/python/demo/cut_prism/cut_prism_n0.py @@ -71,7 +71,11 @@ def main() -> None: ) points = np.asarray(cut_cell.vertex_coords) - grid = pv.UnstructuredGrid(cut_cell.connectivity, cut_cell.types, points) + grid = pv.UnstructuredGrid( + cut_cell.cells, + cut_cell.vtk_types, + points, + ) interior_mask = np.array( [is_strictly_inside_unit_prism(p) for p in points], dtype=bool diff --git a/python/demo/cut_pyramid/cut_pyramid_n0.py b/python/demo/cut_pyramid/cut_pyramid_n0.py index 42963e2..62f2a8d 100644 --- a/python/demo/cut_pyramid/cut_pyramid_n0.py +++ b/python/demo/cut_pyramid/cut_pyramid_n0.py @@ -77,7 +77,11 @@ def main() -> None: ) points = np.asarray(cut_cell.vertex_coords) - grid = pv.UnstructuredGrid(cut_cell.connectivity, cut_cell.types, points) + grid = pv.UnstructuredGrid( + cut_cell.cells, + cut_cell.vtk_types, + points, + ) interior_mask = np.array( [is_strictly_inside_unit_pyramid(p) for p in points], dtype=bool diff --git a/python/demo/cut_quadrilateral/plot_quadrilateral_cases.py b/python/demo/cut_quadrilateral/plot_quadrilateral_cases.py index c0d7b94..254164a 100644 --- a/python/demo/cut_quadrilateral/plot_quadrilateral_cases.py +++ b/python/demo/cut_quadrilateral/plot_quadrilateral_cases.py @@ -131,8 +131,8 @@ def _plot_runtime_cutcell( ): coords = np.asarray(cut_cell.vertex_coords) pts = [(float(p[0]), float(p[1])) for p in coords] - connectivity = list(cut_cell.connectivity) - types = list(cut_cell.types) + connectivity = list(cut_cell.cells) + types = list(cut_cell.vtk_types) for cell_type, verts in zip(types, _iter_connectivity(connectivity)): poly = [pts[v] for v in verts] diff --git a/python/demo/cut_tetrahedron/cut_tetrahedron.py b/python/demo/cut_tetrahedron/cut_tetrahedron.py index 7c138dd..d7f67ae 100644 --- a/python/demo/cut_tetrahedron/cut_tetrahedron.py +++ b/python/demo/cut_tetrahedron/cut_tetrahedron.py @@ -32,14 +32,14 @@ pv.start_xvfb() grid_int = pv.UnstructuredGrid( - cut_cell_int.connectivity, - cut_cell_int.types, - cut_cell_int.vertex_coords, + cut_cell_int.cells, + cut_cell_int.vtk_types, + np.asarray(cut_cell_int.vertex_coords), ) grid_ext = pv.UnstructuredGrid( - cut_cell_ext.connectivity, - cut_cell_ext.types, - cut_cell_ext.vertex_coords, + cut_cell_ext.cells, + cut_cell_ext.vtk_types, + np.asarray(cut_cell_ext.vertex_coords), ) split_cells_int = grid_int.explode() diff --git a/python/demo/cut_triangle/cut_triangle.py b/python/demo/cut_triangle/cut_triangle.py index 9cc41c9..ae6e347 100644 --- a/python/demo/cut_triangle/cut_triangle.py +++ b/python/demo/cut_triangle/cut_triangle.py @@ -46,8 +46,16 @@ def cut_cell_type_to_pyvista(cut_cell_type): if pts_ext.shape[1] == 2: pts_ext = np.c_[pts_ext, np.zeros((pts_ext.shape[0],), dtype=pts_ext.dtype)] -grid_int = pv.UnstructuredGrid(cut_cell_int.connectivity, cut_cell_int.types, pts_int) -grid_ext = pv.UnstructuredGrid(cut_cell_ext.connectivity, cut_cell_ext.types, pts_ext) +grid_int = pv.UnstructuredGrid( + cut_cell_int.cells, + cut_cell_int.vtk_types, + pts_int, +) +grid_ext = pv.UnstructuredGrid( + cut_cell_ext.cells, + cut_cell_ext.vtk_types, + pts_ext, +) plotter = pv.Plotter(off_screen=False) plotter.set_background("white", top="white") diff --git a/python/demo/cut_vtk_mesh/cut_hex_mesh_3D.py b/python/demo/cut_vtk_mesh/cut_hex_mesh_3D.py index 7ec41c5..0aded03 100644 --- a/python/demo/cut_vtk_mesh/cut_hex_mesh_3D.py +++ b/python/demo/cut_vtk_mesh/cut_hex_mesh_3D.py @@ -88,10 +88,14 @@ def main(): ) ls_values = np.array([level_set(p) for p in points], dtype=float) + points_flat = np.asarray(points, dtype=np.float64).reshape(-1) + connectivity = np.asarray(connectivity, dtype=np.int32) + offset = np.asarray(offset, dtype=np.int32) + celltypes = np.asarray(celltypes, dtype=np.int32) cut_mesh = cutcells.cut_vtk_mesh( ls_values, - points, + points_flat, connectivity, offset, celltypes, @@ -107,8 +111,8 @@ def main(): pv_in = pv.UnstructuredGrid(cells, celltypes, points) pv_cut = pv.UnstructuredGrid( cut_mesh.cells, - cut_mesh.types, - cut_mesh.vertex_coords, + cut_mesh.vtk_types, + np.asarray(cut_mesh.vertex_coords), ) pl = pv.Plotter() diff --git a/python/demo/cut_vtk_mesh/cut_hybrid_flower_2D.py b/python/demo/cut_vtk_mesh/cut_hybrid_flower_2D.py index 2079263..71ab1bd 100644 --- a/python/demo/cut_vtk_mesh/cut_hybrid_flower_2D.py +++ b/python/demo/cut_vtk_mesh/cut_hybrid_flower_2D.py @@ -67,7 +67,11 @@ def level_set(xs, R0=1.0, a=0.25, k=6, center=(0.0, 0.0), sdf_like=True, eps=1e- def create_cut_mesh(grid): - points = grid.points + points = np.asarray(grid.points, dtype=np.float64) + points_flat = points.reshape(-1) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offset = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) ls_values = np.zeros(len(points)) j = 0 for point in points: @@ -75,14 +79,16 @@ def create_cut_mesh(grid): j = j + 1 cut_mesh = cutcells.cut_vtk_mesh( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) inside_cells = cutcells.locate_cells( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) pv_cut = pv.UnstructuredGrid( - cut_mesh.cells, cut_mesh.types, np.asarray(cut_mesh.vertex_coords) + cut_mesh.cells, + cut_mesh.vtk_types, + np.asarray(cut_mesh.vertex_coords), ) extract = grid.extract_cells(inside_cells) diff --git a/python/demo/cut_vtk_mesh/cut_mesh_2D.py b/python/demo/cut_vtk_mesh/cut_mesh_2D.py index 1a9411d..48c4c10 100644 --- a/python/demo/cut_vtk_mesh/cut_mesh_2D.py +++ b/python/demo/cut_vtk_mesh/cut_mesh_2D.py @@ -28,7 +28,11 @@ def create_rectangle_mesh(x0, y0, x1, y1, Nx, Ny): def create_cut_mesh(grid): - points = grid.points + points = np.asarray(grid.points, dtype=np.float64) + points_flat = points.reshape(-1) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offset = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) ls_values = np.zeros(len(points)) j = 0 for point in points: @@ -36,14 +40,16 @@ def create_cut_mesh(grid): j = j + 1 cut_mesh = cutcells.cut_vtk_mesh( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) inside_cells = cutcells.locate_cells( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) pv_cut = pv.UnstructuredGrid( - cut_mesh.cells, cut_mesh.types, np.asarray(cut_mesh.vertex_coords) + cut_mesh.cells, + cut_mesh.vtk_types, + np.asarray(cut_mesh.vertex_coords), ) extract = grid.extract_cells(inside_cells) diff --git a/python/demo/cut_vtk_mesh/cut_mesh_3D.py b/python/demo/cut_vtk_mesh/cut_mesh_3D.py index 7e70f2e..3cd6e62 100644 --- a/python/demo/cut_vtk_mesh/cut_mesh_3D.py +++ b/python/demo/cut_vtk_mesh/cut_mesh_3D.py @@ -71,7 +71,11 @@ def create_box_mesh(x0, y0, z0, x1, y1, z1, Nx, Ny, Nz): def create_cut_mesh(grid): - points = grid.points + points = np.asarray(grid.points, dtype=np.float64) + points_flat = points.reshape(-1) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offset = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) ls_values = np.zeros(len(points)) j = 0 for point in points: @@ -79,16 +83,16 @@ def create_cut_mesh(grid): j = j + 1 cut_mesh = cutcells.cut_vtk_mesh( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) inside_cells = cutcells.locate_cells( - ls_values, points, grid.cell_connectivity, grid.offset, grid.celltypes, "phi<0" + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" ) pv_cut = pv.UnstructuredGrid( cut_mesh.cells, - cut_mesh.types, - cut_mesh.vertex_coords, + cut_mesh.vtk_types, + np.asarray(cut_mesh.vertex_coords), ) extract = grid.extract_cells(inside_cells) diff --git a/python/demo/cut_vtk_mesh/cut_popcorn_hex_mesh_3D.py b/python/demo/cut_vtk_mesh/cut_popcorn_hex_mesh_3D.py index 3937e72..8f1cc3b 100644 --- a/python/demo/cut_vtk_mesh/cut_popcorn_hex_mesh_3D.py +++ b/python/demo/cut_vtk_mesh/cut_popcorn_hex_mesh_3D.py @@ -70,7 +70,11 @@ def create_hex_box_mesh(x0, y0, z0, x1, y1, z1, nx, ny, nz) -> pv.UnstructuredGr def create_cut_mesh( grid: pv.UnstructuredGrid, triangulate: bool ) -> pv.UnstructuredGrid: - points = grid.points + points = np.asarray(grid.points, dtype=np.float64) + points_flat = points.reshape(-1) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offset = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) ls_values = np.zeros(len(points), dtype=float) for j, point in enumerate(points): @@ -78,26 +82,26 @@ def create_cut_mesh( cut_mesh = cutcells.cut_vtk_mesh( ls_values, - points, - grid.cell_connectivity, - grid.offset, - grid.celltypes, + points_flat, + connectivity, + offset, + celltypes, "phi<0", triangulate, ) inside_cells = cutcells.locate_cells( ls_values, - points, - grid.cell_connectivity, - grid.offset, - grid.celltypes, + points_flat, + connectivity, + offset, + celltypes, "phi<0", ) pv_cut = pv.UnstructuredGrid( cut_mesh.cells, - cut_mesh.types, - cut_mesh.vertex_coords, + np.asarray(cut_mesh.types, dtype=np.uint8), + np.asarray(cut_mesh.vertex_coords), ) extract = grid.extract_cells(inside_cells) diff --git a/python/demo/cut_vtk_mesh/cut_quad_mesh_2D.py b/python/demo/cut_vtk_mesh/cut_quad_mesh_2D.py index 2d1bd5a..1b78f02 100644 --- a/python/demo/cut_vtk_mesh/cut_quad_mesh_2D.py +++ b/python/demo/cut_vtk_mesh/cut_quad_mesh_2D.py @@ -126,12 +126,22 @@ def main() -> None: # level set at points ls = np.array([level_set_popcorn_2d(p) for p in points], dtype=float) + points_flat = points.reshape(-1) + conn = np.asarray(conn, dtype=np.int32) + off = np.asarray(off, dtype=np.int32) + celltypes = np.asarray(celltypes, dtype=np.int32) # Cut mesh and also extract all inside (phi<0) background cells for context cut_mesh = cutcells.cut_vtk_mesh( - ls, points, conn, off, celltypes, "phi<0", triangulate=bool(args.triangulate) + ls, + points_flat, + conn, + off, + celltypes, + "phi<0", + triangulate=bool(args.triangulate), ) - inside_cells = cutcells.locate_cells(ls, points, conn, off, celltypes, "phi<0") + inside_cells = cutcells.locate_cells(ls, points_flat, conn, off, celltypes, "phi<0") # Build a pyvista grid for visualization cells_with_counts = np.empty((len(off) * 5,), dtype=np.int32) @@ -145,7 +155,7 @@ def main() -> None: cut_points = np.asarray(cut_mesh.vertex_coords) cut_cells = np.asarray(cut_mesh.cells, dtype=np.int32) - cut_types = np.asarray(cut_mesh.types, dtype=np.int32) + cut_types = cut_mesh.vtk_types pv_cut = pv.UnstructuredGrid(cut_cells, cut_types, cut_points) extract = bg.extract_cells(inside_cells) merged = extract.merge(pv_cut) diff --git a/python/tests/test_cut_mesh_parent_map_layout.py b/python/tests/test_cut_mesh_parent_map_layout.py new file mode 100644 index 0000000..9d126ac --- /dev/null +++ b/python/tests/test_cut_mesh_parent_map_layout.py @@ -0,0 +1,61 @@ +import numpy as np + +import cutcells + +VTK_QUAD = 9 + + +def _two_quad_strip_vtk(): + points = np.array( + [ + [0.0, 0.0, 0.0], # 0 + [1.0, 0.0, 0.0], # 1 + [2.0, 0.0, 0.0], # 2 + [0.0, 1.0, 0.0], # 3 + [1.0, 1.0, 0.0], # 4 + [2.0, 1.0, 0.0], # 5 + ], + dtype=float, + ) + + connectivity = np.array( + [ + 0, + 1, + 4, + 3, + 1, + 2, + 5, + 4, + ], + dtype=np.int32, + ) + offset = np.array([0, 4], dtype=np.int32) + vtk_type = np.array([VTK_QUAD, VTK_QUAD], dtype=np.int32) + + return points, connectivity, offset, vtk_type + + +def test_cut_mesh_parent_map_row_layout_smoke(): + points, connectivity, offset, vtk_type = _two_quad_strip_vtk() + + ls_vals = points[:, 1] - 0.5 + + cut_mesh = cutcells.cut_vtk_mesh( + ls_vals, + points.ravel(), + connectivity, + offset, + vtk_type, + "phi<0", + triangulate=False, + ) + + types = np.asarray(cut_mesh.types) + parent_map = np.asarray(cut_mesh.parent_map) + + assert types.size > 0 + assert parent_map.size == types.size + assert set(parent_map.tolist()).issubset({0, 1}) + assert 0 in parent_map and 1 in parent_map diff --git a/python/tests/test_cut_vtk_mesh_hexahedron_smoke.py b/python/tests/test_cut_vtk_mesh_hexahedron_smoke.py index 4aa3acb..d1ad5b0 100644 --- a/python/tests/test_cut_vtk_mesh_hexahedron_smoke.py +++ b/python/tests/test_cut_vtk_mesh_hexahedron_smoke.py @@ -43,7 +43,7 @@ def test_cut_vtk_mesh_single_hexahedron_smoke(): cut_mesh = cutcells.cut_vtk_mesh( ls_vals, - points, + points.ravel(), connectivity, offset, vtk_type, @@ -58,6 +58,6 @@ def test_cut_vtk_mesh_single_hexahedron_smoke(): assert coords[:, 1].min() >= -tol and coords[:, 1].max() <= 1.0 + tol assert coords[:, 2].min() >= -tol and coords[:, 2].max() <= 1.0 + tol - assert len(cut_mesh.types) > 0 + assert cut_mesh.types.size > 0 # Do not constrain emitted cell types here; the table-driven hex cutter may emit # hex/prism/pyramid/tet depending on the case. diff --git a/python/tests/test_cut_vtk_mesh_prism_smoke.py b/python/tests/test_cut_vtk_mesh_prism_smoke.py index a4ad210..0c0de17 100644 --- a/python/tests/test_cut_vtk_mesh_prism_smoke.py +++ b/python/tests/test_cut_vtk_mesh_prism_smoke.py @@ -34,7 +34,7 @@ def test_cut_vtk_mesh_single_prism_smoke(): cut_mesh = cutcells.cut_vtk_mesh( ls_vals, - points, + points.ravel(), connectivity, offset, vtk_type, @@ -48,4 +48,4 @@ def test_cut_vtk_mesh_single_prism_smoke(): assert coords[:, 1].min() >= -tol and coords[:, 1].max() <= 1.0 + tol assert coords[:, 2].min() >= -tol and coords[:, 2].max() <= 1.0 + tol - assert len(cut_mesh.types) > 0 + assert cut_mesh.types.size > 0 diff --git a/python/tests/test_cut_vtk_mesh_pyramid_smoke.py b/python/tests/test_cut_vtk_mesh_pyramid_smoke.py index a87aede..898b17c 100644 --- a/python/tests/test_cut_vtk_mesh_pyramid_smoke.py +++ b/python/tests/test_cut_vtk_mesh_pyramid_smoke.py @@ -33,7 +33,7 @@ def test_cut_vtk_mesh_single_pyramid_smoke(): cut_mesh = cutcells.cut_vtk_mesh( ls_vals, - points, + points.ravel(), connectivity, offset, vtk_type, @@ -47,4 +47,4 @@ def test_cut_vtk_mesh_single_pyramid_smoke(): assert coords[:, 1].min() >= -tol and coords[:, 1].max() <= 1.0 + tol assert coords[:, 2].min() >= -tol and coords[:, 2].max() <= 1.0 + tol - assert len(cut_mesh.types) > 0 + assert cut_mesh.types.size > 0 diff --git a/python/tests/test_cut_vtk_mesh_termination.py b/python/tests/test_cut_vtk_mesh_termination.py new file mode 100644 index 0000000..4f6be20 --- /dev/null +++ b/python/tests/test_cut_vtk_mesh_termination.py @@ -0,0 +1,63 @@ +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT + +import time + +import numpy as np + +import cutcells + +VTK_HEXAHEDRON = 12 + + +def _single_unit_hex_vtk(): + points = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + [1.0, 1.0, 1.0], + [0.0, 1.0, 1.0], + ], + dtype=float, + ) + + connectivity = np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.int32) + offset = np.array([0], dtype=np.int32) + vtk_type = np.array([VTK_HEXAHEDRON], dtype=np.int32) + + return points, connectivity, offset, vtk_type + + +def test_cut_vtk_mesh_terminates_quickly_on_small_mesh(): + points, connectivity, offset, vtk_type = _single_unit_hex_vtk() + ls_vals = points[:, 0] - 0.3 + + iterations = 200 + start = time.perf_counter() + + for _ in range(iterations): + cut_mesh = cutcells.cut_vtk_mesh( + ls_vals, + points.ravel(), + connectivity, + offset, + vtk_type, + "phi<0", + triangulate=True, + ) + + elapsed = time.perf_counter() - start + + assert cut_mesh.types.size > 0 + assert np.asarray(cut_mesh.vertex_coords).shape[1] == 3 + assert elapsed < 5.0, ( + f"cut_vtk_mesh appears too slow/non-terminating for a trivial mesh: " + f"{iterations} calls took {elapsed:.3f}s" + ) diff --git a/python/tests/test_cut_vtk_mesh_triangulate_flag.py b/python/tests/test_cut_vtk_mesh_triangulate_flag.py index 1925710..7dd7b4e 100644 --- a/python/tests/test_cut_vtk_mesh_triangulate_flag.py +++ b/python/tests/test_cut_vtk_mesh_triangulate_flag.py @@ -8,7 +8,8 @@ import cutcells -VTK_TRIANGLE = 5 +TRIANGLE = int(cutcells.CellType.triangle.value) +QUADRILATERAL = int(cutcells.CellType.quadrilateral.value) VTK_QUAD = 9 @@ -39,12 +40,24 @@ def test_cut_vtk_mesh_triangulate_flag_controls_output_types(): ls_vals = points[:, 0] - 0.3 cut_mesh_quads = cutcells.cut_vtk_mesh( - ls_vals, points, connectivity, offset, vtk_type, "phi<0", triangulate=False + ls_vals, + points.ravel(), + connectivity, + offset, + vtk_type, + "phi<0", + triangulate=False, ) - assert VTK_QUAD in list(cut_mesh_quads.types) + assert QUADRILATERAL in list(cut_mesh_quads.types) cut_mesh_tris = cutcells.cut_vtk_mesh( - ls_vals, points, connectivity, offset, vtk_type, "phi<0", triangulate=True + ls_vals, + points.ravel(), + connectivity, + offset, + vtk_type, + "phi<0", + triangulate=True, ) - assert VTK_QUAD not in list(cut_mesh_tris.types) - assert VTK_TRIANGLE in list(cut_mesh_tris.types) + assert QUADRILATERAL not in list(cut_mesh_tris.types) + assert TRIANGLE in list(cut_mesh_tris.types) diff --git a/python/tests/test_cutcell_original_info.py b/python/tests/test_cutcell_original_info.py index 23dfc7f..4257ddf 100644 --- a/python/tests/test_cutcell_original_info.py +++ b/python/tests/test_cutcell_original_info.py @@ -12,7 +12,7 @@ def test_cutcell_original_info(): # Cut it cut = cutcells.cut( - cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi<0", False + cutcells.CellType.quadrilateral, vertices.ravel(), 2, ls_values, "phi<0", False ) # For now, just verify the cut doesn't crash @@ -21,7 +21,7 @@ def test_cutcell_original_info(): # - cut._parent_vertex_coords matches vertices # - cut._parent_vertex_ids is [0,1,2,3] (or similar default) - assert len(cut.types) > 0, "Should produce some cut cells" + assert cut.types.size > 0, "Should produce some cut cells" assert cut.vertex_coords is not None, "Should have vertex coordinates" diff --git a/python/tests/test_cutcontext_edge_reuse.py b/python/tests/test_cutcontext_edge_reuse.py index 137a3f4..b9d1c90 100644 --- a/python/tests/test_cutcontext_edge_reuse.py +++ b/python/tests/test_cutcontext_edge_reuse.py @@ -25,11 +25,11 @@ def test_cutcontext_edge_reuse(): ls_values = np.array([-0.5, 0.5, 0.5, -0.5], dtype=float) cut = cutcells.cut( - cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi<0", False + cutcells.CellType.quadrilateral, vertices.ravel(), 2, ls_values, "phi<0", False ) # Basic sanity: cut should produce cells - assert len(cut.types) > 0 + assert cut.types.size > 0 assert cut.vertex_coords is not None # TODO: When CutContext is integrated, verify: diff --git a/python/tests/test_quad_table_sanity.py b/python/tests/test_quad_table_sanity.py index b450ce0..ab2f459 100644 --- a/python/tests/test_quad_table_sanity.py +++ b/python/tests/test_quad_table_sanity.py @@ -30,13 +30,13 @@ def test_quad_table_sanity(): ls_values = np.array([-0.5, 0.5, 0.5, -0.5], dtype=float) cut = cutcells.cut( - cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi<0", False + cutcells.CellType.quadrilateral, vertices.ravel(), 2, ls_values, "phi<0", False ) # Should produce valid output - assert len(cut.types) > 0 + assert cut.types.size > 0 assert cut.vertex_coords is not None # Basic validation passed if we got here - print(f"Produced {len(cut.types)} subcells") + print(f"Produced {cut.types.size} subcells") print("✓ test_quad_table_sanity passed") diff --git a/python/tests/test_quadrilateral.py b/python/tests/test_quadrilateral.py index 7809167..61fe6dc 100644 --- a/python/tests/test_quadrilateral.py +++ b/python/tests/test_quadrilateral.py @@ -8,8 +8,8 @@ import cutcells -VTK_TRIANGLE = 5 -VTK_QUAD = 9 +TRIANGLE = int(cutcells.CellType.triangle.value) +QUADRILATERAL = int(cutcells.CellType.quadrilateral.value) def _square_vertices(): @@ -23,13 +23,13 @@ def test_quadrilateral_opposite_corners_disconnected(): cut_inside = cutcells.cut( cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi<0", False ) - assert cut_inside.types == [VTK_TRIANGLE, VTK_TRIANGLE] + assert list(cut_inside.types) == [TRIANGLE, TRIANGLE] assert np.isclose(cut_inside.volume(), 0.25) cut_outside = cutcells.cut( cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi>0", False ) - assert cut_outside.types == [VTK_QUAD, VTK_QUAD] + assert list(cut_outside.types) == [QUADRILATERAL, QUADRILATERAL] assert np.isclose(cut_outside.volume(), 0.75) @@ -40,11 +40,11 @@ def test_quadrilateral_pentagon_splits_triangle_quad(): cut_inside = cutcells.cut( cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi<0", False ) - assert cut_inside.types == [VTK_TRIANGLE, VTK_QUAD] + assert list(cut_inside.types) == [TRIANGLE, QUADRILATERAL] assert np.isclose(cut_inside.volume(), 0.875) cut_outside = cutcells.cut( cutcells.CellType.quadrilateral, vertices, 2, ls_values, "phi>0", False ) - assert cut_outside.types == [VTK_TRIANGLE] + assert list(cut_outside.types) == [TRIANGLE] assert np.isclose(cut_outside.volume(), 0.125) diff --git a/python/tests/test_quadrilateral_ambiguity.py b/python/tests/test_quadrilateral_ambiguity.py index e7918f0..226bbd2 100644 --- a/python/tests/test_quadrilateral_ambiguity.py +++ b/python/tests/test_quadrilateral_ambiguity.py @@ -48,16 +48,17 @@ def test_asymptotic_decider_scale_invariant_and_different_diagonals(): def _segments_from_cutcell(cut_cell): coords = np.asarray(cut_cell.vertex_coords) - conn = list(cut_cell.connectivity) + conn = np.asarray(cut_cell.connectivity) + offsets = np.asarray(cut_cell.offsets) segments = [] - i = 0 - while i < len(conn): - n = int(conn[i]) + for i in range(len(offsets) - 1): + begin = int(offsets[i]) + end = int(offsets[i + 1]) + n = end - begin assert n == 2 - a = int(conn[i + 1]) - b = int(conn[i + 2]) + a = int(conn[begin]) + b = int(conn[begin + 1]) segments.append((coords[a], coords[b])) - i += 1 + n return segments @@ -124,12 +125,14 @@ def test_quad_ambiguous_connected_variant_keeps_quads(ls_values, expected_mask): ) types = list(inside.types) - # VTK_QUAD = 9 - assert types == [9, 9] - - conn = list(inside.connectivity) - i = 0 - for _ in range(2): - assert int(conn[i]) == 4 - i += 5 - assert i == len(conn) + assert types == [ + int(cutcells.CellType.quadrilateral.value), + int(cutcells.CellType.quadrilateral.value), + ] + + conn = np.asarray(inside.connectivity) + offsets = np.asarray(inside.offsets) + assert len(offsets) == 3 + for i in range(2): + assert int(offsets[i + 1] - offsets[i]) == 4 + assert int(offsets[-1]) == len(conn) From c26425db3cec089c31a3e87edc63f1a62299e8fb Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Mon, 9 Mar 2026 15:46:27 +0100 Subject: [PATCH 04/23] add quadrature rule routines to CutCells. Now CutCells can generate runtime rules and visualize them --- benchmarks/CMakeLists.txt | 30 +- benchmarks/bench_cut_mesh.cpp | 11 +- cpp/src/CMakeLists.txt | 5 + cpp/src/cut_cell.cpp | 33 + cpp/src/cut_cell.h | 16 +- .../generated/quadrature_tables_hexahedron.h | 762 ++++++++++ .../generated/quadrature_tables_interval.h | 118 ++ cpp/src/generated/quadrature_tables_prism.h | 762 ++++++++++ cpp/src/generated/quadrature_tables_pyramid.h | 1312 +++++++++++++++++ .../quadrature_tables_quadrilateral.h | 207 +++ .../generated/quadrature_tables_tetrahedron.h | 370 +++++ .../generated/quadrature_tables_triangle.h | 181 +++ cpp/src/mapping.cpp | 367 +++++ cpp/src/mapping.h | 140 ++ cpp/src/quadrature.cpp | 685 +++++++++ cpp/src/quadrature.h | 146 ++ cpp/src/quadrature_tables.h | 125 ++ cpp/src/triangulation.h | 119 +- python/CMakeLists.txt | 1 + python/cutcells/__init__.py | 7 + python/cutcells/wrapper.cpp | 170 ++- python/demo/quadrature/demo_quadrature_2D.py | 261 ++++ python/demo/quadrature/demo_quadrature_3D.py | 196 +++ python/tests/test_mapping.py | 223 +++ python/tests/test_quadrature.py | 309 ++++ python/tests/test_runtime_quadrature.py | 166 +++ tablegen/scripts/gen_quadrature_tables.py | 157 ++ 27 files changed, 6844 insertions(+), 35 deletions(-) create mode 100644 cpp/src/generated/quadrature_tables_hexahedron.h create mode 100644 cpp/src/generated/quadrature_tables_interval.h create mode 100644 cpp/src/generated/quadrature_tables_prism.h create mode 100644 cpp/src/generated/quadrature_tables_pyramid.h create mode 100644 cpp/src/generated/quadrature_tables_quadrilateral.h create mode 100644 cpp/src/generated/quadrature_tables_tetrahedron.h create mode 100644 cpp/src/generated/quadrature_tables_triangle.h create mode 100644 cpp/src/mapping.cpp create mode 100644 cpp/src/mapping.h create mode 100644 cpp/src/quadrature.cpp create mode 100644 cpp/src/quadrature.h create mode 100644 cpp/src/quadrature_tables.h create mode 100644 python/demo/quadrature/demo_quadrature_2D.py create mode 100644 python/demo/quadrature/demo_quadrature_3D.py create mode 100644 python/tests/test_mapping.py create mode 100644 python/tests/test_quadrature.py create mode 100644 python/tests/test_runtime_quadrature.py create mode 100644 tablegen/scripts/gen_quadrature_tables.py diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index a5d4d6e..b125227 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -25,6 +25,22 @@ function(add_cutcells_benchmark target source_file) add_executable(${target} ${source_file}) target_link_libraries(${target} PRIVATE cutcells benchmark::benchmark) target_compile_features(${target} PRIVATE cxx_std_20) + # On macOS, CMake embeds the Conda lib dir (from the transitive OpenMP + # dependency) in the binary RPATH before the build-tree src path, causing + # the OS to load the *installed* libcutcells instead of the freshly-built one. + # + # Post-build: run a cmake -P script that uses otool to find every + # Conda/miniforge RPATH entry and strips it with install_name_tool. + # The build-tree path (build-bench-release/src) that CMake already embeds + # then becomes the first — and only — place the OS looks for libcutcells. + if(APPLE) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -D "TARGET_FILE=$" + -P "${CMAKE_CURRENT_LIST_DIR}/fix_benchmark_rpath.cmake" + COMMENT "Stripping conda rpaths from ${target}" + ) + endif() endfunction() add_cutcells_benchmark(bench_cut_triangle bench_cut_triangle.cpp) @@ -32,11 +48,17 @@ add_cutcells_benchmark(bench_cut_tetrahedron bench_cut_tetrahedron.cpp) add_cutcells_benchmark(bench_cut_hex bench_cut_hex.cpp) add_cutcells_benchmark(bench_cut_mesh bench_cut_mesh.cpp) +# run_benchmarks: also set DYLD_LIBRARY_PATH as an extra safety net in case the +# rpath ordering is still overridden by the system linker. add_custom_target(run_benchmarks - COMMAND $ - COMMAND $ - COMMAND $ - COMMAND $ + COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" + $ + COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" + $ + COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" + $ + COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" + $ DEPENDS bench_cut_triangle bench_cut_tetrahedron bench_cut_hex bench_cut_mesh USES_TERMINAL COMMENT "Build and run all CutCells benchmarks" diff --git a/benchmarks/bench_cut_mesh.cpp b/benchmarks/bench_cut_mesh.cpp index 412aaaf..6cc8337 100644 --- a/benchmarks/bench_cut_mesh.cpp +++ b/benchmarks/bench_cut_mesh.cpp @@ -12,7 +12,8 @@ namespace { -constexpr int kRepeats = 1'000'000; +constexpr int kRepeatsCutVtkMesh = 10'000; +constexpr int kRepeatsCreateCutMesh = 10'000; struct RandomHexCellData { @@ -110,7 +111,7 @@ static void BM_CutVtkMesh(benchmark::State& state) for (auto _ : state) { cutcells::mesh::CutMesh out; - for (int i = 0; i < kRepeats; ++i) + for (int i = 0; i < kRepeatsCutVtkMesh; ++i) { out = cutcells::mesh::cut_vtk_mesh(data.ls, data.points, data.connectivity, data.offset, data.vtk_type, "phi=0", false); @@ -119,7 +120,7 @@ static void BM_CutVtkMesh(benchmark::State& state) benchmark::ClobberMemory(); } - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeatsCutVtkMesh); } static void BM_CreateCutMesh(benchmark::State& state) @@ -128,7 +129,7 @@ static void BM_CreateCutMesh(benchmark::State& state) for (auto _ : state) { - for (int i = 0; i < kRepeats; ++i) + for (int i = 0; i < kRepeatsCreateCutMesh; ++i) { auto cells = make_random_cut_cells(rng); auto out = cutcells::mesh::create_cut_mesh(cells); @@ -137,7 +138,7 @@ static void BM_CreateCutMesh(benchmark::State& state) benchmark::ClobberMemory(); } - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); + state.SetItemsProcessed(static_cast(state.iterations()) * kRepeatsCreateCutMesh); } BENCHMARK(BM_CutVtkMesh); diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 9781cff..aed58e4 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -28,6 +28,9 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/triangulation.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.h + ${CMAKE_CURRENT_SOURCE_DIR}/mapping.h + ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.h + ${CMAKE_CURRENT_SOURCE_DIR}/quadrature_tables.h ${CMAKE_CURRENT_SOURCE_DIR}/span_math.h ${CMAKE_CURRENT_SOURCE_DIR}/utils.h ${CMAKE_CURRENT_SOURCE_DIR}/write_tikz.h @@ -44,6 +47,8 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cut_tetrahedron.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.cpp ${CMAKE_CURRENT_SOURCE_DIR}/write_vtk.cpp ) diff --git a/cpp/src/cut_cell.cpp b/cpp/src/cut_cell.cpp index fa820e2..7afc6df 100644 --- a/cpp/src/cut_cell.cpp +++ b/cpp/src/cut_cell.cpp @@ -120,6 +120,26 @@ namespace cutcells::cell{ } } + template + void sub_cell_vertices_phys(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates) + { + int gdim = cut_cell._gdim; + const auto vertices = cell_vertices(cut_cell, id); + int num_vertices = vertices.size(); + vertex_coordinates.resize(num_vertices*gdim); + int local_vertex_id = 0; + + for(std::size_t j=0;j T volume(const CutCell &cut_cell) { @@ -176,6 +196,13 @@ namespace cutcells::cell{ default: throw std::invalid_argument("Only intervals, triangles, quadrilaterals, tetrahedra, hexahedra, prisms and pyramids are implemented for cutting so far."); break; } + // Ensure parent meta-data is always set so mapping functions can use it. + cut_cell._parent_cell_type = cell_type; + if (cut_cell._parent_vertex_coords.empty()) + { + cut_cell._parent_vertex_coords.assign( + vertex_coordinates.begin(), vertex_coordinates.end()); + } } //Cutting of 2nd order triangles (6-node) and tetrahedra (10-node) @@ -596,6 +623,12 @@ namespace cutcells::cell{ template void str(const CutCell &cut_cell); template void str(const CutCell &cut_cell); + template void sub_cell_vertices(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + template void sub_cell_vertices(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + + template void sub_cell_vertices_phys(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + template void sub_cell_vertices_phys(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + template double volume(const CutCell &cut_cell); template float volume(const CutCell &cut_cell); diff --git a/cpp/src/cut_cell.h b/cpp/src/cut_cell.h index 9790193..ef67626 100644 --- a/cpp/src/cut_cell.h +++ b/cpp/src/cut_cell.h @@ -30,9 +30,17 @@ namespace cutcells /// Topological Dimension of Cell int _tdim; - /// Coordinates of vertices of cut cell + /// Cut vertices as produced by the cutting routine. + /// Semantic: always interpreted as parent reference coordinates after frame completion. + /// When cutting is done in reference space these are ready to use. + /// When cutting is done in physical space, call complete_from_physical() which + /// copies these into _vertex_coords_phys and pulls back to fill _vertex_coords. std::vector _vertex_coords; + /// Cut vertices in physical space. + /// Empty until compute_physical_cut_vertices() or complete_from_physical() is called. + std::vector _vertex_coords_phys; + /// Vertex ids of cut cells /// Flattened cell-to-vertex connectivity in CSR layout std::vector _connectivity; @@ -67,6 +75,12 @@ namespace cutcells template void sub_cell_vertices(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + /// Gather physical-space coordinates of sub-cell id into vertex_coordinates. + /// Reads from _vertex_coords_phys, which must be filled before calling + /// (call compute_physical_cut_vertices or complete_from_physical first). + template + void sub_cell_vertices_phys(const CutCell &cut_cell, const int& id, std::vector& vertex_coordinates); + template T volume(const CutCell &cut_cell); diff --git a/cpp/src/generated/quadrature_tables_hexahedron.h b/cpp/src/generated/quadrature_tables_hexahedron.h new file mode 100644 index 0000000..f2f711b --- /dev/null +++ b/cpp/src/generated/quadrature_tables_hexahedron.h @@ -0,0 +1,762 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'hexahedron', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=3 ---- +inline constexpr int hexahedron_o1_npts = 1; +inline constexpr int hexahedron_o1_tdim = 3; +inline constexpr double hexahedron_o1_points[3] = { + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01 +}; +inline constexpr double hexahedron_o1_weights[1] = { + 1.000000000000000000e+00 +}; + +// ---- order 2: 8 point(s), tdim=3 ---- +inline constexpr int hexahedron_o2_npts = 8; +inline constexpr int hexahedron_o2_tdim = 3; +inline constexpr double hexahedron_o2_points[24] = { + 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, + 7.886751345948128655e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, 7.886751345948128655e-01 +}; +inline constexpr double hexahedron_o2_weights[8] = { + 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, + 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01 +}; + +// ---- order 3: 8 point(s), tdim=3 ---- +inline constexpr int hexahedron_o3_npts = 8; +inline constexpr int hexahedron_o3_tdim = 3; +inline constexpr double hexahedron_o3_points[24] = { + 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, + 7.886751345948128655e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, + 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01, 7.886751345948128655e-01 +}; +inline constexpr double hexahedron_o3_weights[8] = { + 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, + 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01, 1.250000000000000000e-01 +}; + +// ---- order 4: 27 point(s), tdim=3 ---- +inline constexpr int hexahedron_o4_npts = 27; +inline constexpr int hexahedron_o4_tdim = 3; +inline constexpr double hexahedron_o4_points[81] = { + 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 1.127016653792582979e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, + 5.000000000000000000e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01 +}; +inline constexpr double hexahedron_o4_weights[27] = { + 2.143347050754455807e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 8.779149519890259989e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 2.143347050754455807e-02 +}; + +// ---- order 5: 27 point(s), tdim=3 ---- +inline constexpr int hexahedron_o5_npts = 27; +inline constexpr int hexahedron_o5_tdim = 3; +inline constexpr double hexahedron_o5_points[81] = { + 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 1.127016653792582979e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, + 5.000000000000000000e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01 +}; +inline constexpr double hexahedron_o5_weights[27] = { + 2.143347050754455807e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 8.779149519890259989e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 5.486968449931410585e-02, 3.429355281207130401e-02, 2.143347050754455807e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 5.486968449931410585e-02, 3.429355281207130401e-02, + 2.143347050754455807e-02, 3.429355281207130401e-02, 2.143347050754455807e-02 +}; + +// ---- order 6: 64 point(s), tdim=3 ---- +inline constexpr int hexahedron_o6_npts = 64; +inline constexpr int hexahedron_o6_tdim = 3; +inline constexpr double hexahedron_o6_points[192] = { + 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.943184420297371373e-02, 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.943184420297371373e-02, 9.305681557970262308e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, 9.305681557970262308e-01 +}; +inline constexpr double hexahedron_o6_weights[64] = { + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 1.849254200709765650e-02, 3.466912086923912978e-02, 3.466912086923912284e-02, 1.849254200709765650e-02, + 1.849254200709764956e-02, 3.466912086923912284e-02, 3.466912086923910896e-02, 1.849254200709764956e-02, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 1.849254200709764956e-02, 3.466912086923912284e-02, 3.466912086923910896e-02, 1.849254200709764956e-02, + 1.849254200709764262e-02, 3.466912086923910896e-02, 3.466912086923909508e-02, 1.849254200709764262e-02, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03 +}; + +// ---- order 7: 64 point(s), tdim=3 ---- +inline constexpr int hexahedron_o7_npts = 64; +inline constexpr int hexahedron_o7_tdim = 3; +inline constexpr double hexahedron_o7_points[192] = { + 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.943184420297371373e-02, 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.943184420297371373e-02, 9.305681557970262308e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 3.300094782075718713e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.943184420297371373e-02, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.943184420297371373e-02, 3.300094782075718713e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 6.699905217924281287e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 9.305681557970262308e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01, 9.305681557970262308e-01 +}; +inline constexpr double hexahedron_o7_weights[64] = { + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 1.849254200709765650e-02, 3.466912086923912978e-02, 3.466912086923912284e-02, 1.849254200709765650e-02, + 1.849254200709764956e-02, 3.466912086923912284e-02, 3.466912086923910896e-02, 1.849254200709764956e-02, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 1.849254200709764956e-02, 3.466912086923912284e-02, 3.466912086923910896e-02, 1.849254200709764956e-02, + 1.849254200709764262e-02, 3.466912086923910896e-02, 3.466912086923909508e-02, 1.849254200709764262e-02, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03, + 9.863939474383808664e-03, 1.849254200709765650e-02, 1.849254200709764956e-02, 9.863939474383808664e-03, + 9.863939474383806930e-03, 1.849254200709765303e-02, 1.849254200709764609e-02, 9.863939474383806930e-03, + 5.261434686316423560e-03, 9.863939474383808664e-03, 9.863939474383806930e-03, 5.261434686316423560e-03 +}; + +// ---- order 8: 125 point(s), tdim=3 ---- +inline constexpr int hexahedron_o8_npts = 125; +inline constexpr int hexahedron_o8_tdim = 3; +inline constexpr double hexahedron_o8_points[375] = { + 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, + 4.691007703066801815e-02, 2.307653449471584461e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 5.000000000000000000e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 7.692346550528414983e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 9.530899229693319263e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 5.000000000000000000e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 4.691007703066801815e-02, + 4.691007703066801815e-02, 7.692346550528414983e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 7.692346550528414983e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 9.530899229693319263e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 9.530899229693319263e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 4.691007703066801815e-02, 5.000000000000000000e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 7.692346550528414983e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 9.530899229693319263e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 5.000000000000000000e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 7.692346550528414983e-01, + 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 7.692346550528414983e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 7.692346550528414983e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 9.530899229693319263e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 9.530899229693319263e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 9.530899229693319263e-01 +}; +inline constexpr double hexahedron_o8_weights[125] = { + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 1.370585530681736781e-02, + 1.629051763370604339e-02, 1.370585530681736781e-02, 6.784561404328525239e-03, 8.064000000000001722e-03, + 1.629051763370604339e-02, 1.936259787027560381e-02, 1.629051763370604339e-02, 8.064000000000001722e-03, + 6.784561404328525239e-03, 1.370585530681736781e-02, 1.629051763370604339e-02, 1.370585530681736781e-02, + 6.784561404328525239e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 1.629051763370604339e-02, 1.936259787027560381e-02, 1.629051763370604339e-02, 8.064000000000001722e-03, + 9.584716258668151204e-03, 1.936259787027560728e-02, 2.301401371742111193e-02, 1.936259787027560728e-02, + 9.584716258668151204e-03, 8.064000000000001722e-03, 1.629051763370604339e-02, 1.936259787027560381e-02, + 1.629051763370604339e-02, 8.064000000000001722e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 1.370585530681736781e-02, 1.629051763370604339e-02, 1.370585530681736781e-02, + 6.784561404328525239e-03, 8.064000000000001722e-03, 1.629051763370604339e-02, 1.936259787027560381e-02, + 1.629051763370604339e-02, 8.064000000000001722e-03, 6.784561404328525239e-03, 1.370585530681736781e-02, + 1.629051763370604339e-02, 1.370585530681736781e-02, 6.784561404328525239e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03 +}; + +// ---- order 9: 125 point(s), tdim=3 ---- +inline constexpr int hexahedron_o9_npts = 125; +inline constexpr int hexahedron_o9_tdim = 3; +inline constexpr double hexahedron_o9_points[375] = { + 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, + 4.691007703066801815e-02, 2.307653449471584461e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 5.000000000000000000e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 7.692346550528414983e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 9.530899229693319263e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 5.000000000000000000e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 4.691007703066801815e-02, + 4.691007703066801815e-02, 7.692346550528414983e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 7.692346550528414983e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 9.530899229693319263e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 9.530899229693319263e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 4.691007703066801815e-02, 5.000000000000000000e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 7.692346550528414983e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, + 9.530899229693319263e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, + 5.000000000000000000e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 7.692346550528414983e-01, + 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, + 7.692346550528414983e-01, 4.691007703066801815e-02, 2.307653449471584461e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 2.307653449471584461e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 4.691007703066801815e-02, 7.692346550528414983e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 4.691007703066801815e-02, 9.530899229693319263e-01, + 4.691007703066801815e-02, 2.307653449471584461e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, + 5.000000000000000000e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 2.307653449471584461e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 5.000000000000000000e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, 5.000000000000000000e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 9.530899229693319263e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 7.692346550528414983e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 7.692346550528414983e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, + 9.530899229693319263e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01, 9.530899229693319263e-01 +}; +inline constexpr double hexahedron_o9_weights[125] = { + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 1.370585530681736781e-02, + 1.629051763370604339e-02, 1.370585530681736781e-02, 6.784561404328525239e-03, 8.064000000000001722e-03, + 1.629051763370604339e-02, 1.936259787027560381e-02, 1.629051763370604339e-02, 8.064000000000001722e-03, + 6.784561404328525239e-03, 1.370585530681736781e-02, 1.629051763370604339e-02, 1.370585530681736781e-02, + 6.784561404328525239e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 1.629051763370604339e-02, 1.936259787027560381e-02, 1.629051763370604339e-02, 8.064000000000001722e-03, + 9.584716258668151204e-03, 1.936259787027560728e-02, 2.301401371742111193e-02, 1.936259787027560728e-02, + 9.584716258668151204e-03, 8.064000000000001722e-03, 1.629051763370604339e-02, 1.936259787027560381e-02, + 1.629051763370604339e-02, 8.064000000000001722e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 1.370585530681736781e-02, 1.629051763370604339e-02, 1.370585530681736781e-02, + 6.784561404328525239e-03, 8.064000000000001722e-03, 1.629051763370604339e-02, 1.936259787027560381e-02, + 1.629051763370604339e-02, 8.064000000000001722e-03, 6.784561404328525239e-03, 1.370585530681736781e-02, + 1.629051763370604339e-02, 1.370585530681736781e-02, 6.784561404328525239e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 6.784561404328525239e-03, 8.064000000000001722e-03, + 6.784561404328525239e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 8.064000000000001722e-03, + 9.584716258668149469e-03, 8.064000000000001722e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 6.784561404328525239e-03, 8.064000000000001722e-03, 6.784561404328525239e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03, 3.358438595671482240e-03, 3.991775919106036428e-03, 3.358438595671482240e-03, + 1.662467052579081651e-03 +}; + +// ---- order 10: 216 point(s), tdim=3 ---- +inline constexpr int hexahedron_o10_npts = 216; +inline constexpr int hexahedron_o10_tdim = 3; +inline constexpr double hexahedron_o10_points[648] = { + 3.376524289842397497e-02, 3.376524289842397497e-02, 3.376524289842397497e-02, 3.376524289842397497e-02, + 3.376524289842397497e-02, 1.693953067668677037e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, + 3.806904069584015060e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.376524289842397497e-02, 3.376524289842397497e-02, 8.306046932331323518e-01, 3.376524289842397497e-02, + 3.376524289842397497e-02, 9.662347571015760250e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, + 3.376524289842397497e-02, 3.376524289842397497e-02, 1.693953067668677037e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, + 1.693953067668677037e-01, 6.193095930415984940e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, + 3.806904069584015060e-01, 1.693953067668677037e-01, 3.376524289842397497e-02, 3.806904069584015060e-01, + 3.806904069584015060e-01, 3.376524289842397497e-02, 3.806904069584015060e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 9.662347571015760250e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.376524289842397497e-02, 3.376524289842397497e-02, 6.193095930415984940e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 6.193095930415984940e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, + 6.193095930415984940e-01, 6.193095930415984940e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, + 8.306046932331323518e-01, 1.693953067668677037e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, + 3.806904069584015060e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, + 8.306046932331323518e-01, 9.662347571015760250e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, + 3.376524289842397497e-02, 3.376524289842397497e-02, 9.662347571015760250e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, + 9.662347571015760250e-01, 6.193095930415984940e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, 9.662347571015760250e-01, + 1.693953067668677037e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 1.693953067668677037e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 1.693953067668677037e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 1.693953067668677037e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 1.693953067668677037e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 1.693953067668677037e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 1.693953067668677037e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 1.693953067668677037e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 1.693953067668677037e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 1.693953067668677037e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 1.693953067668677037e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 3.806904069584015060e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 3.806904069584015060e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.806904069584015060e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 3.806904069584015060e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 3.806904069584015060e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 3.806904069584015060e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 3.806904069584015060e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 3.806904069584015060e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 3.806904069584015060e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 3.806904069584015060e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 3.806904069584015060e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 3.806904069584015060e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 6.193095930415984940e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 6.193095930415984940e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 6.193095930415984940e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 6.193095930415984940e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 6.193095930415984940e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 6.193095930415984940e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 6.193095930415984940e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 6.193095930415984940e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 6.193095930415984940e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 6.193095930415984940e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 6.193095930415984940e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 6.193095930415984940e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 6.193095930415984940e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 6.193095930415984940e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 8.306046932331323518e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 8.306046932331323518e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 8.306046932331323518e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 8.306046932331323518e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 9.662347571015760250e-01, 3.376524289842397497e-02, 3.376524289842397497e-02, 9.662347571015760250e-01, + 3.376524289842397497e-02, 1.693953067668677037e-01, 9.662347571015760250e-01, 3.376524289842397497e-02, + 3.806904069584015060e-01, 9.662347571015760250e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 9.662347571015760250e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 1.693953067668677037e-01, 1.693953067668677037e-01, + 9.662347571015760250e-01, 1.693953067668677037e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 1.693953067668677037e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 9.662347571015760250e-01, 3.806904069584015060e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, + 3.806904069584015060e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 9.662347571015760250e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 6.193095930415984940e-01, 1.693953067668677037e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 6.193095930415984940e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 9.662347571015760250e-01, 8.306046932331323518e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, + 8.306046932331323518e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, 8.306046932331323518e-01, + 3.806904069584015060e-01, 9.662347571015760250e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 9.662347571015760250e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 3.376524289842397497e-02, 9.662347571015760250e-01, 9.662347571015760250e-01, 1.693953067668677037e-01, + 9.662347571015760250e-01, 9.662347571015760250e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 9.662347571015760250e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01, 9.662347571015760250e-01, 9.662347571015760250e-01 +}; +inline constexpr double hexahedron_o10_weights[216] = { + 6.285913119545592570e-04, 1.323637895294248364e-03, 1.716781003873725248e-03, 1.716781003873724164e-03, + 1.323637895294248364e-03, 6.285913119545592570e-04, 1.323637895294248364e-03, 2.787212047858594107e-03, + 3.615061728395055352e-03, 3.615061728395053183e-03, 2.787212047858594107e-03, 1.323637895294248364e-03, + 1.716781003873725465e-03, 3.615061728395055785e-03, 4.688796932457027673e-03, 4.688796932457025071e-03, + 3.615061728395055785e-03, 1.716781003873725465e-03, 1.716781003873724164e-03, 3.615061728395053183e-03, + 4.688796932457024204e-03, 4.688796932457021602e-03, 3.615061728395053183e-03, 1.716781003873724164e-03, + 1.323637895294248364e-03, 2.787212047858594107e-03, 3.615061728395055352e-03, 3.615061728395053183e-03, + 2.787212047858594107e-03, 1.323637895294248364e-03, 6.285913119545592570e-04, 1.323637895294248364e-03, + 1.716781003873725248e-03, 1.716781003873724164e-03, 1.323637895294248364e-03, 6.285913119545592570e-04, + 1.323637895294248364e-03, 2.787212047858594107e-03, 3.615061728395055352e-03, 3.615061728395053183e-03, + 2.787212047858594107e-03, 1.323637895294248364e-03, 2.787212047858593673e-03, 5.869090804476496254e-03, + 7.612311221185836217e-03, 7.612311221185831880e-03, 5.869090804476496254e-03, 2.787212047858593673e-03, + 3.615061728395054918e-03, 7.612311221185836217e-03, 9.873297936367590732e-03, 9.873297936367585528e-03, + 7.612311221185836217e-03, 3.615061728395054918e-03, 3.615061728395053183e-03, 7.612311221185832748e-03, + 9.873297936367587263e-03, 9.873297936367580324e-03, 7.612311221185832748e-03, 3.615061728395053183e-03, + 2.787212047858593673e-03, 5.869090804476496254e-03, 7.612311221185836217e-03, 7.612311221185831880e-03, + 5.869090804476496254e-03, 2.787212047858593673e-03, 1.323637895294248364e-03, 2.787212047858594107e-03, + 3.615061728395055352e-03, 3.615061728395053183e-03, 2.787212047858594107e-03, 1.323637895294248364e-03, + 1.716781003873725465e-03, 3.615061728395055785e-03, 4.688796932457027673e-03, 4.688796932457025071e-03, + 3.615061728395055785e-03, 1.716781003873725465e-03, 3.615061728395054918e-03, 7.612311221185836217e-03, + 9.873297936367590732e-03, 9.873297936367585528e-03, 7.612311221185836217e-03, 3.615061728395054918e-03, + 4.688796932457027673e-03, 9.873297936367592467e-03, 1.280583640208747542e-02, 1.280583640208746848e-02, + 9.873297936367592467e-03, 4.688796932457027673e-03, 4.688796932457024204e-03, 9.873297936367585528e-03, + 1.280583640208746675e-02, 1.280583640208745981e-02, 9.873297936367585528e-03, 4.688796932457024204e-03, + 3.615061728395054918e-03, 7.612311221185836217e-03, 9.873297936367590732e-03, 9.873297936367585528e-03, + 7.612311221185836217e-03, 3.615061728395054918e-03, 1.716781003873725465e-03, 3.615061728395055785e-03, + 4.688796932457027673e-03, 4.688796932457025071e-03, 3.615061728395055785e-03, 1.716781003873725465e-03, + 1.716781003873724164e-03, 3.615061728395053183e-03, 4.688796932457024204e-03, 4.688796932457021602e-03, + 3.615061728395053183e-03, 1.716781003873724164e-03, 3.615061728395053183e-03, 7.612311221185832748e-03, + 9.873297936367587263e-03, 9.873297936367580324e-03, 7.612311221185832748e-03, 3.615061728395053183e-03, + 4.688796932457024204e-03, 9.873297936367585528e-03, 1.280583640208746675e-02, 1.280583640208745981e-02, + 9.873297936367585528e-03, 4.688796932457024204e-03, 4.688796932457022469e-03, 9.873297936367580324e-03, + 1.280583640208746155e-02, 1.280583640208745287e-02, 9.873297936367580324e-03, 4.688796932457022469e-03, + 3.615061728395053183e-03, 7.612311221185832748e-03, 9.873297936367587263e-03, 9.873297936367580324e-03, + 7.612311221185832748e-03, 3.615061728395053183e-03, 1.716781003873724164e-03, 3.615061728395053183e-03, + 4.688796932457024204e-03, 4.688796932457021602e-03, 3.615061728395053183e-03, 1.716781003873724164e-03, + 1.323637895294248364e-03, 2.787212047858594107e-03, 3.615061728395055352e-03, 3.615061728395053183e-03, + 2.787212047858594107e-03, 1.323637895294248364e-03, 2.787212047858593673e-03, 5.869090804476496254e-03, + 7.612311221185836217e-03, 7.612311221185831880e-03, 5.869090804476496254e-03, 2.787212047858593673e-03, + 3.615061728395054918e-03, 7.612311221185836217e-03, 9.873297936367590732e-03, 9.873297936367585528e-03, + 7.612311221185836217e-03, 3.615061728395054918e-03, 3.615061728395053183e-03, 7.612311221185832748e-03, + 9.873297936367587263e-03, 9.873297936367580324e-03, 7.612311221185832748e-03, 3.615061728395053183e-03, + 2.787212047858593673e-03, 5.869090804476496254e-03, 7.612311221185836217e-03, 7.612311221185831880e-03, + 5.869090804476496254e-03, 2.787212047858593673e-03, 1.323637895294248364e-03, 2.787212047858594107e-03, + 3.615061728395055352e-03, 3.615061728395053183e-03, 2.787212047858594107e-03, 1.323637895294248364e-03, + 6.285913119545592570e-04, 1.323637895294248364e-03, 1.716781003873725248e-03, 1.716781003873724164e-03, + 1.323637895294248364e-03, 6.285913119545592570e-04, 1.323637895294248364e-03, 2.787212047858594107e-03, + 3.615061728395055352e-03, 3.615061728395053183e-03, 2.787212047858594107e-03, 1.323637895294248364e-03, + 1.716781003873725465e-03, 3.615061728395055785e-03, 4.688796932457027673e-03, 4.688796932457025071e-03, + 3.615061728395055785e-03, 1.716781003873725465e-03, 1.716781003873724164e-03, 3.615061728395053183e-03, + 4.688796932457024204e-03, 4.688796932457021602e-03, 3.615061728395053183e-03, 1.716781003873724164e-03, + 1.323637895294248364e-03, 2.787212047858594107e-03, 3.615061728395055352e-03, 3.615061728395053183e-03, + 2.787212047858594107e-03, 1.323637895294248364e-03, 6.285913119545592570e-04, 1.323637895294248364e-03, + 1.716781003873725248e-03, 1.716781003873724164e-03, 1.323637895294248364e-03, 6.285913119545592570e-04 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_interval.h b/cpp/src/generated/quadrature_tables_interval.h new file mode 100644 index 0000000..c3662a2 --- /dev/null +++ b/cpp/src/generated/quadrature_tables_interval.h @@ -0,0 +1,118 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'interval', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=1 ---- +inline constexpr int interval_o1_npts = 1; +inline constexpr int interval_o1_tdim = 1; +inline constexpr double interval_o1_points[1] = { + 5.000000000000000000e-01 +}; +inline constexpr double interval_o1_weights[1] = { + 1.000000000000000000e+00 +}; + +// ---- order 2: 2 point(s), tdim=1 ---- +inline constexpr int interval_o2_npts = 2; +inline constexpr int interval_o2_tdim = 1; +inline constexpr double interval_o2_points[2] = { + 2.113248654051871345e-01, 7.886751345948128655e-01 +}; +inline constexpr double interval_o2_weights[2] = { + 5.000000000000000000e-01, 5.000000000000000000e-01 +}; + +// ---- order 3: 2 point(s), tdim=1 ---- +inline constexpr int interval_o3_npts = 2; +inline constexpr int interval_o3_tdim = 1; +inline constexpr double interval_o3_points[2] = { + 2.113248654051871345e-01, 7.886751345948128655e-01 +}; +inline constexpr double interval_o3_weights[2] = { + 5.000000000000000000e-01, 5.000000000000000000e-01 +}; + +// ---- order 4: 3 point(s), tdim=1 ---- +inline constexpr int interval_o4_npts = 3; +inline constexpr int interval_o4_tdim = 1; +inline constexpr double interval_o4_points[3] = { + 1.127016653792582979e-01, 5.000000000000000000e-01, 8.872983346207417021e-01 +}; +inline constexpr double interval_o4_weights[3] = { + 2.777777777777776791e-01, 4.444444444444444198e-01, 2.777777777777776791e-01 +}; + +// ---- order 5: 3 point(s), tdim=1 ---- +inline constexpr int interval_o5_npts = 3; +inline constexpr int interval_o5_tdim = 1; +inline constexpr double interval_o5_points[3] = { + 1.127016653792582979e-01, 5.000000000000000000e-01, 8.872983346207417021e-01 +}; +inline constexpr double interval_o5_weights[3] = { + 2.777777777777776791e-01, 4.444444444444444198e-01, 2.777777777777776791e-01 +}; + +// ---- order 6: 4 point(s), tdim=1 ---- +inline constexpr int interval_o6_npts = 4; +inline constexpr int interval_o6_tdim = 1; +inline constexpr double interval_o6_points[4] = { + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01 +}; +inline constexpr double interval_o6_weights[4] = { + 1.739274225687268416e-01, 3.260725774312731029e-01, 3.260725774312729919e-01, 1.739274225687268416e-01 +}; + +// ---- order 7: 4 point(s), tdim=1 ---- +inline constexpr int interval_o7_npts = 4; +inline constexpr int interval_o7_tdim = 1; +inline constexpr double interval_o7_points[4] = { + 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01 +}; +inline constexpr double interval_o7_weights[4] = { + 1.739274225687268416e-01, 3.260725774312731029e-01, 3.260725774312729919e-01, 1.739274225687268416e-01 +}; + +// ---- order 8: 5 point(s), tdim=1 ---- +inline constexpr int interval_o8_npts = 5; +inline constexpr int interval_o8_tdim = 1; +inline constexpr double interval_o8_points[5] = { + 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01 +}; +inline constexpr double interval_o8_weights[5] = { + 1.184634425280945563e-01, 2.393143352496833187e-01, 2.844444444444443887e-01, 2.393143352496833187e-01, + 1.184634425280945563e-01 +}; + +// ---- order 9: 5 point(s), tdim=1 ---- +inline constexpr int interval_o9_npts = 5; +inline constexpr int interval_o9_tdim = 1; +inline constexpr double interval_o9_points[5] = { + 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01 +}; +inline constexpr double interval_o9_weights[5] = { + 1.184634425280945563e-01, 2.393143352496833187e-01, 2.844444444444443887e-01, 2.393143352496833187e-01, + 1.184634425280945563e-01 +}; + +// ---- order 10: 6 point(s), tdim=1 ---- +inline constexpr int interval_o10_npts = 6; +inline constexpr int interval_o10_tdim = 1; +inline constexpr double interval_o10_points[6] = { + 3.376524289842397497e-02, 1.693953067668677037e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 9.662347571015760250e-01 +}; +inline constexpr double interval_o10_weights[6] = { + 8.566224618958498405e-02, 1.803807865240693031e-01, 2.339569672863456296e-01, 2.339569672863454908e-01, + 1.803807865240693031e-01, 8.566224618958498405e-02 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_prism.h b/cpp/src/generated/quadrature_tables_prism.h new file mode 100644 index 0000000..d486d5f --- /dev/null +++ b/cpp/src/generated/quadrature_tables_prism.h @@ -0,0 +1,762 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'prism', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=3 ---- +inline constexpr int prism_o1_npts = 1; +inline constexpr int prism_o1_tdim = 3; +inline constexpr double prism_o1_points[3] = { + 3.333333333333333148e-01, 3.333333333333333703e-01, 5.000000000000000000e-01 +}; +inline constexpr double prism_o1_weights[1] = { + 5.000000000000000000e-01 +}; + +// ---- order 2: 8 point(s), tdim=3 ---- +inline constexpr int prism_o2_npts = 8; +inline constexpr int prism_o2_tdim = 3; +inline constexpr double prism_o2_points[24] = { + 1.785587282636164341e-01, 1.550510257216821675e-01, 2.113248654051871345e-01, 1.785587282636164341e-01, + 1.550510257216821675e-01, 7.886751345948128655e-01, 7.503111022260813834e-02, 6.449489742783177659e-01, + 2.113248654051871345e-01, 7.503111022260813834e-02, 6.449489742783177659e-01, 7.886751345948128655e-01, + 6.663902460147014262e-01, 1.550510257216821675e-01, 2.113248654051871345e-01, 6.663902460147014262e-01, + 1.550510257216821675e-01, 7.886751345948128655e-01, 2.800199154990741235e-01, 6.449489742783177659e-01, + 2.113248654051871345e-01, 2.800199154990741235e-01, 6.449489742783177659e-01, 7.886751345948128655e-01 +}; +inline constexpr double prism_o2_weights[8] = { + 7.951034543599429238e-02, 7.951034543599429238e-02, 4.548965456400570762e-02, 4.548965456400570762e-02, + 7.951034543599429238e-02, 7.951034543599429238e-02, 4.548965456400570762e-02, 4.548965456400570762e-02 +}; + +// ---- order 3: 8 point(s), tdim=3 ---- +inline constexpr int prism_o3_npts = 8; +inline constexpr int prism_o3_tdim = 3; +inline constexpr double prism_o3_points[24] = { + 1.785587282636164341e-01, 1.550510257216821675e-01, 2.113248654051871345e-01, 1.785587282636164341e-01, + 1.550510257216821675e-01, 7.886751345948128655e-01, 7.503111022260813834e-02, 6.449489742783177659e-01, + 2.113248654051871345e-01, 7.503111022260813834e-02, 6.449489742783177659e-01, 7.886751345948128655e-01, + 6.663902460147014262e-01, 1.550510257216821675e-01, 2.113248654051871345e-01, 6.663902460147014262e-01, + 1.550510257216821675e-01, 7.886751345948128655e-01, 2.800199154990741235e-01, 6.449489742783177659e-01, + 2.113248654051871345e-01, 2.800199154990741235e-01, 6.449489742783177659e-01, 7.886751345948128655e-01 +}; +inline constexpr double prism_o3_weights[8] = { + 7.951034543599429238e-02, 7.951034543599429238e-02, 4.548965456400570762e-02, 4.548965456400570762e-02, + 7.951034543599429238e-02, 7.951034543599429238e-02, 4.548965456400570762e-02, 4.548965456400570762e-02 +}; + +// ---- order 4: 27 point(s), tdim=3 ---- +inline constexpr int prism_o4_npts = 27; +inline constexpr int prism_o4_tdim = 3; +inline constexpr double prism_o4_points[81] = { + 1.027176548096262604e-01, 8.858795951270398428e-02, 1.127016653792582979e-01, 1.027176548096262604e-01, + 8.858795951270398428e-02, 5.000000000000000000e-01, 1.027176548096262604e-01, 8.858795951270398428e-02, + 8.872983346207417021e-01, 6.655406783916449631e-02, 4.094668644407347124e-01, 1.127016653792582979e-01, + 6.655406783916449631e-02, 4.094668644407347124e-01, 5.000000000000000000e-01, 6.655406783916449631e-02, + 4.094668644407347124e-01, 8.872983346207417021e-01, 2.393113228708061702e-02, 7.876594617608470017e-01, + 1.127016653792582979e-01, 2.393113228708061702e-02, 7.876594617608470017e-01, 5.000000000000000000e-01, + 2.393113228708061702e-02, 7.876594617608470017e-01, 8.872983346207417021e-01, 4.557060202436480356e-01, + 8.858795951270398428e-02, 1.127016653792582979e-01, 4.557060202436480356e-01, 8.858795951270398428e-02, + 5.000000000000000000e-01, 4.557060202436480356e-01, 8.858795951270398428e-02, 8.872983346207417021e-01, + 2.952665677796326715e-01, 4.094668644407347124e-01, 1.127016653792582979e-01, 2.952665677796326715e-01, + 4.094668644407347124e-01, 5.000000000000000000e-01, 2.952665677796326715e-01, 4.094668644407347124e-01, + 8.872983346207417021e-01, 1.061702691195764714e-01, 7.876594617608470017e-01, 1.127016653792582979e-01, + 1.061702691195764714e-01, 7.876594617608470017e-01, 5.000000000000000000e-01, 1.061702691195764714e-01, + 7.876594617608470017e-01, 8.872983346207417021e-01, 8.086943856776698247e-01, 8.858795951270398428e-02, + 1.127016653792582979e-01, 8.086943856776698247e-01, 8.858795951270398428e-02, 5.000000000000000000e-01, + 8.086943856776698247e-01, 8.858795951270398428e-02, 8.872983346207417021e-01, 5.239790677201008329e-01, + 4.094668644407347124e-01, 1.127016653792582979e-01, 5.239790677201008329e-01, 4.094668644407347124e-01, + 5.000000000000000000e-01, 5.239790677201008329e-01, 4.094668644407347124e-01, 8.872983346207417021e-01, + 1.884094059520723397e-01, 7.876594617608470017e-01, 1.127016653792582979e-01, 1.884094059520723397e-01, + 7.876594617608470017e-01, 5.000000000000000000e-01, 1.884094059520723397e-01, 7.876594617608470017e-01, + 8.872983346207417021e-01 +}; +inline constexpr double prism_o4_weights[27] = { + 1.550400568973453463e-02, 2.480640910357526097e-02, 1.550400568973453463e-02, 1.768835697219028891e-02, + 2.830137115550447266e-02, 1.768835697219028891e-02, 5.387884251655403883e-03, 8.620614802648649336e-03, + 5.387884251655403883e-03, 2.480640910357526444e-02, 3.969025456572043697e-02, 2.480640910357526444e-02, + 2.830137115550447266e-02, 4.528219384880716875e-02, 2.830137115550447266e-02, 8.620614802648649336e-03, + 1.379298368423784345e-02, 8.620614802648649336e-03, 1.550400568973453463e-02, 2.480640910357526097e-02, + 1.550400568973453463e-02, 1.768835697219028891e-02, 2.830137115550447266e-02, 1.768835697219028891e-02, + 5.387884251655403883e-03, 8.620614802648649336e-03, 5.387884251655403883e-03 +}; + +// ---- order 5: 27 point(s), tdim=3 ---- +inline constexpr int prism_o5_npts = 27; +inline constexpr int prism_o5_tdim = 3; +inline constexpr double prism_o5_points[81] = { + 1.027176548096262604e-01, 8.858795951270398428e-02, 1.127016653792582979e-01, 1.027176548096262604e-01, + 8.858795951270398428e-02, 5.000000000000000000e-01, 1.027176548096262604e-01, 8.858795951270398428e-02, + 8.872983346207417021e-01, 6.655406783916449631e-02, 4.094668644407347124e-01, 1.127016653792582979e-01, + 6.655406783916449631e-02, 4.094668644407347124e-01, 5.000000000000000000e-01, 6.655406783916449631e-02, + 4.094668644407347124e-01, 8.872983346207417021e-01, 2.393113228708061702e-02, 7.876594617608470017e-01, + 1.127016653792582979e-01, 2.393113228708061702e-02, 7.876594617608470017e-01, 5.000000000000000000e-01, + 2.393113228708061702e-02, 7.876594617608470017e-01, 8.872983346207417021e-01, 4.557060202436480356e-01, + 8.858795951270398428e-02, 1.127016653792582979e-01, 4.557060202436480356e-01, 8.858795951270398428e-02, + 5.000000000000000000e-01, 4.557060202436480356e-01, 8.858795951270398428e-02, 8.872983346207417021e-01, + 2.952665677796326715e-01, 4.094668644407347124e-01, 1.127016653792582979e-01, 2.952665677796326715e-01, + 4.094668644407347124e-01, 5.000000000000000000e-01, 2.952665677796326715e-01, 4.094668644407347124e-01, + 8.872983346207417021e-01, 1.061702691195764714e-01, 7.876594617608470017e-01, 1.127016653792582979e-01, + 1.061702691195764714e-01, 7.876594617608470017e-01, 5.000000000000000000e-01, 1.061702691195764714e-01, + 7.876594617608470017e-01, 8.872983346207417021e-01, 8.086943856776698247e-01, 8.858795951270398428e-02, + 1.127016653792582979e-01, 8.086943856776698247e-01, 8.858795951270398428e-02, 5.000000000000000000e-01, + 8.086943856776698247e-01, 8.858795951270398428e-02, 8.872983346207417021e-01, 5.239790677201008329e-01, + 4.094668644407347124e-01, 1.127016653792582979e-01, 5.239790677201008329e-01, 4.094668644407347124e-01, + 5.000000000000000000e-01, 5.239790677201008329e-01, 4.094668644407347124e-01, 8.872983346207417021e-01, + 1.884094059520723397e-01, 7.876594617608470017e-01, 1.127016653792582979e-01, 1.884094059520723397e-01, + 7.876594617608470017e-01, 5.000000000000000000e-01, 1.884094059520723397e-01, 7.876594617608470017e-01, + 8.872983346207417021e-01 +}; +inline constexpr double prism_o5_weights[27] = { + 1.550400568973453463e-02, 2.480640910357526097e-02, 1.550400568973453463e-02, 1.768835697219028891e-02, + 2.830137115550447266e-02, 1.768835697219028891e-02, 5.387884251655403883e-03, 8.620614802648649336e-03, + 5.387884251655403883e-03, 2.480640910357526444e-02, 3.969025456572043697e-02, 2.480640910357526444e-02, + 2.830137115550447266e-02, 4.528219384880716875e-02, 2.830137115550447266e-02, 8.620614802648649336e-03, + 1.379298368423784345e-02, 8.620614802648649336e-03, 1.550400568973453463e-02, 2.480640910357526097e-02, + 1.550400568973453463e-02, 1.768835697219028891e-02, 2.830137115550447266e-02, 1.768835697219028891e-02, + 5.387884251655403883e-03, 8.620614802648649336e-03, 5.387884251655403883e-03 +}; + +// ---- order 6: 64 point(s), tdim=3 ---- +inline constexpr int prism_o6_npts = 64; +inline constexpr int prism_o6_tdim = 3; +inline constexpr double prism_o6_points[192] = { + 6.546699455501446552e-02, 5.710419611451766908e-02, 6.943184420297371373e-02, 6.546699455501446552e-02, + 5.710419611451766908e-02, 3.300094782075718713e-01, 6.546699455501446552e-02, 5.710419611451766908e-02, + 6.699905217924281287e-01, 6.546699455501446552e-02, 5.710419611451766908e-02, 9.305681557970262308e-01, + 5.021012321136977818e-02, 2.768430136381238027e-01, 6.943184420297371373e-02, 5.021012321136977818e-02, + 2.768430136381238027e-01, 3.300094782075718713e-01, 5.021012321136977818e-02, 2.768430136381238027e-01, + 6.699905217924281287e-01, 5.021012321136977818e-02, 2.768430136381238027e-01, 9.305681557970262308e-01, + 2.891208422438901193e-02, 5.835904323689168338e-01, 6.943184420297371373e-02, 2.891208422438901193e-02, + 5.835904323689168338e-01, 3.300094782075718713e-01, 2.891208422438901193e-02, 5.835904323689168338e-01, + 6.699905217924281287e-01, 2.891208422438901193e-02, 5.835904323689168338e-01, 9.305681557970262308e-01, + 9.703785126946112838e-03, 8.602401356562194845e-01, 6.943184420297371373e-02, 9.703785126946112838e-03, + 8.602401356562194845e-01, 3.300094782075718713e-01, 9.703785126946112838e-03, 8.602401356562194845e-01, + 6.699905217924281287e-01, 9.703785126946112838e-03, 8.602401356562194845e-01, 9.305681557970262308e-01, + 3.111645522443570178e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 3.111645522443570178e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 3.111645522443570178e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 3.111645522443570178e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 2.386486597314429192e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 2.386486597314429192e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 2.386486597314429192e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 2.386486597314429192e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 1.374191041345743658e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 1.374191041345743658e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 1.374191041345743658e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 1.374191041345743658e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 4.612207990645205563e-02, 8.602401356562194845e-01, 6.943184420297371373e-02, 4.612207990645205563e-02, + 8.602401356562194845e-01, 3.300094782075718713e-01, 4.612207990645205563e-02, 8.602401356562194845e-01, + 6.699905217924281287e-01, 4.612207990645205563e-02, 8.602401356562194845e-01, 9.305681557970262308e-01, + 6.317312516411253132e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 6.317312516411253132e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 6.317312516411253132e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 6.317312516411253132e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 4.845083266304332503e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 4.845083266304332503e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 4.845083266304332503e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 4.845083266304332503e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 2.789904634965088004e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 2.789904634965088004e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 2.789904634965088004e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 2.789904634965088004e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 9.363778443732852230e-02, 8.602401356562194845e-01, 6.943184420297371373e-02, 9.363778443732852230e-02, + 8.602401356562194845e-01, 3.300094782075718713e-01, 9.363778443732852230e-02, 8.602401356562194845e-01, + 6.699905217924281287e-01, 9.363778443732852230e-02, 8.602401356562194845e-01, 9.305681557970262308e-01, + 8.774288093304678515e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 8.774288093304678515e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 8.774288093304678515e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 8.774288093304678515e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 6.729468631505063358e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 6.729468631505063358e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 6.729468631505063358e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 6.729468631505063358e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 3.874974834066941543e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 3.874974834066941543e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 3.874974834066941543e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 3.874974834066941543e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 1.300560792168344582e-01, 8.602401356562194845e-01, 6.943184420297371373e-02, 1.300560792168344582e-01, + 8.602401356562194845e-01, 3.300094782075718713e-01, 1.300560792168344582e-01, 8.602401356562194845e-01, + 6.699905217924281287e-01, 1.300560792168344582e-01, 8.602401356562194845e-01, 9.305681557970262308e-01 +}; +inline constexpr double prism_o6_weights[64] = { + 4.099185534025746348e-03, 7.684998562665407498e-03, 7.684998562665404896e-03, 4.099185534025746348e-03, + 6.154955439201190322e-03, 1.153907850984177766e-02, 1.153907850984177419e-02, 6.154955439201190322e-03, + 3.927985482847706419e-03, 7.364039158337254941e-03, 7.364039158337252339e-03, 3.927985482847706419e-03, + 9.432477046255943395e-04, 1.768365250637032654e-03, 1.768365250637032003e-03, 9.432477046255943395e-04, + 7.684998562665406631e-03, 1.440754569851544417e-02, 1.440754569851543897e-02, 7.684998562665406631e-03, + 1.153907850984177939e-02, 2.163302954368310355e-02, 2.163302954368309314e-02, 1.153907850984177939e-02, + 7.364039158337254941e-03, 1.380582310253589302e-02, 1.380582310253588782e-02, 7.364039158337254941e-03, + 1.768365250637032654e-03, 3.315264531602363316e-03, 3.315264531602362015e-03, 1.768365250637032654e-03, + 7.684998562665404029e-03, 1.440754569851543897e-02, 1.440754569851543550e-02, 7.684998562665404029e-03, + 1.153907850984177419e-02, 2.163302954368309314e-02, 2.163302954368308620e-02, 1.153907850984177419e-02, + 7.364039158337252339e-03, 1.380582310253588955e-02, 1.380582310253588435e-02, 7.364039158337252339e-03, + 1.768365250637032003e-03, 3.315264531602362015e-03, 3.315264531602361148e-03, 1.768365250637032003e-03, + 4.099185534025746348e-03, 7.684998562665407498e-03, 7.684998562665404896e-03, 4.099185534025746348e-03, + 6.154955439201190322e-03, 1.153907850984177766e-02, 1.153907850984177419e-02, 6.154955439201190322e-03, + 3.927985482847706419e-03, 7.364039158337254941e-03, 7.364039158337252339e-03, 3.927985482847706419e-03, + 9.432477046255943395e-04, 1.768365250637032654e-03, 1.768365250637032003e-03, 9.432477046255943395e-04 +}; + +// ---- order 7: 64 point(s), tdim=3 ---- +inline constexpr int prism_o7_npts = 64; +inline constexpr int prism_o7_tdim = 3; +inline constexpr double prism_o7_points[192] = { + 6.546699455501446552e-02, 5.710419611451766908e-02, 6.943184420297371373e-02, 6.546699455501446552e-02, + 5.710419611451766908e-02, 3.300094782075718713e-01, 6.546699455501446552e-02, 5.710419611451766908e-02, + 6.699905217924281287e-01, 6.546699455501446552e-02, 5.710419611451766908e-02, 9.305681557970262308e-01, + 5.021012321136977818e-02, 2.768430136381238027e-01, 6.943184420297371373e-02, 5.021012321136977818e-02, + 2.768430136381238027e-01, 3.300094782075718713e-01, 5.021012321136977818e-02, 2.768430136381238027e-01, + 6.699905217924281287e-01, 5.021012321136977818e-02, 2.768430136381238027e-01, 9.305681557970262308e-01, + 2.891208422438901193e-02, 5.835904323689168338e-01, 6.943184420297371373e-02, 2.891208422438901193e-02, + 5.835904323689168338e-01, 3.300094782075718713e-01, 2.891208422438901193e-02, 5.835904323689168338e-01, + 6.699905217924281287e-01, 2.891208422438901193e-02, 5.835904323689168338e-01, 9.305681557970262308e-01, + 9.703785126946112838e-03, 8.602401356562194845e-01, 6.943184420297371373e-02, 9.703785126946112838e-03, + 8.602401356562194845e-01, 3.300094782075718713e-01, 9.703785126946112838e-03, 8.602401356562194845e-01, + 6.699905217924281287e-01, 9.703785126946112838e-03, 8.602401356562194845e-01, 9.305681557970262308e-01, + 3.111645522443570178e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 3.111645522443570178e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 3.111645522443570178e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 3.111645522443570178e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 2.386486597314429192e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 2.386486597314429192e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 2.386486597314429192e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 2.386486597314429192e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 1.374191041345743658e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 1.374191041345743658e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 1.374191041345743658e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 1.374191041345743658e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 4.612207990645205563e-02, 8.602401356562194845e-01, 6.943184420297371373e-02, 4.612207990645205563e-02, + 8.602401356562194845e-01, 3.300094782075718713e-01, 4.612207990645205563e-02, 8.602401356562194845e-01, + 6.699905217924281287e-01, 4.612207990645205563e-02, 8.602401356562194845e-01, 9.305681557970262308e-01, + 6.317312516411253132e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 6.317312516411253132e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 6.317312516411253132e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 6.317312516411253132e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 4.845083266304332503e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 4.845083266304332503e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 4.845083266304332503e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 4.845083266304332503e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 2.789904634965088004e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 2.789904634965088004e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 2.789904634965088004e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 2.789904634965088004e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 9.363778443732852230e-02, 8.602401356562194845e-01, 6.943184420297371373e-02, 9.363778443732852230e-02, + 8.602401356562194845e-01, 3.300094782075718713e-01, 9.363778443732852230e-02, 8.602401356562194845e-01, + 6.699905217924281287e-01, 9.363778443732852230e-02, 8.602401356562194845e-01, 9.305681557970262308e-01, + 8.774288093304678515e-01, 5.710419611451766908e-02, 6.943184420297371373e-02, 8.774288093304678515e-01, + 5.710419611451766908e-02, 3.300094782075718713e-01, 8.774288093304678515e-01, 5.710419611451766908e-02, + 6.699905217924281287e-01, 8.774288093304678515e-01, 5.710419611451766908e-02, 9.305681557970262308e-01, + 6.729468631505063358e-01, 2.768430136381238027e-01, 6.943184420297371373e-02, 6.729468631505063358e-01, + 2.768430136381238027e-01, 3.300094782075718713e-01, 6.729468631505063358e-01, 2.768430136381238027e-01, + 6.699905217924281287e-01, 6.729468631505063358e-01, 2.768430136381238027e-01, 9.305681557970262308e-01, + 3.874974834066941543e-01, 5.835904323689168338e-01, 6.943184420297371373e-02, 3.874974834066941543e-01, + 5.835904323689168338e-01, 3.300094782075718713e-01, 3.874974834066941543e-01, 5.835904323689168338e-01, + 6.699905217924281287e-01, 3.874974834066941543e-01, 5.835904323689168338e-01, 9.305681557970262308e-01, + 1.300560792168344582e-01, 8.602401356562194845e-01, 6.943184420297371373e-02, 1.300560792168344582e-01, + 8.602401356562194845e-01, 3.300094782075718713e-01, 1.300560792168344582e-01, 8.602401356562194845e-01, + 6.699905217924281287e-01, 1.300560792168344582e-01, 8.602401356562194845e-01, 9.305681557970262308e-01 +}; +inline constexpr double prism_o7_weights[64] = { + 4.099185534025746348e-03, 7.684998562665407498e-03, 7.684998562665404896e-03, 4.099185534025746348e-03, + 6.154955439201190322e-03, 1.153907850984177766e-02, 1.153907850984177419e-02, 6.154955439201190322e-03, + 3.927985482847706419e-03, 7.364039158337254941e-03, 7.364039158337252339e-03, 3.927985482847706419e-03, + 9.432477046255943395e-04, 1.768365250637032654e-03, 1.768365250637032003e-03, 9.432477046255943395e-04, + 7.684998562665406631e-03, 1.440754569851544417e-02, 1.440754569851543897e-02, 7.684998562665406631e-03, + 1.153907850984177939e-02, 2.163302954368310355e-02, 2.163302954368309314e-02, 1.153907850984177939e-02, + 7.364039158337254941e-03, 1.380582310253589302e-02, 1.380582310253588782e-02, 7.364039158337254941e-03, + 1.768365250637032654e-03, 3.315264531602363316e-03, 3.315264531602362015e-03, 1.768365250637032654e-03, + 7.684998562665404029e-03, 1.440754569851543897e-02, 1.440754569851543550e-02, 7.684998562665404029e-03, + 1.153907850984177419e-02, 2.163302954368309314e-02, 2.163302954368308620e-02, 1.153907850984177419e-02, + 7.364039158337252339e-03, 1.380582310253588955e-02, 1.380582310253588435e-02, 7.364039158337252339e-03, + 1.768365250637032003e-03, 3.315264531602362015e-03, 3.315264531602361148e-03, 1.768365250637032003e-03, + 4.099185534025746348e-03, 7.684998562665407498e-03, 7.684998562665404896e-03, 4.099185534025746348e-03, + 6.154955439201190322e-03, 1.153907850984177766e-02, 1.153907850984177419e-02, 6.154955439201190322e-03, + 3.927985482847706419e-03, 7.364039158337254941e-03, 7.364039158337252339e-03, 3.927985482847706419e-03, + 9.432477046255943395e-04, 1.768365250637032654e-03, 1.768365250637032003e-03, 9.432477046255943395e-04 +}; + +// ---- order 8: 125 point(s), tdim=3 ---- +inline constexpr int prism_o8_npts = 125; +inline constexpr int prism_o8_tdim = 3; +inline constexpr double prism_o8_points[375] = { + 4.504259356980373774e-02, 3.980985705146872222e-02, 4.691007703066801815e-02, 4.504259356980373774e-02, + 3.980985705146872222e-02, 2.307653449471584461e-01, 4.504259356980373774e-02, 3.980985705146872222e-02, + 5.000000000000000000e-01, 4.504259356980373774e-02, 3.980985705146872222e-02, 7.692346550528414983e-01, + 4.504259356980373774e-02, 3.980985705146872222e-02, 9.530899229693319263e-01, 3.762125234511120436e-02, + 1.980134178736082107e-01, 4.691007703066801815e-02, 3.762125234511120436e-02, 1.980134178736082107e-01, + 2.307653449471584461e-01, 3.762125234511120436e-02, 1.980134178736082107e-01, 5.000000000000000000e-01, + 3.762125234511120436e-02, 1.980134178736082107e-01, 7.692346550528414983e-01, 3.762125234511120436e-02, + 1.980134178736082107e-01, 9.530899229693319263e-01, 2.636464494447092466e-02, 4.379748102473861593e-01, + 4.691007703066801815e-02, 2.636464494447092466e-02, 4.379748102473861593e-01, 2.307653449471584461e-01, + 2.636464494447092466e-02, 4.379748102473861593e-01, 5.000000000000000000e-01, 2.636464494447092466e-02, + 4.379748102473861593e-01, 7.692346550528414983e-01, 2.636464494447092466e-02, 4.379748102473861593e-01, + 9.530899229693319263e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, 4.691007703066801815e-02, + 1.428579439557139084e-02, 6.954642733536360311e-01, 2.307653449471584461e-01, 1.428579439557139084e-02, + 6.954642733536360311e-01, 5.000000000000000000e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, + 7.692346550528414983e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, 9.530899229693319263e-01, + 4.622288465046429755e-03, 9.014649142011735838e-01, 4.691007703066801815e-02, 4.622288465046429755e-03, + 9.014649142011735838e-01, 2.307653449471584461e-01, 4.622288465046429755e-03, 9.014649142011735838e-01, + 5.000000000000000000e-01, 4.622288465046429755e-03, 9.014649142011735838e-01, 7.692346550528414983e-01, + 4.622288465046429755e-03, 9.014649142011735838e-01, 9.530899229693319263e-01, 2.215786095523792076e-01, + 3.980985705146872222e-02, 4.691007703066801815e-02, 2.215786095523792076e-01, 3.980985705146872222e-02, + 2.307653449471584461e-01, 2.215786095523792076e-01, 3.980985705146872222e-02, 5.000000000000000000e-01, + 2.215786095523792076e-01, 3.980985705146872222e-02, 7.692346550528414983e-01, 2.215786095523792076e-01, + 3.980985705146872222e-02, 9.530899229693319263e-01, 1.850707102673894433e-01, 1.980134178736082107e-01, + 4.691007703066801815e-02, 1.850707102673894433e-01, 1.980134178736082107e-01, 2.307653449471584461e-01, + 1.850707102673894433e-01, 1.980134178736082107e-01, 5.000000000000000000e-01, 1.850707102673894433e-01, + 1.980134178736082107e-01, 7.692346550528414983e-01, 1.850707102673894433e-01, 1.980134178736082107e-01, + 9.530899229693319263e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, 4.691007703066801815e-02, + 1.296959367822541065e-01, 4.379748102473861593e-01, 2.307653449471584461e-01, 1.296959367822541065e-01, + 4.379748102473861593e-01, 5.000000000000000000e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, + 7.692346550528414983e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, 9.530899229693319263e-01, + 7.027629200828172662e-02, 6.954642733536360311e-01, 4.691007703066801815e-02, 7.027629200828172662e-02, + 6.954642733536360311e-01, 2.307653449471584461e-01, 7.027629200828172662e-02, 6.954642733536360311e-01, + 5.000000000000000000e-01, 7.027629200828172662e-02, 6.954642733536360311e-01, 7.692346550528414983e-01, + 7.027629200828172662e-02, 6.954642733536360311e-01, 9.530899229693319263e-01, 2.273848306376403255e-02, + 9.014649142011735838e-01, 4.691007703066801815e-02, 2.273848306376403255e-02, 9.014649142011735838e-01, + 2.307653449471584461e-01, 2.273848306376403255e-02, 9.014649142011735838e-01, 5.000000000000000000e-01, + 2.273848306376403255e-02, 9.014649142011735838e-01, 7.692346550528414983e-01, 2.273848306376403255e-02, + 9.014649142011735838e-01, 9.530899229693319263e-01, 4.800950714742656666e-01, 3.980985705146872222e-02, + 4.691007703066801815e-02, 4.800950714742656666e-01, 3.980985705146872222e-02, 2.307653449471584461e-01, + 4.800950714742656666e-01, 3.980985705146872222e-02, 5.000000000000000000e-01, 4.800950714742656666e-01, + 3.980985705146872222e-02, 7.692346550528414983e-01, 4.800950714742656666e-01, 3.980985705146872222e-02, + 9.530899229693319263e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, 4.691007703066801815e-02, + 4.009932910631959224e-01, 1.980134178736082107e-01, 2.307653449471584461e-01, 4.009932910631959224e-01, + 1.980134178736082107e-01, 5.000000000000000000e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, + 7.692346550528414983e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, 9.530899229693319263e-01, + 2.810125948763069204e-01, 4.379748102473861593e-01, 4.691007703066801815e-02, 2.810125948763069204e-01, + 4.379748102473861593e-01, 2.307653449471584461e-01, 2.810125948763069204e-01, 4.379748102473861593e-01, + 5.000000000000000000e-01, 2.810125948763069204e-01, 4.379748102473861593e-01, 7.692346550528414983e-01, + 2.810125948763069204e-01, 4.379748102473861593e-01, 9.530899229693319263e-01, 1.522678633231819567e-01, + 6.954642733536360311e-01, 4.691007703066801815e-02, 1.522678633231819567e-01, 6.954642733536360311e-01, + 2.307653449471584461e-01, 1.522678633231819567e-01, 6.954642733536360311e-01, 5.000000000000000000e-01, + 1.522678633231819567e-01, 6.954642733536360311e-01, 7.692346550528414983e-01, 1.522678633231819567e-01, + 6.954642733536360311e-01, 9.530899229693319263e-01, 4.926754289941320808e-02, 9.014649142011735838e-01, + 4.691007703066801815e-02, 4.926754289941320808e-02, 9.014649142011735838e-01, 2.307653449471584461e-01, + 4.926754289941320808e-02, 9.014649142011735838e-01, 5.000000000000000000e-01, 4.926754289941320808e-02, + 9.014649142011735838e-01, 7.692346550528414983e-01, 4.926754289941320808e-02, 9.014649142011735838e-01, + 9.530899229693319263e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, 4.691007703066801815e-02, + 7.386115333961520424e-01, 3.980985705146872222e-02, 2.307653449471584461e-01, 7.386115333961520424e-01, + 3.980985705146872222e-02, 5.000000000000000000e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, + 7.692346550528414983e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, 9.530899229693319263e-01, + 6.169158718590023183e-01, 1.980134178736082107e-01, 4.691007703066801815e-02, 6.169158718590023183e-01, + 1.980134178736082107e-01, 2.307653449471584461e-01, 6.169158718590023183e-01, 1.980134178736082107e-01, + 5.000000000000000000e-01, 6.169158718590023183e-01, 1.980134178736082107e-01, 7.692346550528414983e-01, + 6.169158718590023183e-01, 1.980134178736082107e-01, 9.530899229693319263e-01, 4.323292529703596787e-01, + 4.379748102473861593e-01, 4.691007703066801815e-02, 4.323292529703596787e-01, 4.379748102473861593e-01, + 2.307653449471584461e-01, 4.323292529703596787e-01, 4.379748102473861593e-01, 5.000000000000000000e-01, + 4.323292529703596787e-01, 4.379748102473861593e-01, 7.692346550528414983e-01, 4.323292529703596787e-01, + 4.379748102473861593e-01, 9.530899229693319263e-01, 2.342594346380821868e-01, 6.954642733536360311e-01, + 4.691007703066801815e-02, 2.342594346380821868e-01, 6.954642733536360311e-01, 2.307653449471584461e-01, + 2.342594346380821868e-01, 6.954642733536360311e-01, 5.000000000000000000e-01, 2.342594346380821868e-01, + 6.954642733536360311e-01, 7.692346550528414983e-01, 2.342594346380821868e-01, 6.954642733536360311e-01, + 9.530899229693319263e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, 4.691007703066801815e-02, + 7.579660273506237667e-02, 9.014649142011735838e-01, 2.307653449471584461e-01, 7.579660273506237667e-02, + 9.014649142011735838e-01, 5.000000000000000000e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, + 7.692346550528414983e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, 9.530899229693319263e-01, + 9.151475493787275539e-01, 3.980985705146872222e-02, 4.691007703066801815e-02, 9.151475493787275539e-01, + 3.980985705146872222e-02, 2.307653449471584461e-01, 9.151475493787275539e-01, 3.980985705146872222e-02, + 5.000000000000000000e-01, 9.151475493787275539e-01, 3.980985705146872222e-02, 7.692346550528414983e-01, + 9.151475493787275539e-01, 3.980985705146872222e-02, 9.530899229693319263e-01, 7.643653297812805780e-01, + 1.980134178736082107e-01, 4.691007703066801815e-02, 7.643653297812805780e-01, 1.980134178736082107e-01, + 2.307653449471584461e-01, 7.643653297812805780e-01, 1.980134178736082107e-01, 5.000000000000000000e-01, + 7.643653297812805780e-01, 1.980134178736082107e-01, 7.692346550528414983e-01, 7.643653297812805780e-01, + 1.980134178736082107e-01, 9.530899229693319263e-01, 5.356605448081428467e-01, 4.379748102473861593e-01, + 4.691007703066801815e-02, 5.356605448081428467e-01, 4.379748102473861593e-01, 2.307653449471584461e-01, + 5.356605448081428467e-01, 4.379748102473861593e-01, 5.000000000000000000e-01, 5.356605448081428467e-01, + 4.379748102473861593e-01, 7.692346550528414983e-01, 5.356605448081428467e-01, 4.379748102473861593e-01, + 9.530899229693319263e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, 4.691007703066801815e-02, + 2.902499322507924862e-01, 6.954642733536360311e-01, 2.307653449471584461e-01, 2.902499322507924862e-01, + 6.954642733536360311e-01, 5.000000000000000000e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, + 7.692346550528414983e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, 9.530899229693319263e-01, + 9.391279733377998207e-02, 9.014649142011735838e-01, 4.691007703066801815e-02, 9.391279733377998207e-02, + 9.014649142011735838e-01, 2.307653449471584461e-01, 9.391279733377998207e-02, 9.014649142011735838e-01, + 5.000000000000000000e-01, 9.391279733377998207e-02, 9.014649142011735838e-01, 7.692346550528414983e-01, + 9.391279733377998207e-02, 9.014649142011735838e-01, 9.530899229693319263e-01 +}; +inline constexpr double prism_o8_weights[125] = { + 1.358192887310869131e-03, 2.743758082925574181e-03, 3.261178411119655240e-03, 2.743758082925574181e-03, + 1.358192887310869131e-03, 2.346059863934899480e-03, 4.739400989975380701e-03, 5.633161424226804104e-03, + 4.739400989975380701e-03, 2.346059863934899480e-03, 2.054334550482672574e-03, 4.150071083850390141e-03, + 4.932695162699575996e-03, 4.150071083850390141e-03, 2.054334550482672574e-03, 1.037206574171043762e-03, + 2.095316466558683026e-03, 2.490453100704378107e-03, 2.095316466558683026e-03, 1.037206574171043762e-03, + 2.209997319040949370e-04, 4.464533766899758996e-04, 5.306459496896958608e-04, 4.464533766899758996e-04, + 2.209997319040949370e-04, 2.743758082925574181e-03, 5.542812429628291536e-03, 6.588080904390647939e-03, + 5.542812429628291536e-03, 2.743758082925574181e-03, 4.739400989975380701e-03, 9.574317385962028554e-03, + 1.137985063428563655e-02, 9.574317385962028554e-03, 4.739400989975380701e-03, 4.150071083850391009e-03, + 8.383780527355944034e-03, 9.964801280959069071e-03, 8.383780527355944034e-03, 4.150071083850391009e-03, + 2.095316466558683026e-03, 4.232860844080959389e-03, 5.031097489204185841e-03, 4.232860844080959389e-03, + 2.095316466558683026e-03, 4.464533766899758996e-04, 9.019043409716836438e-04, 1.071986260004300241e-03, + 9.019043409716836438e-04, 4.464533766899758996e-04, 3.261178411119655240e-03, 6.588080904390647939e-03, + 7.830467033449181602e-03, 6.588080904390647939e-03, 3.261178411119655240e-03, 5.633161424226804104e-03, + 1.137985063428563655e-02, 1.352587294092913797e-02, 1.137985063428563655e-02, 5.633161424226804104e-03, + 4.932695162699575996e-03, 9.964801280959069071e-03, 1.184397232787767415e-02, 9.964801280959069071e-03, + 4.932695162699575996e-03, 2.490453100704377674e-03, 5.031097489204184973e-03, 5.979866307504941997e-03, + 5.031097489204184973e-03, 2.490453100704377674e-03, 5.306459496896957524e-04, 1.071986260004300241e-03, + 1.274142377893362679e-03, 1.071986260004300241e-03, 5.306459496896957524e-04, 2.743758082925574181e-03, + 5.542812429628291536e-03, 6.588080904390647939e-03, 5.542812429628291536e-03, 2.743758082925574181e-03, + 4.739400989975380701e-03, 9.574317385962028554e-03, 1.137985063428563655e-02, 9.574317385962028554e-03, + 4.739400989975380701e-03, 4.150071083850391009e-03, 8.383780527355944034e-03, 9.964801280959069071e-03, + 8.383780527355944034e-03, 4.150071083850391009e-03, 2.095316466558683026e-03, 4.232860844080959389e-03, + 5.031097489204185841e-03, 4.232860844080959389e-03, 2.095316466558683026e-03, 4.464533766899758996e-04, + 9.019043409716836438e-04, 1.071986260004300241e-03, 9.019043409716836438e-04, 4.464533766899758996e-04, + 1.358192887310869131e-03, 2.743758082925574181e-03, 3.261178411119655240e-03, 2.743758082925574181e-03, + 1.358192887310869131e-03, 2.346059863934899480e-03, 4.739400989975380701e-03, 5.633161424226804104e-03, + 4.739400989975380701e-03, 2.346059863934899480e-03, 2.054334550482672574e-03, 4.150071083850390141e-03, + 4.932695162699575996e-03, 4.150071083850390141e-03, 2.054334550482672574e-03, 1.037206574171043762e-03, + 2.095316466558683026e-03, 2.490453100704378107e-03, 2.095316466558683026e-03, 1.037206574171043762e-03, + 2.209997319040949370e-04, 4.464533766899758996e-04, 5.306459496896958608e-04, 4.464533766899758996e-04, + 2.209997319040949370e-04 +}; + +// ---- order 9: 125 point(s), tdim=3 ---- +inline constexpr int prism_o9_npts = 125; +inline constexpr int prism_o9_tdim = 3; +inline constexpr double prism_o9_points[375] = { + 4.504259356980373774e-02, 3.980985705146872222e-02, 4.691007703066801815e-02, 4.504259356980373774e-02, + 3.980985705146872222e-02, 2.307653449471584461e-01, 4.504259356980373774e-02, 3.980985705146872222e-02, + 5.000000000000000000e-01, 4.504259356980373774e-02, 3.980985705146872222e-02, 7.692346550528414983e-01, + 4.504259356980373774e-02, 3.980985705146872222e-02, 9.530899229693319263e-01, 3.762125234511120436e-02, + 1.980134178736082107e-01, 4.691007703066801815e-02, 3.762125234511120436e-02, 1.980134178736082107e-01, + 2.307653449471584461e-01, 3.762125234511120436e-02, 1.980134178736082107e-01, 5.000000000000000000e-01, + 3.762125234511120436e-02, 1.980134178736082107e-01, 7.692346550528414983e-01, 3.762125234511120436e-02, + 1.980134178736082107e-01, 9.530899229693319263e-01, 2.636464494447092466e-02, 4.379748102473861593e-01, + 4.691007703066801815e-02, 2.636464494447092466e-02, 4.379748102473861593e-01, 2.307653449471584461e-01, + 2.636464494447092466e-02, 4.379748102473861593e-01, 5.000000000000000000e-01, 2.636464494447092466e-02, + 4.379748102473861593e-01, 7.692346550528414983e-01, 2.636464494447092466e-02, 4.379748102473861593e-01, + 9.530899229693319263e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, 4.691007703066801815e-02, + 1.428579439557139084e-02, 6.954642733536360311e-01, 2.307653449471584461e-01, 1.428579439557139084e-02, + 6.954642733536360311e-01, 5.000000000000000000e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, + 7.692346550528414983e-01, 1.428579439557139084e-02, 6.954642733536360311e-01, 9.530899229693319263e-01, + 4.622288465046429755e-03, 9.014649142011735838e-01, 4.691007703066801815e-02, 4.622288465046429755e-03, + 9.014649142011735838e-01, 2.307653449471584461e-01, 4.622288465046429755e-03, 9.014649142011735838e-01, + 5.000000000000000000e-01, 4.622288465046429755e-03, 9.014649142011735838e-01, 7.692346550528414983e-01, + 4.622288465046429755e-03, 9.014649142011735838e-01, 9.530899229693319263e-01, 2.215786095523792076e-01, + 3.980985705146872222e-02, 4.691007703066801815e-02, 2.215786095523792076e-01, 3.980985705146872222e-02, + 2.307653449471584461e-01, 2.215786095523792076e-01, 3.980985705146872222e-02, 5.000000000000000000e-01, + 2.215786095523792076e-01, 3.980985705146872222e-02, 7.692346550528414983e-01, 2.215786095523792076e-01, + 3.980985705146872222e-02, 9.530899229693319263e-01, 1.850707102673894433e-01, 1.980134178736082107e-01, + 4.691007703066801815e-02, 1.850707102673894433e-01, 1.980134178736082107e-01, 2.307653449471584461e-01, + 1.850707102673894433e-01, 1.980134178736082107e-01, 5.000000000000000000e-01, 1.850707102673894433e-01, + 1.980134178736082107e-01, 7.692346550528414983e-01, 1.850707102673894433e-01, 1.980134178736082107e-01, + 9.530899229693319263e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, 4.691007703066801815e-02, + 1.296959367822541065e-01, 4.379748102473861593e-01, 2.307653449471584461e-01, 1.296959367822541065e-01, + 4.379748102473861593e-01, 5.000000000000000000e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, + 7.692346550528414983e-01, 1.296959367822541065e-01, 4.379748102473861593e-01, 9.530899229693319263e-01, + 7.027629200828172662e-02, 6.954642733536360311e-01, 4.691007703066801815e-02, 7.027629200828172662e-02, + 6.954642733536360311e-01, 2.307653449471584461e-01, 7.027629200828172662e-02, 6.954642733536360311e-01, + 5.000000000000000000e-01, 7.027629200828172662e-02, 6.954642733536360311e-01, 7.692346550528414983e-01, + 7.027629200828172662e-02, 6.954642733536360311e-01, 9.530899229693319263e-01, 2.273848306376403255e-02, + 9.014649142011735838e-01, 4.691007703066801815e-02, 2.273848306376403255e-02, 9.014649142011735838e-01, + 2.307653449471584461e-01, 2.273848306376403255e-02, 9.014649142011735838e-01, 5.000000000000000000e-01, + 2.273848306376403255e-02, 9.014649142011735838e-01, 7.692346550528414983e-01, 2.273848306376403255e-02, + 9.014649142011735838e-01, 9.530899229693319263e-01, 4.800950714742656666e-01, 3.980985705146872222e-02, + 4.691007703066801815e-02, 4.800950714742656666e-01, 3.980985705146872222e-02, 2.307653449471584461e-01, + 4.800950714742656666e-01, 3.980985705146872222e-02, 5.000000000000000000e-01, 4.800950714742656666e-01, + 3.980985705146872222e-02, 7.692346550528414983e-01, 4.800950714742656666e-01, 3.980985705146872222e-02, + 9.530899229693319263e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, 4.691007703066801815e-02, + 4.009932910631959224e-01, 1.980134178736082107e-01, 2.307653449471584461e-01, 4.009932910631959224e-01, + 1.980134178736082107e-01, 5.000000000000000000e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, + 7.692346550528414983e-01, 4.009932910631959224e-01, 1.980134178736082107e-01, 9.530899229693319263e-01, + 2.810125948763069204e-01, 4.379748102473861593e-01, 4.691007703066801815e-02, 2.810125948763069204e-01, + 4.379748102473861593e-01, 2.307653449471584461e-01, 2.810125948763069204e-01, 4.379748102473861593e-01, + 5.000000000000000000e-01, 2.810125948763069204e-01, 4.379748102473861593e-01, 7.692346550528414983e-01, + 2.810125948763069204e-01, 4.379748102473861593e-01, 9.530899229693319263e-01, 1.522678633231819567e-01, + 6.954642733536360311e-01, 4.691007703066801815e-02, 1.522678633231819567e-01, 6.954642733536360311e-01, + 2.307653449471584461e-01, 1.522678633231819567e-01, 6.954642733536360311e-01, 5.000000000000000000e-01, + 1.522678633231819567e-01, 6.954642733536360311e-01, 7.692346550528414983e-01, 1.522678633231819567e-01, + 6.954642733536360311e-01, 9.530899229693319263e-01, 4.926754289941320808e-02, 9.014649142011735838e-01, + 4.691007703066801815e-02, 4.926754289941320808e-02, 9.014649142011735838e-01, 2.307653449471584461e-01, + 4.926754289941320808e-02, 9.014649142011735838e-01, 5.000000000000000000e-01, 4.926754289941320808e-02, + 9.014649142011735838e-01, 7.692346550528414983e-01, 4.926754289941320808e-02, 9.014649142011735838e-01, + 9.530899229693319263e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, 4.691007703066801815e-02, + 7.386115333961520424e-01, 3.980985705146872222e-02, 2.307653449471584461e-01, 7.386115333961520424e-01, + 3.980985705146872222e-02, 5.000000000000000000e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, + 7.692346550528414983e-01, 7.386115333961520424e-01, 3.980985705146872222e-02, 9.530899229693319263e-01, + 6.169158718590023183e-01, 1.980134178736082107e-01, 4.691007703066801815e-02, 6.169158718590023183e-01, + 1.980134178736082107e-01, 2.307653449471584461e-01, 6.169158718590023183e-01, 1.980134178736082107e-01, + 5.000000000000000000e-01, 6.169158718590023183e-01, 1.980134178736082107e-01, 7.692346550528414983e-01, + 6.169158718590023183e-01, 1.980134178736082107e-01, 9.530899229693319263e-01, 4.323292529703596787e-01, + 4.379748102473861593e-01, 4.691007703066801815e-02, 4.323292529703596787e-01, 4.379748102473861593e-01, + 2.307653449471584461e-01, 4.323292529703596787e-01, 4.379748102473861593e-01, 5.000000000000000000e-01, + 4.323292529703596787e-01, 4.379748102473861593e-01, 7.692346550528414983e-01, 4.323292529703596787e-01, + 4.379748102473861593e-01, 9.530899229693319263e-01, 2.342594346380821868e-01, 6.954642733536360311e-01, + 4.691007703066801815e-02, 2.342594346380821868e-01, 6.954642733536360311e-01, 2.307653449471584461e-01, + 2.342594346380821868e-01, 6.954642733536360311e-01, 5.000000000000000000e-01, 2.342594346380821868e-01, + 6.954642733536360311e-01, 7.692346550528414983e-01, 2.342594346380821868e-01, 6.954642733536360311e-01, + 9.530899229693319263e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, 4.691007703066801815e-02, + 7.579660273506237667e-02, 9.014649142011735838e-01, 2.307653449471584461e-01, 7.579660273506237667e-02, + 9.014649142011735838e-01, 5.000000000000000000e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, + 7.692346550528414983e-01, 7.579660273506237667e-02, 9.014649142011735838e-01, 9.530899229693319263e-01, + 9.151475493787275539e-01, 3.980985705146872222e-02, 4.691007703066801815e-02, 9.151475493787275539e-01, + 3.980985705146872222e-02, 2.307653449471584461e-01, 9.151475493787275539e-01, 3.980985705146872222e-02, + 5.000000000000000000e-01, 9.151475493787275539e-01, 3.980985705146872222e-02, 7.692346550528414983e-01, + 9.151475493787275539e-01, 3.980985705146872222e-02, 9.530899229693319263e-01, 7.643653297812805780e-01, + 1.980134178736082107e-01, 4.691007703066801815e-02, 7.643653297812805780e-01, 1.980134178736082107e-01, + 2.307653449471584461e-01, 7.643653297812805780e-01, 1.980134178736082107e-01, 5.000000000000000000e-01, + 7.643653297812805780e-01, 1.980134178736082107e-01, 7.692346550528414983e-01, 7.643653297812805780e-01, + 1.980134178736082107e-01, 9.530899229693319263e-01, 5.356605448081428467e-01, 4.379748102473861593e-01, + 4.691007703066801815e-02, 5.356605448081428467e-01, 4.379748102473861593e-01, 2.307653449471584461e-01, + 5.356605448081428467e-01, 4.379748102473861593e-01, 5.000000000000000000e-01, 5.356605448081428467e-01, + 4.379748102473861593e-01, 7.692346550528414983e-01, 5.356605448081428467e-01, 4.379748102473861593e-01, + 9.530899229693319263e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, 4.691007703066801815e-02, + 2.902499322507924862e-01, 6.954642733536360311e-01, 2.307653449471584461e-01, 2.902499322507924862e-01, + 6.954642733536360311e-01, 5.000000000000000000e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, + 7.692346550528414983e-01, 2.902499322507924862e-01, 6.954642733536360311e-01, 9.530899229693319263e-01, + 9.391279733377998207e-02, 9.014649142011735838e-01, 4.691007703066801815e-02, 9.391279733377998207e-02, + 9.014649142011735838e-01, 2.307653449471584461e-01, 9.391279733377998207e-02, 9.014649142011735838e-01, + 5.000000000000000000e-01, 9.391279733377998207e-02, 9.014649142011735838e-01, 7.692346550528414983e-01, + 9.391279733377998207e-02, 9.014649142011735838e-01, 9.530899229693319263e-01 +}; +inline constexpr double prism_o9_weights[125] = { + 1.358192887310869131e-03, 2.743758082925574181e-03, 3.261178411119655240e-03, 2.743758082925574181e-03, + 1.358192887310869131e-03, 2.346059863934899480e-03, 4.739400989975380701e-03, 5.633161424226804104e-03, + 4.739400989975380701e-03, 2.346059863934899480e-03, 2.054334550482672574e-03, 4.150071083850390141e-03, + 4.932695162699575996e-03, 4.150071083850390141e-03, 2.054334550482672574e-03, 1.037206574171043762e-03, + 2.095316466558683026e-03, 2.490453100704378107e-03, 2.095316466558683026e-03, 1.037206574171043762e-03, + 2.209997319040949370e-04, 4.464533766899758996e-04, 5.306459496896958608e-04, 4.464533766899758996e-04, + 2.209997319040949370e-04, 2.743758082925574181e-03, 5.542812429628291536e-03, 6.588080904390647939e-03, + 5.542812429628291536e-03, 2.743758082925574181e-03, 4.739400989975380701e-03, 9.574317385962028554e-03, + 1.137985063428563655e-02, 9.574317385962028554e-03, 4.739400989975380701e-03, 4.150071083850391009e-03, + 8.383780527355944034e-03, 9.964801280959069071e-03, 8.383780527355944034e-03, 4.150071083850391009e-03, + 2.095316466558683026e-03, 4.232860844080959389e-03, 5.031097489204185841e-03, 4.232860844080959389e-03, + 2.095316466558683026e-03, 4.464533766899758996e-04, 9.019043409716836438e-04, 1.071986260004300241e-03, + 9.019043409716836438e-04, 4.464533766899758996e-04, 3.261178411119655240e-03, 6.588080904390647939e-03, + 7.830467033449181602e-03, 6.588080904390647939e-03, 3.261178411119655240e-03, 5.633161424226804104e-03, + 1.137985063428563655e-02, 1.352587294092913797e-02, 1.137985063428563655e-02, 5.633161424226804104e-03, + 4.932695162699575996e-03, 9.964801280959069071e-03, 1.184397232787767415e-02, 9.964801280959069071e-03, + 4.932695162699575996e-03, 2.490453100704377674e-03, 5.031097489204184973e-03, 5.979866307504941997e-03, + 5.031097489204184973e-03, 2.490453100704377674e-03, 5.306459496896957524e-04, 1.071986260004300241e-03, + 1.274142377893362679e-03, 1.071986260004300241e-03, 5.306459496896957524e-04, 2.743758082925574181e-03, + 5.542812429628291536e-03, 6.588080904390647939e-03, 5.542812429628291536e-03, 2.743758082925574181e-03, + 4.739400989975380701e-03, 9.574317385962028554e-03, 1.137985063428563655e-02, 9.574317385962028554e-03, + 4.739400989975380701e-03, 4.150071083850391009e-03, 8.383780527355944034e-03, 9.964801280959069071e-03, + 8.383780527355944034e-03, 4.150071083850391009e-03, 2.095316466558683026e-03, 4.232860844080959389e-03, + 5.031097489204185841e-03, 4.232860844080959389e-03, 2.095316466558683026e-03, 4.464533766899758996e-04, + 9.019043409716836438e-04, 1.071986260004300241e-03, 9.019043409716836438e-04, 4.464533766899758996e-04, + 1.358192887310869131e-03, 2.743758082925574181e-03, 3.261178411119655240e-03, 2.743758082925574181e-03, + 1.358192887310869131e-03, 2.346059863934899480e-03, 4.739400989975380701e-03, 5.633161424226804104e-03, + 4.739400989975380701e-03, 2.346059863934899480e-03, 2.054334550482672574e-03, 4.150071083850390141e-03, + 4.932695162699575996e-03, 4.150071083850390141e-03, 2.054334550482672574e-03, 1.037206574171043762e-03, + 2.095316466558683026e-03, 2.490453100704378107e-03, 2.095316466558683026e-03, 1.037206574171043762e-03, + 2.209997319040949370e-04, 4.464533766899758996e-04, 5.306459496896958608e-04, 4.464533766899758996e-04, + 2.209997319040949370e-04 +}; + +// ---- order 10: 216 point(s), tdim=3 ---- +inline constexpr int prism_o10_npts = 216; +inline constexpr int prism_o10_tdim = 3; +inline constexpr double prism_o10_points[648] = { + 3.277536661445988597e-02, 2.931642715978488578e-02, 3.376524289842397497e-02, 3.277536661445988597e-02, + 2.931642715978488578e-02, 1.693953067668677037e-01, 3.277536661445988597e-02, 2.931642715978488578e-02, + 3.806904069584015060e-01, 3.277536661445988597e-02, 2.931642715978488578e-02, 6.193095930415984940e-01, + 3.277536661445988597e-02, 2.931642715978488578e-02, 8.306046932331323518e-01, 3.277536661445988597e-02, + 2.931642715978488578e-02, 9.662347571015760250e-01, 2.876533301255911751e-02, 1.480785996684843009e-01, + 3.376524289842397497e-02, 2.876533301255911751e-02, 1.480785996684843009e-01, 1.693953067668677037e-01, + 2.876533301255911751e-02, 1.480785996684843009e-01, 3.806904069584015060e-01, 2.876533301255911751e-02, + 1.480785996684843009e-01, 6.193095930415984940e-01, 2.876533301255911751e-02, 1.480785996684843009e-01, + 8.306046932331323518e-01, 2.876533301255911751e-02, 1.480785996684843009e-01, 9.662347571015760250e-01, + 2.238687297803062734e-02, 3.369846902811542977e-01, 3.376524289842397497e-02, 2.238687297803062734e-02, + 3.369846902811542977e-01, 1.693953067668677037e-01, 2.238687297803062734e-02, 3.369846902811542977e-01, + 3.806904069584015060e-01, 2.238687297803062734e-02, 3.369846902811542977e-01, 6.193095930415984940e-01, + 2.238687297803062734e-02, 3.369846902811542977e-01, 8.306046932331323518e-01, 2.238687297803062734e-02, + 3.369846902811542977e-01, 9.662347571015760250e-01, 1.490156336667115486e-02, 5.586715187715501907e-01, + 3.376524289842397497e-02, 1.490156336667115486e-02, 5.586715187715501907e-01, 1.693953067668677037e-01, + 1.490156336667115486e-02, 5.586715187715501907e-01, 3.806904069584015060e-01, 1.490156336667115486e-02, + 5.586715187715501907e-01, 6.193095930415984940e-01, 1.490156336667115486e-02, 5.586715187715501907e-01, + 8.306046932331323518e-01, 1.490156336667115486e-02, 5.586715187715501907e-01, 9.662347571015760250e-01, + 7.791874701286428950e-03, 7.692338620300545049e-01, 3.376524289842397497e-02, 7.791874701286428950e-03, + 7.692338620300545049e-01, 1.693953067668677037e-01, 7.791874701286428950e-03, 7.692338620300545049e-01, + 3.806904069584015060e-01, 7.791874701286428950e-03, 7.692338620300545049e-01, 6.193095930415984940e-01, + 7.791874701286428950e-03, 7.692338620300545049e-01, 8.306046932331323518e-01, 7.791874701286428950e-03, + 7.692338620300545049e-01, 9.662347571015760250e-01, 2.466697152670243101e-03, 9.269456713197410380e-01, + 3.376524289842397497e-02, 2.466697152670243101e-03, 9.269456713197410380e-01, 1.693953067668677037e-01, + 2.466697152670243101e-03, 9.269456713197410380e-01, 3.806904069584015060e-01, 2.466697152670243101e-03, + 9.269456713197410380e-01, 6.193095930415984940e-01, 2.466697152670243101e-03, 9.269456713197410380e-01, + 8.306046932331323518e-01, 2.466697152670243101e-03, 9.269456713197410380e-01, 9.662347571015760250e-01, + 1.644292415948274133e-01, 2.931642715978488578e-02, 3.376524289842397497e-02, 1.644292415948274133e-01, + 2.931642715978488578e-02, 1.693953067668677037e-01, 1.644292415948274133e-01, 2.931642715978488578e-02, + 3.806904069584015060e-01, 1.644292415948274133e-01, 2.931642715978488578e-02, 6.193095930415984940e-01, + 1.644292415948274133e-01, 2.931642715978488578e-02, 8.306046932331323518e-01, 1.644292415948274133e-01, + 2.931642715978488578e-02, 9.662347571015760250e-01, 1.443114869504165954e-01, 1.480785996684843009e-01, + 3.376524289842397497e-02, 1.443114869504165954e-01, 1.480785996684843009e-01, 1.693953067668677037e-01, + 1.443114869504165954e-01, 1.480785996684843009e-01, 3.806904069584015060e-01, 1.443114869504165954e-01, + 1.480785996684843009e-01, 6.193095930415984940e-01, 1.443114869504165954e-01, 1.480785996684843009e-01, + 8.306046932331323518e-01, 1.443114869504165954e-01, 1.480785996684843009e-01, 9.662347571015760250e-01, + 1.123116817809536733e-01, 3.369846902811542977e-01, 3.376524289842397497e-02, 1.123116817809536733e-01, + 3.369846902811542977e-01, 1.693953067668677037e-01, 1.123116817809536733e-01, 3.369846902811542977e-01, + 3.806904069584015060e-01, 1.123116817809536733e-01, 3.369846902811542977e-01, 6.193095930415984940e-01, + 1.123116817809536733e-01, 3.369846902811542977e-01, 8.306046932331323518e-01, 1.123116817809536733e-01, + 3.369846902811542977e-01, 9.662347571015760250e-01, 7.475897346264907817e-02, 5.586715187715501907e-01, + 3.376524289842397497e-02, 7.475897346264907817e-02, 5.586715187715501907e-01, 1.693953067668677037e-01, + 7.475897346264907817e-02, 5.586715187715501907e-01, 3.806904069584015060e-01, 7.475897346264907817e-02, + 5.586715187715501907e-01, 6.193095930415984940e-01, 7.475897346264907817e-02, 5.586715187715501907e-01, + 8.306046932331323518e-01, 7.475897346264907817e-02, 5.586715187715501907e-01, 9.662347571015760250e-01, + 3.909070073282423091e-02, 7.692338620300545049e-01, 3.376524289842397497e-02, 3.909070073282423091e-02, + 7.692338620300545049e-01, 1.693953067668677037e-01, 3.909070073282423091e-02, 7.692338620300545049e-01, + 3.806904069584015060e-01, 3.909070073282423091e-02, 7.692338620300545049e-01, 6.193095930415984940e-01, + 3.909070073282423091e-02, 7.692338620300545049e-01, 8.306046932331323518e-01, 3.909070073282423091e-02, + 7.692338620300545049e-01, 9.662347571015760250e-01, 1.237506041744003936e-02, 9.269456713197410380e-01, + 3.376524289842397497e-02, 1.237506041744003936e-02, 9.269456713197410380e-01, 1.693953067668677037e-01, + 1.237506041744003936e-02, 9.269456713197410380e-01, 3.806904069584015060e-01, 1.237506041744003936e-02, + 9.269456713197410380e-01, 6.193095930415984940e-01, 1.237506041744003936e-02, 9.269456713197410380e-01, + 8.306046932331323518e-01, 1.237506041744003936e-02, 9.269456713197410380e-01, 9.662347571015760250e-01, + 3.695299243723766391e-01, 2.931642715978488578e-02, 3.376524289842397497e-02, 3.695299243723766391e-01, + 2.931642715978488578e-02, 1.693953067668677037e-01, 3.695299243723766391e-01, 2.931642715978488578e-02, + 3.806904069584015060e-01, 3.695299243723766391e-01, 2.931642715978488578e-02, 6.193095930415984940e-01, + 3.695299243723766391e-01, 2.931642715978488578e-02, 8.306046932331323518e-01, 3.695299243723766391e-01, + 2.931642715978488578e-02, 9.662347571015760250e-01, 3.243183045887759741e-01, 1.480785996684843009e-01, + 3.376524289842397497e-02, 3.243183045887759741e-01, 1.480785996684843009e-01, 1.693953067668677037e-01, + 3.243183045887759741e-01, 1.480785996684843009e-01, 3.806904069584015060e-01, 3.243183045887759741e-01, + 1.480785996684843009e-01, 6.193095930415984940e-01, 3.243183045887759741e-01, 1.480785996684843009e-01, + 8.306046932331323518e-01, 3.243183045887759741e-01, 1.480785996684843009e-01, 9.662347571015760250e-01, + 2.524035680765179812e-01, 3.369846902811542977e-01, 3.376524289842397497e-02, 2.524035680765179812e-01, + 3.369846902811542977e-01, 1.693953067668677037e-01, 2.524035680765179812e-01, 3.369846902811542977e-01, + 3.806904069584015060e-01, 2.524035680765179812e-01, 3.369846902811542977e-01, 6.193095930415984940e-01, + 2.524035680765179812e-01, 3.369846902811542977e-01, 8.306046932331323518e-01, 2.524035680765179812e-01, + 3.369846902811542977e-01, 9.662347571015760250e-01, 1.680095191211918304e-01, 5.586715187715501907e-01, + 3.376524289842397497e-02, 1.680095191211918304e-01, 5.586715187715501907e-01, 1.693953067668677037e-01, + 1.680095191211918304e-01, 5.586715187715501907e-01, 3.806904069584015060e-01, 1.680095191211918304e-01, + 5.586715187715501907e-01, 6.193095930415984940e-01, 1.680095191211918304e-01, 5.586715187715501907e-01, + 8.306046932331323518e-01, 1.680095191211918304e-01, 5.586715187715501907e-01, 9.662347571015760250e-01, + 8.785045497599718034e-02, 7.692338620300545049e-01, 3.376524289842397497e-02, 8.785045497599718034e-02, + 7.692338620300545049e-01, 1.693953067668677037e-01, 8.785045497599718034e-02, 7.692338620300545049e-01, + 3.806904069584015060e-01, 8.785045497599718034e-02, 7.692338620300545049e-01, 6.193095930415984940e-01, + 8.785045497599718034e-02, 7.692338620300545049e-01, 8.306046932331323518e-01, 8.785045497599718034e-02, + 7.692338620300545049e-01, 9.662347571015760250e-01, 2.781108211536058653e-02, 9.269456713197410380e-01, + 3.376524289842397497e-02, 2.781108211536058653e-02, 9.269456713197410380e-01, 1.693953067668677037e-01, + 2.781108211536058653e-02, 9.269456713197410380e-01, 3.806904069584015060e-01, 2.781108211536058653e-02, + 9.269456713197410380e-01, 6.193095930415984940e-01, 2.781108211536058653e-02, 9.269456713197410380e-01, + 8.306046932331323518e-01, 2.781108211536058653e-02, 9.269456713197410380e-01, 9.662347571015760250e-01, + 6.011536484678384751e-01, 2.931642715978488578e-02, 3.376524289842397497e-02, 6.011536484678384751e-01, + 2.931642715978488578e-02, 1.693953067668677037e-01, 6.011536484678384751e-01, 2.931642715978488578e-02, + 3.806904069584015060e-01, 6.011536484678384751e-01, 2.931642715978488578e-02, 6.193095930415984940e-01, + 6.011536484678384751e-01, 2.931642715978488578e-02, 8.306046932331323518e-01, 6.011536484678384751e-01, + 2.931642715978488578e-02, 9.662347571015760250e-01, 5.276030957427396695e-01, 1.480785996684843009e-01, + 3.376524289842397497e-02, 5.276030957427396695e-01, 1.480785996684843009e-01, 1.693953067668677037e-01, + 5.276030957427396695e-01, 1.480785996684843009e-01, 3.806904069584015060e-01, 5.276030957427396695e-01, + 1.480785996684843009e-01, 6.193095930415984940e-01, 5.276030957427396695e-01, 1.480785996684843009e-01, + 8.306046932331323518e-01, 5.276030957427396695e-01, 1.480785996684843009e-01, 9.662347571015760250e-01, + 4.106117416423277211e-01, 3.369846902811542977e-01, 3.376524289842397497e-02, 4.106117416423277211e-01, + 3.369846902811542977e-01, 1.693953067668677037e-01, 4.106117416423277211e-01, 3.369846902811542977e-01, + 3.806904069584015060e-01, 4.106117416423277211e-01, 3.369846902811542977e-01, 6.193095930415984940e-01, + 4.106117416423277211e-01, 3.369846902811542977e-01, 8.306046932331323518e-01, 4.106117416423277211e-01, + 3.369846902811542977e-01, 9.662347571015760250e-01, 2.733189621072580344e-01, 5.586715187715501907e-01, + 3.376524289842397497e-02, 2.733189621072580344e-01, 5.586715187715501907e-01, 1.693953067668677037e-01, + 2.733189621072580344e-01, 5.586715187715501907e-01, 3.806904069584015060e-01, 2.733189621072580344e-01, + 5.586715187715501907e-01, 6.193095930415984940e-01, 2.733189621072580344e-01, 5.586715187715501907e-01, + 8.306046932331323518e-01, 2.733189621072580344e-01, 5.586715187715501907e-01, 9.662347571015760250e-01, + 1.429156829939483009e-01, 7.692338620300545049e-01, 3.376524289842397497e-02, 1.429156829939483009e-01, + 7.692338620300545049e-01, 1.693953067668677037e-01, 1.429156829939483009e-01, 7.692338620300545049e-01, + 3.806904069584015060e-01, 1.429156829939483009e-01, 7.692338620300545049e-01, 6.193095930415984940e-01, + 1.429156829939483009e-01, 7.692338620300545049e-01, 8.306046932331323518e-01, 1.429156829939483009e-01, + 7.692338620300545049e-01, 9.662347571015760250e-01, 4.524324656489832341e-02, 9.269456713197410380e-01, + 3.376524289842397497e-02, 4.524324656489832341e-02, 9.269456713197410380e-01, 1.693953067668677037e-01, + 4.524324656489832341e-02, 9.269456713197410380e-01, 3.806904069584015060e-01, 4.524324656489832341e-02, + 9.269456713197410380e-01, 6.193095930415984940e-01, 4.524324656489832341e-02, 9.269456713197410380e-01, + 8.306046932331323518e-01, 4.524324656489832341e-02, 9.269456713197410380e-01, 9.662347571015760250e-01, + 8.062543312453878119e-01, 2.931642715978488578e-02, 3.376524289842397497e-02, 8.062543312453878119e-01, + 2.931642715978488578e-02, 1.693953067668677037e-01, 8.062543312453878119e-01, 2.931642715978488578e-02, + 3.806904069584015060e-01, 8.062543312453878119e-01, 2.931642715978488578e-02, 6.193095930415984940e-01, + 8.062543312453878119e-01, 2.931642715978488578e-02, 8.306046932331323518e-01, 8.062543312453878119e-01, + 2.931642715978488578e-02, 9.662347571015760250e-01, 7.076099133810991315e-01, 1.480785996684843009e-01, + 3.376524289842397497e-02, 7.076099133810991315e-01, 1.480785996684843009e-01, 1.693953067668677037e-01, + 7.076099133810991315e-01, 1.480785996684843009e-01, 3.806904069584015060e-01, 7.076099133810991315e-01, + 1.480785996684843009e-01, 6.193095930415984940e-01, 7.076099133810991315e-01, 1.480785996684843009e-01, + 8.306046932331323518e-01, 7.076099133810991315e-01, 1.480785996684843009e-01, 9.662347571015760250e-01, + 5.507036279378920707e-01, 3.369846902811542977e-01, 3.376524289842397497e-02, 5.507036279378920707e-01, + 3.369846902811542977e-01, 1.693953067668677037e-01, 5.507036279378920707e-01, 3.369846902811542977e-01, + 3.806904069584015060e-01, 5.507036279378920707e-01, 3.369846902811542977e-01, 6.193095930415984940e-01, + 5.507036279378920707e-01, 3.369846902811542977e-01, 8.306046932331323518e-01, 5.507036279378920707e-01, + 3.369846902811542977e-01, 9.662347571015760250e-01, 3.665695077658008283e-01, 5.586715187715501907e-01, + 3.376524289842397497e-02, 3.665695077658008283e-01, 5.586715187715501907e-01, 1.693953067668677037e-01, + 3.665695077658008283e-01, 5.586715187715501907e-01, 3.806904069584015060e-01, 3.665695077658008283e-01, + 5.586715187715501907e-01, 6.193095930415984940e-01, 3.665695077658008283e-01, 5.586715187715501907e-01, + 8.306046932331323518e-01, 3.665695077658008283e-01, 5.586715187715501907e-01, 9.662347571015760250e-01, + 1.916754372371212711e-01, 7.692338620300545049e-01, 3.376524289842397497e-02, 1.916754372371212711e-01, + 7.692338620300545049e-01, 1.693953067668677037e-01, 1.916754372371212711e-01, 7.692338620300545049e-01, + 3.806904069584015060e-01, 1.916754372371212711e-01, 7.692338620300545049e-01, 6.193095930415984940e-01, + 1.916754372371212711e-01, 7.692338620300545049e-01, 8.306046932331323518e-01, 1.916754372371212711e-01, + 7.692338620300545049e-01, 9.662347571015760250e-01, 6.067926826281887231e-02, 9.269456713197410380e-01, + 3.376524289842397497e-02, 6.067926826281887231e-02, 9.269456713197410380e-01, 1.693953067668677037e-01, + 6.067926826281887231e-02, 9.269456713197410380e-01, 3.806904069584015060e-01, 6.067926826281887231e-02, + 9.269456713197410380e-01, 6.193095930415984940e-01, 6.067926826281887231e-02, 9.269456713197410380e-01, + 8.306046932331323518e-01, 6.067926826281887231e-02, 9.269456713197410380e-01, 9.662347571015760250e-01, + 9.379082062257552144e-01, 2.931642715978488578e-02, 3.376524289842397497e-02, 9.379082062257552144e-01, + 2.931642715978488578e-02, 1.693953067668677037e-01, 9.379082062257552144e-01, 2.931642715978488578e-02, + 3.806904069584015060e-01, 9.379082062257552144e-01, 2.931642715978488578e-02, 6.193095930415984940e-01, + 9.379082062257552144e-01, 2.931642715978488578e-02, 8.306046932331323518e-01, 9.379082062257552144e-01, + 2.931642715978488578e-02, 9.662347571015760250e-01, 8.231560673189565192e-01, 1.480785996684843009e-01, + 3.376524289842397497e-02, 8.231560673189565192e-01, 1.480785996684843009e-01, 1.693953067668677037e-01, + 8.231560673189565192e-01, 1.480785996684843009e-01, 3.806904069584015060e-01, 8.231560673189565192e-01, + 1.480785996684843009e-01, 6.193095930415984940e-01, 8.231560673189565192e-01, 1.480785996684843009e-01, + 8.306046932331323518e-01, 8.231560673189565192e-01, 1.480785996684843009e-01, 9.662347571015760250e-01, + 6.406284367408150437e-01, 3.369846902811542977e-01, 3.376524289842397497e-02, 6.406284367408150437e-01, + 3.369846902811542977e-01, 1.693953067668677037e-01, 6.406284367408150437e-01, 3.369846902811542977e-01, + 3.806904069584015060e-01, 6.406284367408150437e-01, 3.369846902811542977e-01, 6.193095930415984940e-01, + 6.406284367408150437e-01, 3.369846902811542977e-01, 8.306046932331323518e-01, 6.406284367408150437e-01, + 3.369846902811542977e-01, 9.662347571015760250e-01, 4.264269178617787204e-01, 5.586715187715501907e-01, + 3.376524289842397497e-02, 4.264269178617787204e-01, 5.586715187715501907e-01, 1.693953067668677037e-01, + 4.264269178617787204e-01, 5.586715187715501907e-01, 3.806904069584015060e-01, 4.264269178617787204e-01, + 5.586715187715501907e-01, 6.193095930415984940e-01, 4.264269178617787204e-01, 5.586715187715501907e-01, + 8.306046932331323518e-01, 4.264269178617787204e-01, 5.586715187715501907e-01, 9.662347571015760250e-01, + 2.229742632686590731e-01, 7.692338620300545049e-01, 3.376524289842397497e-02, 2.229742632686590731e-01, + 7.692338620300545049e-01, 1.693953067668677037e-01, 2.229742632686590731e-01, 7.692338620300545049e-01, + 3.806904069584015060e-01, 2.229742632686590731e-01, 7.692338620300545049e-01, 6.193095930415984940e-01, + 2.229742632686590731e-01, 7.692338620300545049e-01, 8.306046932331323518e-01, 2.229742632686590731e-01, + 7.692338620300545049e-01, 9.662347571015760250e-01, 7.058763152758866510e-02, 9.269456713197410380e-01, + 3.376524289842397497e-02, 7.058763152758866510e-02, 9.269456713197410380e-01, 1.693953067668677037e-01, + 7.058763152758866510e-02, 9.269456713197410380e-01, 3.806904069584015060e-01, 7.058763152758866510e-02, + 9.269456713197410380e-01, 6.193095930415984940e-01, 7.058763152758866510e-02, 9.269456713197410380e-01, + 8.306046932331323518e-01, 7.058763152758866510e-02, 9.269456713197410380e-01, 9.662347571015760250e-01 +}; +inline constexpr double prism_o10_weights[216] = { + 5.306146836030765966e-04, 1.117326456251390050e-03, 1.449191536474944393e-03, 1.449191536474943526e-03, + 1.117326456251390050e-03, 5.306146836030765966e-04, 9.946136127669785287e-04, 2.094378722703475661e-03, + 2.716445048028285833e-03, 2.716445048028284098e-03, 2.094378722703475661e-03, 9.946136127669785287e-04, + 1.033138634997840619e-03, 2.175501669158562586e-03, 2.821662897976136113e-03, 2.821662897976134378e-03, + 2.175501669158562586e-03, 1.033138634997840619e-03, 7.239775401178304495e-04, 1.524494674388923764e-03, + 1.977295683964796699e-03, 1.977295683964795398e-03, 1.524494674388923764e-03, 7.239775401178304495e-04, + 3.225439024727977779e-04, 6.791874531029950526e-04, 8.809177507700397198e-04, 8.809177507700391777e-04, + 6.791874531029950526e-04, 3.225439024727977779e-04, 6.412183716400783975e-05, 1.350226959425480792e-04, + 1.751267475116022624e-04, 1.751267475116021540e-04, 1.350226959425480792e-04, 6.412183716400783975e-05, + 1.117326456251390050e-03, 2.352777728203922416e-03, 3.051592980586088997e-03, 3.051592980586086828e-03, + 2.352777728203922416e-03, 1.117326456251390050e-03, 2.094378722703475661e-03, 4.410177156041708568e-03, + 5.720075250283713705e-03, 5.720075250283710236e-03, 4.410177156041708568e-03, 2.094378722703475661e-03, + 2.175501669158562586e-03, 4.580999444011290937e-03, 5.941634681353970965e-03, 5.941634681353966628e-03, + 4.580999444011290937e-03, 2.175501669158562586e-03, 1.524494674388923981e-03, 3.210160375778973139e-03, + 4.163632948345241322e-03, 4.163632948345238720e-03, 3.210160375778973139e-03, 1.524494674388923981e-03, + 6.791874531029950526e-04, 1.430179249757905936e-03, 1.854966964037341919e-03, 1.854966964037340835e-03, + 1.430179249757905936e-03, 6.791874531029950526e-04, 1.350226959425480792e-04, 2.843201197271229253e-04, + 3.687680613421229540e-04, 3.687680613421227371e-04, 2.843201197271229253e-04, 1.350226959425480792e-04, + 1.449191536474944393e-03, 3.051592980586088563e-03, 3.957968322945281704e-03, 3.957968322945279102e-03, + 3.051592980586088563e-03, 1.449191536474944393e-03, 2.716445048028286267e-03, 5.720075250283714573e-03, + 7.419035497039986465e-03, 7.419035497039982129e-03, 5.720075250283714573e-03, 2.716445048028286267e-03, + 2.821662897976136113e-03, 5.941634681353970965e-03, 7.706401871063245715e-03, 7.706401871063241378e-03, + 5.941634681353970965e-03, 2.821662897976136113e-03, 1.977295683964796699e-03, 4.163632948345241322e-03, + 5.400303193369083293e-03, 5.400303193369079824e-03, 4.163632948345241322e-03, 1.977295683964796699e-03, + 8.809177507700397198e-04, 1.854966964037341919e-03, 2.405923899575786173e-03, 2.405923899575784872e-03, + 1.854966964037341919e-03, 8.809177507700397198e-04, 1.751267475116022353e-04, 3.687680613421228997e-04, + 4.782984869187042434e-04, 4.782984869187039724e-04, 3.687680613421228997e-04, 1.751267475116022353e-04, + 1.449191536474943526e-03, 3.051592980586086828e-03, 3.957968322945279102e-03, 3.957968322945277367e-03, + 3.051592980586086828e-03, 1.449191536474943526e-03, 2.716445048028284532e-03, 5.720075250283710236e-03, + 7.419035497039981261e-03, 7.419035497039976924e-03, 5.720075250283710236e-03, 2.716445048028284532e-03, + 2.821662897976134378e-03, 5.941634681353966628e-03, 7.706401871063240511e-03, 7.706401871063236174e-03, + 5.941634681353966628e-03, 2.821662897976134378e-03, 1.977295683964795832e-03, 4.163632948345238720e-03, + 5.400303193369079824e-03, 5.400303193369077222e-03, 4.163632948345238720e-03, 1.977295683964795832e-03, + 8.809177507700392861e-04, 1.854966964037341052e-03, 2.405923899575785306e-03, 2.405923899575783571e-03, + 1.854966964037341052e-03, 8.809177507700392861e-04, 1.751267475116021811e-04, 3.687680613421227371e-04, + 4.782984869187040266e-04, 4.782984869187037556e-04, 3.687680613421227371e-04, 1.751267475116021811e-04, + 1.117326456251390050e-03, 2.352777728203922416e-03, 3.051592980586088997e-03, 3.051592980586086828e-03, + 2.352777728203922416e-03, 1.117326456251390050e-03, 2.094378722703475661e-03, 4.410177156041708568e-03, + 5.720075250283713705e-03, 5.720075250283710236e-03, 4.410177156041708568e-03, 2.094378722703475661e-03, + 2.175501669158562586e-03, 4.580999444011290937e-03, 5.941634681353970965e-03, 5.941634681353966628e-03, + 4.580999444011290937e-03, 2.175501669158562586e-03, 1.524494674388923981e-03, 3.210160375778973139e-03, + 4.163632948345241322e-03, 4.163632948345238720e-03, 3.210160375778973139e-03, 1.524494674388923981e-03, + 6.791874531029950526e-04, 1.430179249757905936e-03, 1.854966964037341919e-03, 1.854966964037340835e-03, + 1.430179249757905936e-03, 6.791874531029950526e-04, 1.350226959425480792e-04, 2.843201197271229253e-04, + 3.687680613421229540e-04, 3.687680613421227371e-04, 2.843201197271229253e-04, 1.350226959425480792e-04, + 5.306146836030765966e-04, 1.117326456251390050e-03, 1.449191536474944393e-03, 1.449191536474943526e-03, + 1.117326456251390050e-03, 5.306146836030765966e-04, 9.946136127669785287e-04, 2.094378722703475661e-03, + 2.716445048028285833e-03, 2.716445048028284098e-03, 2.094378722703475661e-03, 9.946136127669785287e-04, + 1.033138634997840619e-03, 2.175501669158562586e-03, 2.821662897976136113e-03, 2.821662897976134378e-03, + 2.175501669158562586e-03, 1.033138634997840619e-03, 7.239775401178304495e-04, 1.524494674388923764e-03, + 1.977295683964796699e-03, 1.977295683964795398e-03, 1.524494674388923764e-03, 7.239775401178304495e-04, + 3.225439024727977779e-04, 6.791874531029950526e-04, 8.809177507700397198e-04, 8.809177507700391777e-04, + 6.791874531029950526e-04, 3.225439024727977779e-04, 6.412183716400783975e-05, 1.350226959425480792e-04, + 1.751267475116022624e-04, 1.751267475116021540e-04, 1.350226959425480792e-04, 6.412183716400783975e-05 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_pyramid.h b/cpp/src/generated/quadrature_tables_pyramid.h new file mode 100644 index 0000000..6eae09f --- /dev/null +++ b/cpp/src/generated/quadrature_tables_pyramid.h @@ -0,0 +1,1312 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'pyramid', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 8 point(s), tdim=3 ---- +inline constexpr int pyramid_o1_npts = 8; +inline constexpr int pyramid_o1_tdim = 3; +inline constexpr double pyramid_o1_points[24] = { + 1.666666666666666852e-01, 1.666666666666666852e-01, 2.113248654051871345e-01, 4.465819873852045624e-02, + 4.465819873852045624e-02, 7.886751345948128655e-01, 1.666666666666666852e-01, 6.220084679281462359e-01, + 2.113248654051871345e-01, 4.465819873852045624e-02, 1.666666666666666852e-01, 7.886751345948128655e-01, + 6.220084679281462359e-01, 1.666666666666666852e-01, 2.113248654051871345e-01, 1.666666666666666852e-01, + 4.465819873852045624e-02, 7.886751345948128655e-01, 6.220084679281462359e-01, 6.220084679281462359e-01, + 2.113248654051871345e-01, 1.666666666666666852e-01, 1.666666666666666852e-01, 7.886751345948128655e-01 +}; +inline constexpr double pyramid_o1_weights[8] = { + 7.775105849101827948e-02, 5.582274842315057030e-03, 7.775105849101827948e-02, 5.582274842315057030e-03, + 7.775105849101827948e-02, 5.582274842315057030e-03, 7.775105849101827948e-02, 5.582274842315057030e-03 +}; + +// ---- order 2: 27 point(s), tdim=3 ---- +inline constexpr int pyramid_o2_npts = 27; +inline constexpr int pyramid_o2_tdim = 3; +inline constexpr double pyramid_o2_points[81] = { + 9.999999999999999167e-02, 9.999999999999999167e-02, 1.127016653792582979e-01, 5.635083268962914893e-02, + 5.635083268962914893e-02, 5.000000000000000000e-01, 1.270166537925830792e-02, 1.270166537925830792e-02, + 8.872983346207417021e-01, 9.999999999999999167e-02, 4.436491673103708511e-01, 1.127016653792582979e-01, + 5.635083268962914893e-02, 2.500000000000000000e-01, 5.000000000000000000e-01, 1.270166537925830792e-02, + 5.635083268962914893e-02, 8.872983346207417021e-01, 9.999999999999999167e-02, 7.872983346207417243e-01, + 1.127016653792582979e-01, 5.635083268962914893e-02, 4.436491673103708511e-01, 5.000000000000000000e-01, + 1.270166537925830792e-02, 9.999999999999999167e-02, 8.872983346207417021e-01, 4.436491673103708511e-01, + 9.999999999999999167e-02, 1.127016653792582979e-01, 2.500000000000000000e-01, 5.635083268962914893e-02, + 5.000000000000000000e-01, 5.635083268962914893e-02, 1.270166537925830792e-02, 8.872983346207417021e-01, + 4.436491673103708511e-01, 4.436491673103708511e-01, 1.127016653792582979e-01, 2.500000000000000000e-01, + 2.500000000000000000e-01, 5.000000000000000000e-01, 5.635083268962914893e-02, 5.635083268962914893e-02, + 8.872983346207417021e-01, 4.436491673103708511e-01, 7.872983346207417243e-01, 1.127016653792582979e-01, + 2.500000000000000000e-01, 4.436491673103708511e-01, 5.000000000000000000e-01, 5.635083268962914893e-02, + 9.999999999999999167e-02, 8.872983346207417021e-01, 7.872983346207417243e-01, 9.999999999999999167e-02, + 1.127016653792582979e-01, 4.436491673103708511e-01, 5.635083268962914893e-02, 5.000000000000000000e-01, + 9.999999999999999167e-02, 1.270166537925830792e-02, 8.872983346207417021e-01, 7.872983346207417243e-01, + 4.436491673103708511e-01, 1.127016653792582979e-01, 4.436491673103708511e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 9.999999999999999167e-02, 5.635083268962914893e-02, 8.872983346207417021e-01, + 7.872983346207417243e-01, 7.872983346207417243e-01, 1.127016653792582979e-01, 4.436491673103708511e-01, + 4.436491673103708511e-01, 5.000000000000000000e-01, 9.999999999999999167e-02, 9.999999999999999167e-02, + 8.872983346207417021e-01 +}; +inline constexpr double pyramid_o2_weights[27] = { + 1.687453563573261539e-02, 8.573388203017826004e-03, 2.722407703030327135e-04, 2.699925701717219295e-02, + 1.371742112482852646e-02, 4.355852324848524934e-04, 1.687453563573261539e-02, 8.573388203017826004e-03, + 2.722407703030327135e-04, 2.699925701717219295e-02, 1.371742112482852646e-02, 4.355852324848524934e-04, + 4.319881122747552121e-02, 2.194787379972564997e-02, 6.969363719757642062e-04, 2.699925701717219295e-02, + 1.371742112482852646e-02, 4.355852324848524934e-04, 1.687453563573261539e-02, 8.573388203017826004e-03, + 2.722407703030327135e-04, 2.699925701717219295e-02, 1.371742112482852646e-02, 4.355852324848524934e-04, + 1.687453563573261539e-02, 8.573388203017826004e-03, 2.722407703030327135e-04 +}; + +// ---- order 3: 27 point(s), tdim=3 ---- +inline constexpr int pyramid_o3_npts = 27; +inline constexpr int pyramid_o3_tdim = 3; +inline constexpr double pyramid_o3_points[81] = { + 9.999999999999999167e-02, 9.999999999999999167e-02, 1.127016653792582979e-01, 5.635083268962914893e-02, + 5.635083268962914893e-02, 5.000000000000000000e-01, 1.270166537925830792e-02, 1.270166537925830792e-02, + 8.872983346207417021e-01, 9.999999999999999167e-02, 4.436491673103708511e-01, 1.127016653792582979e-01, + 5.635083268962914893e-02, 2.500000000000000000e-01, 5.000000000000000000e-01, 1.270166537925830792e-02, + 5.635083268962914893e-02, 8.872983346207417021e-01, 9.999999999999999167e-02, 7.872983346207417243e-01, + 1.127016653792582979e-01, 5.635083268962914893e-02, 4.436491673103708511e-01, 5.000000000000000000e-01, + 1.270166537925830792e-02, 9.999999999999999167e-02, 8.872983346207417021e-01, 4.436491673103708511e-01, + 9.999999999999999167e-02, 1.127016653792582979e-01, 2.500000000000000000e-01, 5.635083268962914893e-02, + 5.000000000000000000e-01, 5.635083268962914893e-02, 1.270166537925830792e-02, 8.872983346207417021e-01, + 4.436491673103708511e-01, 4.436491673103708511e-01, 1.127016653792582979e-01, 2.500000000000000000e-01, + 2.500000000000000000e-01, 5.000000000000000000e-01, 5.635083268962914893e-02, 5.635083268962914893e-02, + 8.872983346207417021e-01, 4.436491673103708511e-01, 7.872983346207417243e-01, 1.127016653792582979e-01, + 2.500000000000000000e-01, 4.436491673103708511e-01, 5.000000000000000000e-01, 5.635083268962914893e-02, + 9.999999999999999167e-02, 8.872983346207417021e-01, 7.872983346207417243e-01, 9.999999999999999167e-02, + 1.127016653792582979e-01, 4.436491673103708511e-01, 5.635083268962914893e-02, 5.000000000000000000e-01, + 9.999999999999999167e-02, 1.270166537925830792e-02, 8.872983346207417021e-01, 7.872983346207417243e-01, + 4.436491673103708511e-01, 1.127016653792582979e-01, 4.436491673103708511e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 9.999999999999999167e-02, 5.635083268962914893e-02, 8.872983346207417021e-01, + 7.872983346207417243e-01, 7.872983346207417243e-01, 1.127016653792582979e-01, 4.436491673103708511e-01, + 4.436491673103708511e-01, 5.000000000000000000e-01, 9.999999999999999167e-02, 9.999999999999999167e-02, + 8.872983346207417021e-01 +}; +inline constexpr double pyramid_o3_weights[27] = { + 1.687453563573261539e-02, 8.573388203017826004e-03, 2.722407703030327135e-04, 2.699925701717219295e-02, + 1.371742112482852646e-02, 4.355852324848524934e-04, 1.687453563573261539e-02, 8.573388203017826004e-03, + 2.722407703030327135e-04, 2.699925701717219295e-02, 1.371742112482852646e-02, 4.355852324848524934e-04, + 4.319881122747552121e-02, 2.194787379972564997e-02, 6.969363719757642062e-04, 2.699925701717219295e-02, + 1.371742112482852646e-02, 4.355852324848524934e-04, 1.687453563573261539e-02, 8.573388203017826004e-03, + 2.722407703030327135e-04, 2.699925701717219295e-02, 1.371742112482852646e-02, 4.355852324848524934e-04, + 1.687453563573261539e-02, 8.573388203017826004e-03, 2.722407703030327135e-04 +}; + +// ---- order 4: 64 point(s), tdim=3 ---- +inline constexpr int pyramid_o4_npts = 64; +inline constexpr int pyramid_o4_tdim = 3; +inline constexpr double pyramid_o4_points[192] = { + 6.461106321354768978e-02, 6.461106321354768978e-02, 6.943184420297371373e-02, 4.651867752656093508e-02, + 4.651867752656093508e-02, 3.300094782075718713e-01, 2.291316667641277866e-02, 2.291316667641277866e-02, + 6.699905217924281287e-01, 4.820780989426018751e-03, 4.820780989426018751e-03, 9.305681557970262308e-01, + 6.461106321354768978e-02, 3.070963115311590719e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 2.211032225007380225e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 1.089062557068338488e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 2.291316667641279600e-02, 9.305681557970262308e-01, + 6.461106321354768978e-02, 6.234718442658671034e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 4.488872992916901339e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 2.211032225007380225e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 4.651867752656096977e-02, 9.305681557970262308e-01, + 6.461106321354768978e-02, 8.659570925834785271e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 6.234718442658671034e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 3.070963115311590719e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 6.461106321354774529e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 2.211032225007380225e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 1.089062557068338488e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 2.291316667641279600e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 3.070963115311590719e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 6.461106321354774529e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 4.488872992916901339e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 2.211032225007380225e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 4.651867752656096977e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 6.234718442658671034e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 6.461106321354774529e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 6.234718442658671034e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 3.070963115311590719e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 6.461106321354774529e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 8.659570925834785271e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 6.461106321354774529e-02, 9.305681557970262308e-01 +}; +inline constexpr double pyramid_o4_weights[64] = { + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 1.601374791094413094e-02, 1.556252803580992529e-02, 3.775684142516485663e-03, 8.914849495397851294e-05, + 1.601374791094412400e-02, 1.556252803580992182e-02, 3.775684142516484362e-03, 8.914849495397848583e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 1.601374791094412400e-02, 1.556252803580992182e-02, 3.775684142516484362e-03, 8.914849495397848583e-05, + 1.601374791094412053e-02, 1.556252803580991662e-02, 3.775684142516482628e-03, 8.914849495397845873e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05 +}; + +// ---- order 5: 64 point(s), tdim=3 ---- +inline constexpr int pyramid_o5_npts = 64; +inline constexpr int pyramid_o5_tdim = 3; +inline constexpr double pyramid_o5_points[192] = { + 6.461106321354768978e-02, 6.461106321354768978e-02, 6.943184420297371373e-02, 4.651867752656093508e-02, + 4.651867752656093508e-02, 3.300094782075718713e-01, 2.291316667641277866e-02, 2.291316667641277866e-02, + 6.699905217924281287e-01, 4.820780989426018751e-03, 4.820780989426018751e-03, 9.305681557970262308e-01, + 6.461106321354768978e-02, 3.070963115311590719e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 2.211032225007380225e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 1.089062557068338488e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 2.291316667641279600e-02, 9.305681557970262308e-01, + 6.461106321354768978e-02, 6.234718442658671034e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 4.488872992916901339e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 2.211032225007380225e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 4.651867752656096977e-02, 9.305681557970262308e-01, + 6.461106321354768978e-02, 8.659570925834785271e-01, 6.943184420297371373e-02, 4.651867752656093508e-02, + 6.234718442658671034e-01, 3.300094782075718713e-01, 2.291316667641277866e-02, 3.070963115311590719e-01, + 6.699905217924281287e-01, 4.820780989426018751e-03, 6.461106321354774529e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 2.211032225007380225e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 1.089062557068338488e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 2.291316667641279600e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 3.070963115311590719e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 3.070963115311590719e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 2.211032225007380225e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 1.089062557068338488e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 2.291316667641279600e-02, 6.461106321354774529e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 4.488872992916901339e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 2.211032225007380225e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 4.651867752656096977e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 6.234718442658671034e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 6.234718442658671034e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 4.488872992916901339e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 2.211032225007380225e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 4.651867752656096977e-02, 6.461106321354774529e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 6.461106321354768978e-02, 6.943184420297371373e-02, 6.234718442658671034e-01, + 4.651867752656093508e-02, 3.300094782075718713e-01, 3.070963115311590719e-01, 2.291316667641277866e-02, + 6.699905217924281287e-01, 6.461106321354774529e-02, 4.820780989426018751e-03, 9.305681557970262308e-01, + 8.659570925834785271e-01, 3.070963115311590719e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 2.211032225007380225e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 1.089062557068338488e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 2.291316667641279600e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 6.234718442658671034e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 4.488872992916901339e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 2.211032225007380225e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 4.651867752656096977e-02, 9.305681557970262308e-01, + 8.659570925834785271e-01, 8.659570925834785271e-01, 6.943184420297371373e-02, 6.234718442658671034e-01, + 6.234718442658671034e-01, 3.300094782075718713e-01, 3.070963115311590719e-01, 3.070963115311590719e-01, + 6.699905217924281287e-01, 6.461106321354774529e-02, 6.461106321354774529e-02, 9.305681557970262308e-01 +}; +inline constexpr double pyramid_o5_weights[64] = { + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 1.601374791094413094e-02, 1.556252803580992529e-02, 3.775684142516485663e-03, 8.914849495397851294e-05, + 1.601374791094412400e-02, 1.556252803580992182e-02, 3.775684142516484362e-03, 8.914849495397848583e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 1.601374791094412400e-02, 1.556252803580992182e-02, 3.775684142516484362e-03, 8.914849495397848583e-05, + 1.601374791094412053e-02, 1.556252803580991662e-02, 3.775684142516482628e-03, 8.914849495397845873e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05, + 8.541748348656808335e-03, 8.301067238604197679e-03, 2.013953508494343170e-03, 4.755189189895837415e-05, + 8.541748348656806600e-03, 8.301067238604195944e-03, 2.013953508494342737e-03, 4.755189189895836738e-05, + 4.556176683780436241e-03, 4.427797151032841425e-03, 1.074244714673975188e-03, 2.536422431290088166e-05 +}; + +// ---- order 6: 125 point(s), tdim=3 ---- +inline constexpr int pyramid_o6_npts = 125; +inline constexpr int pyramid_o6_tdim = 3; +inline constexpr double pyramid_o6_points[375] = { + 4.470952170364481115e-02, 4.470952170364481115e-02, 4.691007703066801815e-02, 3.608485692318813554e-02, + 3.608485692318813554e-02, 2.307653449471584461e-01, 2.345503851533400907e-02, 2.345503851533400907e-02, + 5.000000000000000000e-01, 1.082522010747988261e-02, 1.082522010747988261e-02, 7.692346550528414983e-01, + 2.200555327023209599e-03, 2.200555327023209599e-03, 9.530899229693319263e-01, 4.470952170364481115e-02, + 2.199401248396785635e-01, 4.691007703066801815e-02, 3.608485692318813554e-02, 1.775127005185773987e-01, + 2.307653449471584461e-01, 2.345503851533400907e-02, 1.153826724735792231e-01, 5.000000000000000000e-01, + 1.082522010747988261e-02, 5.325264442858104047e-02, 7.692346550528414983e-01, 2.200555327023209599e-03, + 1.082522010747989302e-02, 9.530899229693319263e-01, 4.470952170364481115e-02, 4.765449614846659632e-01, + 4.691007703066801815e-02, 3.608485692318813554e-02, 3.846173275264207492e-01, 2.307653449471584461e-01, + 2.345503851533400907e-02, 2.500000000000000000e-01, 5.000000000000000000e-01, 1.082522010747988261e-02, + 1.153826724735792508e-01, 7.692346550528414983e-01, 2.200555327023209599e-03, 2.345503851533403683e-02, + 9.530899229693319263e-01, 4.470952170364481115e-02, 7.331497981296533073e-01, 4.691007703066801815e-02, + 3.608485692318813554e-02, 5.917219545342640163e-01, 2.307653449471584461e-01, 2.345503851533400907e-02, + 3.846173275264207492e-01, 5.000000000000000000e-01, 1.082522010747988261e-02, 1.775127005185774542e-01, + 7.692346550528414983e-01, 2.200555327023209599e-03, 3.608485692318817717e-02, 9.530899229693319263e-01, + 4.470952170364481115e-02, 9.083804012656870874e-01, 4.691007703066801815e-02, 3.608485692318813554e-02, + 7.331497981296533073e-01, 2.307653449471584461e-01, 2.345503851533400907e-02, 4.765449614846659632e-01, + 5.000000000000000000e-01, 1.082522010747988261e-02, 2.199401248396786190e-01, 7.692346550528414983e-01, + 2.200555327023209599e-03, 4.470952170364485972e-02, 9.530899229693319263e-01, 2.199401248396785635e-01, + 4.470952170364481115e-02, 4.691007703066801815e-02, 1.775127005185773987e-01, 3.608485692318813554e-02, + 2.307653449471584461e-01, 1.153826724735792231e-01, 2.345503851533400907e-02, 5.000000000000000000e-01, + 5.325264442858104047e-02, 1.082522010747988261e-02, 7.692346550528414983e-01, 1.082522010747989302e-02, + 2.200555327023209599e-03, 9.530899229693319263e-01, 2.199401248396785635e-01, 2.199401248396785635e-01, + 4.691007703066801815e-02, 1.775127005185773987e-01, 1.775127005185773987e-01, 2.307653449471584461e-01, + 1.153826724735792231e-01, 1.153826724735792231e-01, 5.000000000000000000e-01, 5.325264442858104047e-02, + 5.325264442858104047e-02, 7.692346550528414983e-01, 1.082522010747989302e-02, 1.082522010747989302e-02, + 9.530899229693319263e-01, 2.199401248396785635e-01, 4.765449614846659632e-01, 4.691007703066801815e-02, + 1.775127005185773987e-01, 3.846173275264207492e-01, 2.307653449471584461e-01, 1.153826724735792231e-01, + 2.500000000000000000e-01, 5.000000000000000000e-01, 5.325264442858104047e-02, 1.153826724735792508e-01, + 7.692346550528414983e-01, 1.082522010747989302e-02, 2.345503851533403683e-02, 9.530899229693319263e-01, + 2.199401248396785635e-01, 7.331497981296533073e-01, 4.691007703066801815e-02, 1.775127005185773987e-01, + 5.917219545342640163e-01, 2.307653449471584461e-01, 1.153826724735792231e-01, 3.846173275264207492e-01, + 5.000000000000000000e-01, 5.325264442858104047e-02, 1.775127005185774542e-01, 7.692346550528414983e-01, + 1.082522010747989302e-02, 3.608485692318817717e-02, 9.530899229693319263e-01, 2.199401248396785635e-01, + 9.083804012656870874e-01, 4.691007703066801815e-02, 1.775127005185773987e-01, 7.331497981296533073e-01, + 2.307653449471584461e-01, 1.153826724735792231e-01, 4.765449614846659632e-01, 5.000000000000000000e-01, + 5.325264442858104047e-02, 2.199401248396786190e-01, 7.692346550528414983e-01, 1.082522010747989302e-02, + 4.470952170364485972e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, 4.470952170364481115e-02, + 4.691007703066801815e-02, 3.846173275264207492e-01, 3.608485692318813554e-02, 2.307653449471584461e-01, + 2.500000000000000000e-01, 2.345503851533400907e-02, 5.000000000000000000e-01, 1.153826724735792508e-01, + 1.082522010747988261e-02, 7.692346550528414983e-01, 2.345503851533403683e-02, 2.200555327023209599e-03, + 9.530899229693319263e-01, 4.765449614846659632e-01, 2.199401248396785635e-01, 4.691007703066801815e-02, + 3.846173275264207492e-01, 1.775127005185773987e-01, 2.307653449471584461e-01, 2.500000000000000000e-01, + 1.153826724735792231e-01, 5.000000000000000000e-01, 1.153826724735792508e-01, 5.325264442858104047e-02, + 7.692346550528414983e-01, 2.345503851533403683e-02, 1.082522010747989302e-02, 9.530899229693319263e-01, + 4.765449614846659632e-01, 4.765449614846659632e-01, 4.691007703066801815e-02, 3.846173275264207492e-01, + 3.846173275264207492e-01, 2.307653449471584461e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 1.153826724735792508e-01, 1.153826724735792508e-01, 7.692346550528414983e-01, + 2.345503851533403683e-02, 2.345503851533403683e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, + 7.331497981296533073e-01, 4.691007703066801815e-02, 3.846173275264207492e-01, 5.917219545342640163e-01, + 2.307653449471584461e-01, 2.500000000000000000e-01, 3.846173275264207492e-01, 5.000000000000000000e-01, + 1.153826724735792508e-01, 1.775127005185774542e-01, 7.692346550528414983e-01, 2.345503851533403683e-02, + 3.608485692318817717e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, 9.083804012656870874e-01, + 4.691007703066801815e-02, 3.846173275264207492e-01, 7.331497981296533073e-01, 2.307653449471584461e-01, + 2.500000000000000000e-01, 4.765449614846659632e-01, 5.000000000000000000e-01, 1.153826724735792508e-01, + 2.199401248396786190e-01, 7.692346550528414983e-01, 2.345503851533403683e-02, 4.470952170364485972e-02, + 9.530899229693319263e-01, 7.331497981296533073e-01, 4.470952170364481115e-02, 4.691007703066801815e-02, + 5.917219545342640163e-01, 3.608485692318813554e-02, 2.307653449471584461e-01, 3.846173275264207492e-01, + 2.345503851533400907e-02, 5.000000000000000000e-01, 1.775127005185774542e-01, 1.082522010747988261e-02, + 7.692346550528414983e-01, 3.608485692318817717e-02, 2.200555327023209599e-03, 9.530899229693319263e-01, + 7.331497981296533073e-01, 2.199401248396785635e-01, 4.691007703066801815e-02, 5.917219545342640163e-01, + 1.775127005185773987e-01, 2.307653449471584461e-01, 3.846173275264207492e-01, 1.153826724735792231e-01, + 5.000000000000000000e-01, 1.775127005185774542e-01, 5.325264442858104047e-02, 7.692346550528414983e-01, + 3.608485692318817717e-02, 1.082522010747989302e-02, 9.530899229693319263e-01, 7.331497981296533073e-01, + 4.765449614846659632e-01, 4.691007703066801815e-02, 5.917219545342640163e-01, 3.846173275264207492e-01, + 2.307653449471584461e-01, 3.846173275264207492e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, + 1.775127005185774542e-01, 1.153826724735792508e-01, 7.692346550528414983e-01, 3.608485692318817717e-02, + 2.345503851533403683e-02, 9.530899229693319263e-01, 7.331497981296533073e-01, 7.331497981296533073e-01, + 4.691007703066801815e-02, 5.917219545342640163e-01, 5.917219545342640163e-01, 2.307653449471584461e-01, + 3.846173275264207492e-01, 3.846173275264207492e-01, 5.000000000000000000e-01, 1.775127005185774542e-01, + 1.775127005185774542e-01, 7.692346550528414983e-01, 3.608485692318817717e-02, 3.608485692318817717e-02, + 9.530899229693319263e-01, 7.331497981296533073e-01, 9.083804012656870874e-01, 4.691007703066801815e-02, + 5.917219545342640163e-01, 7.331497981296533073e-01, 2.307653449471584461e-01, 3.846173275264207492e-01, + 4.765449614846659632e-01, 5.000000000000000000e-01, 1.775127005185774542e-01, 2.199401248396786190e-01, + 7.692346550528414983e-01, 3.608485692318817717e-02, 4.470952170364485972e-02, 9.530899229693319263e-01, + 9.083804012656870874e-01, 4.470952170364481115e-02, 4.691007703066801815e-02, 7.331497981296533073e-01, + 3.608485692318813554e-02, 2.307653449471584461e-01, 4.765449614846659632e-01, 2.345503851533400907e-02, + 5.000000000000000000e-01, 2.199401248396786190e-01, 1.082522010747988261e-02, 7.692346550528414983e-01, + 4.470952170364485972e-02, 2.200555327023209599e-03, 9.530899229693319263e-01, 9.083804012656870874e-01, + 2.199401248396785635e-01, 4.691007703066801815e-02, 7.331497981296533073e-01, 1.775127005185773987e-01, + 2.307653449471584461e-01, 4.765449614846659632e-01, 1.153826724735792231e-01, 5.000000000000000000e-01, + 2.199401248396786190e-01, 5.325264442858104047e-02, 7.692346550528414983e-01, 4.470952170364485972e-02, + 1.082522010747989302e-02, 9.530899229693319263e-01, 9.083804012656870874e-01, 4.765449614846659632e-01, + 4.691007703066801815e-02, 7.331497981296533073e-01, 3.846173275264207492e-01, 2.307653449471584461e-01, + 4.765449614846659632e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, 2.199401248396786190e-01, + 1.153826724735792508e-01, 7.692346550528414983e-01, 4.470952170364485972e-02, 2.345503851533403683e-02, + 9.530899229693319263e-01, 9.083804012656870874e-01, 7.331497981296533073e-01, 4.691007703066801815e-02, + 7.331497981296533073e-01, 5.917219545342640163e-01, 2.307653449471584461e-01, 4.765449614846659632e-01, + 3.846173275264207492e-01, 5.000000000000000000e-01, 2.199401248396786190e-01, 1.775127005185774542e-01, + 7.692346550528414983e-01, 4.470952170364485972e-02, 3.608485692318817717e-02, 9.530899229693319263e-01, + 9.083804012656870874e-01, 9.083804012656870874e-01, 4.691007703066801815e-02, 7.331497981296533073e-01, + 7.331497981296533073e-01, 2.307653449471584461e-01, 4.765449614846659632e-01, 4.765449614846659632e-01, + 5.000000000000000000e-01, 2.199401248396786190e-01, 2.199401248396786190e-01, 7.692346550528414983e-01, + 4.470952170364485972e-02, 4.470952170364485972e-02, 9.530899229693319263e-01 +}; +inline constexpr double pyramid_o6_weights[125] = { + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 6.162962610875639263e-03, 8.110055490713787008e-03, + 4.072629408426510847e-03, 7.298730392435259871e-04, 1.492980273981122173e-05, 7.325179555806502624e-03, + 9.639456934591433129e-03, 4.840649467568900952e-03, 8.675131431052775059e-04, 1.774527815711518676e-05, + 6.162962610875639263e-03, 8.110055490713787008e-03, 4.072629408426510847e-03, 7.298730392435259871e-04, + 1.492980273981122173e-05, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 7.325179555806502624e-03, + 9.639456934591433129e-03, 4.840649467568900952e-03, 8.675131431052775059e-04, 1.774527815711518676e-05, + 8.706568401066729773e-03, 1.145727425666046018e-02, 5.753503429355277983e-03, 1.031109539599387630e-03, + 2.109169842101819183e-05, 7.325179555806502624e-03, 9.639456934591433129e-03, 4.840649467568900952e-03, + 8.675131431052775059e-04, 1.774527815711518676e-05, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 6.162962610875639263e-03, 8.110055490713787008e-03, 4.072629408426510847e-03, 7.298730392435259871e-04, + 1.492980273981122173e-05, 7.325179555806502624e-03, 9.639456934591433129e-03, 4.840649467568900952e-03, + 8.675131431052775059e-04, 1.774527815711518676e-05, 6.162962610875639263e-03, 8.110055490713787008e-03, + 4.072629408426510847e-03, 7.298730392435259871e-04, 1.492980273981122173e-05, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06 +}; + +// ---- order 7: 125 point(s), tdim=3 ---- +inline constexpr int pyramid_o7_npts = 125; +inline constexpr int pyramid_o7_tdim = 3; +inline constexpr double pyramid_o7_points[375] = { + 4.470952170364481115e-02, 4.470952170364481115e-02, 4.691007703066801815e-02, 3.608485692318813554e-02, + 3.608485692318813554e-02, 2.307653449471584461e-01, 2.345503851533400907e-02, 2.345503851533400907e-02, + 5.000000000000000000e-01, 1.082522010747988261e-02, 1.082522010747988261e-02, 7.692346550528414983e-01, + 2.200555327023209599e-03, 2.200555327023209599e-03, 9.530899229693319263e-01, 4.470952170364481115e-02, + 2.199401248396785635e-01, 4.691007703066801815e-02, 3.608485692318813554e-02, 1.775127005185773987e-01, + 2.307653449471584461e-01, 2.345503851533400907e-02, 1.153826724735792231e-01, 5.000000000000000000e-01, + 1.082522010747988261e-02, 5.325264442858104047e-02, 7.692346550528414983e-01, 2.200555327023209599e-03, + 1.082522010747989302e-02, 9.530899229693319263e-01, 4.470952170364481115e-02, 4.765449614846659632e-01, + 4.691007703066801815e-02, 3.608485692318813554e-02, 3.846173275264207492e-01, 2.307653449471584461e-01, + 2.345503851533400907e-02, 2.500000000000000000e-01, 5.000000000000000000e-01, 1.082522010747988261e-02, + 1.153826724735792508e-01, 7.692346550528414983e-01, 2.200555327023209599e-03, 2.345503851533403683e-02, + 9.530899229693319263e-01, 4.470952170364481115e-02, 7.331497981296533073e-01, 4.691007703066801815e-02, + 3.608485692318813554e-02, 5.917219545342640163e-01, 2.307653449471584461e-01, 2.345503851533400907e-02, + 3.846173275264207492e-01, 5.000000000000000000e-01, 1.082522010747988261e-02, 1.775127005185774542e-01, + 7.692346550528414983e-01, 2.200555327023209599e-03, 3.608485692318817717e-02, 9.530899229693319263e-01, + 4.470952170364481115e-02, 9.083804012656870874e-01, 4.691007703066801815e-02, 3.608485692318813554e-02, + 7.331497981296533073e-01, 2.307653449471584461e-01, 2.345503851533400907e-02, 4.765449614846659632e-01, + 5.000000000000000000e-01, 1.082522010747988261e-02, 2.199401248396786190e-01, 7.692346550528414983e-01, + 2.200555327023209599e-03, 4.470952170364485972e-02, 9.530899229693319263e-01, 2.199401248396785635e-01, + 4.470952170364481115e-02, 4.691007703066801815e-02, 1.775127005185773987e-01, 3.608485692318813554e-02, + 2.307653449471584461e-01, 1.153826724735792231e-01, 2.345503851533400907e-02, 5.000000000000000000e-01, + 5.325264442858104047e-02, 1.082522010747988261e-02, 7.692346550528414983e-01, 1.082522010747989302e-02, + 2.200555327023209599e-03, 9.530899229693319263e-01, 2.199401248396785635e-01, 2.199401248396785635e-01, + 4.691007703066801815e-02, 1.775127005185773987e-01, 1.775127005185773987e-01, 2.307653449471584461e-01, + 1.153826724735792231e-01, 1.153826724735792231e-01, 5.000000000000000000e-01, 5.325264442858104047e-02, + 5.325264442858104047e-02, 7.692346550528414983e-01, 1.082522010747989302e-02, 1.082522010747989302e-02, + 9.530899229693319263e-01, 2.199401248396785635e-01, 4.765449614846659632e-01, 4.691007703066801815e-02, + 1.775127005185773987e-01, 3.846173275264207492e-01, 2.307653449471584461e-01, 1.153826724735792231e-01, + 2.500000000000000000e-01, 5.000000000000000000e-01, 5.325264442858104047e-02, 1.153826724735792508e-01, + 7.692346550528414983e-01, 1.082522010747989302e-02, 2.345503851533403683e-02, 9.530899229693319263e-01, + 2.199401248396785635e-01, 7.331497981296533073e-01, 4.691007703066801815e-02, 1.775127005185773987e-01, + 5.917219545342640163e-01, 2.307653449471584461e-01, 1.153826724735792231e-01, 3.846173275264207492e-01, + 5.000000000000000000e-01, 5.325264442858104047e-02, 1.775127005185774542e-01, 7.692346550528414983e-01, + 1.082522010747989302e-02, 3.608485692318817717e-02, 9.530899229693319263e-01, 2.199401248396785635e-01, + 9.083804012656870874e-01, 4.691007703066801815e-02, 1.775127005185773987e-01, 7.331497981296533073e-01, + 2.307653449471584461e-01, 1.153826724735792231e-01, 4.765449614846659632e-01, 5.000000000000000000e-01, + 5.325264442858104047e-02, 2.199401248396786190e-01, 7.692346550528414983e-01, 1.082522010747989302e-02, + 4.470952170364485972e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, 4.470952170364481115e-02, + 4.691007703066801815e-02, 3.846173275264207492e-01, 3.608485692318813554e-02, 2.307653449471584461e-01, + 2.500000000000000000e-01, 2.345503851533400907e-02, 5.000000000000000000e-01, 1.153826724735792508e-01, + 1.082522010747988261e-02, 7.692346550528414983e-01, 2.345503851533403683e-02, 2.200555327023209599e-03, + 9.530899229693319263e-01, 4.765449614846659632e-01, 2.199401248396785635e-01, 4.691007703066801815e-02, + 3.846173275264207492e-01, 1.775127005185773987e-01, 2.307653449471584461e-01, 2.500000000000000000e-01, + 1.153826724735792231e-01, 5.000000000000000000e-01, 1.153826724735792508e-01, 5.325264442858104047e-02, + 7.692346550528414983e-01, 2.345503851533403683e-02, 1.082522010747989302e-02, 9.530899229693319263e-01, + 4.765449614846659632e-01, 4.765449614846659632e-01, 4.691007703066801815e-02, 3.846173275264207492e-01, + 3.846173275264207492e-01, 2.307653449471584461e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 1.153826724735792508e-01, 1.153826724735792508e-01, 7.692346550528414983e-01, + 2.345503851533403683e-02, 2.345503851533403683e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, + 7.331497981296533073e-01, 4.691007703066801815e-02, 3.846173275264207492e-01, 5.917219545342640163e-01, + 2.307653449471584461e-01, 2.500000000000000000e-01, 3.846173275264207492e-01, 5.000000000000000000e-01, + 1.153826724735792508e-01, 1.775127005185774542e-01, 7.692346550528414983e-01, 2.345503851533403683e-02, + 3.608485692318817717e-02, 9.530899229693319263e-01, 4.765449614846659632e-01, 9.083804012656870874e-01, + 4.691007703066801815e-02, 3.846173275264207492e-01, 7.331497981296533073e-01, 2.307653449471584461e-01, + 2.500000000000000000e-01, 4.765449614846659632e-01, 5.000000000000000000e-01, 1.153826724735792508e-01, + 2.199401248396786190e-01, 7.692346550528414983e-01, 2.345503851533403683e-02, 4.470952170364485972e-02, + 9.530899229693319263e-01, 7.331497981296533073e-01, 4.470952170364481115e-02, 4.691007703066801815e-02, + 5.917219545342640163e-01, 3.608485692318813554e-02, 2.307653449471584461e-01, 3.846173275264207492e-01, + 2.345503851533400907e-02, 5.000000000000000000e-01, 1.775127005185774542e-01, 1.082522010747988261e-02, + 7.692346550528414983e-01, 3.608485692318817717e-02, 2.200555327023209599e-03, 9.530899229693319263e-01, + 7.331497981296533073e-01, 2.199401248396785635e-01, 4.691007703066801815e-02, 5.917219545342640163e-01, + 1.775127005185773987e-01, 2.307653449471584461e-01, 3.846173275264207492e-01, 1.153826724735792231e-01, + 5.000000000000000000e-01, 1.775127005185774542e-01, 5.325264442858104047e-02, 7.692346550528414983e-01, + 3.608485692318817717e-02, 1.082522010747989302e-02, 9.530899229693319263e-01, 7.331497981296533073e-01, + 4.765449614846659632e-01, 4.691007703066801815e-02, 5.917219545342640163e-01, 3.846173275264207492e-01, + 2.307653449471584461e-01, 3.846173275264207492e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, + 1.775127005185774542e-01, 1.153826724735792508e-01, 7.692346550528414983e-01, 3.608485692318817717e-02, + 2.345503851533403683e-02, 9.530899229693319263e-01, 7.331497981296533073e-01, 7.331497981296533073e-01, + 4.691007703066801815e-02, 5.917219545342640163e-01, 5.917219545342640163e-01, 2.307653449471584461e-01, + 3.846173275264207492e-01, 3.846173275264207492e-01, 5.000000000000000000e-01, 1.775127005185774542e-01, + 1.775127005185774542e-01, 7.692346550528414983e-01, 3.608485692318817717e-02, 3.608485692318817717e-02, + 9.530899229693319263e-01, 7.331497981296533073e-01, 9.083804012656870874e-01, 4.691007703066801815e-02, + 5.917219545342640163e-01, 7.331497981296533073e-01, 2.307653449471584461e-01, 3.846173275264207492e-01, + 4.765449614846659632e-01, 5.000000000000000000e-01, 1.775127005185774542e-01, 2.199401248396786190e-01, + 7.692346550528414983e-01, 3.608485692318817717e-02, 4.470952170364485972e-02, 9.530899229693319263e-01, + 9.083804012656870874e-01, 4.470952170364481115e-02, 4.691007703066801815e-02, 7.331497981296533073e-01, + 3.608485692318813554e-02, 2.307653449471584461e-01, 4.765449614846659632e-01, 2.345503851533400907e-02, + 5.000000000000000000e-01, 2.199401248396786190e-01, 1.082522010747988261e-02, 7.692346550528414983e-01, + 4.470952170364485972e-02, 2.200555327023209599e-03, 9.530899229693319263e-01, 9.083804012656870874e-01, + 2.199401248396785635e-01, 4.691007703066801815e-02, 7.331497981296533073e-01, 1.775127005185773987e-01, + 2.307653449471584461e-01, 4.765449614846659632e-01, 1.153826724735792231e-01, 5.000000000000000000e-01, + 2.199401248396786190e-01, 5.325264442858104047e-02, 7.692346550528414983e-01, 4.470952170364485972e-02, + 1.082522010747989302e-02, 9.530899229693319263e-01, 9.083804012656870874e-01, 4.765449614846659632e-01, + 4.691007703066801815e-02, 7.331497981296533073e-01, 3.846173275264207492e-01, 2.307653449471584461e-01, + 4.765449614846659632e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, 2.199401248396786190e-01, + 1.153826724735792508e-01, 7.692346550528414983e-01, 4.470952170364485972e-02, 2.345503851533403683e-02, + 9.530899229693319263e-01, 9.083804012656870874e-01, 7.331497981296533073e-01, 4.691007703066801815e-02, + 7.331497981296533073e-01, 5.917219545342640163e-01, 2.307653449471584461e-01, 4.765449614846659632e-01, + 3.846173275264207492e-01, 5.000000000000000000e-01, 2.199401248396786190e-01, 1.775127005185774542e-01, + 7.692346550528414983e-01, 4.470952170364485972e-02, 3.608485692318817717e-02, 9.530899229693319263e-01, + 9.083804012656870874e-01, 9.083804012656870874e-01, 4.691007703066801815e-02, 7.331497981296533073e-01, + 7.331497981296533073e-01, 2.307653449471584461e-01, 4.765449614846659632e-01, 4.765449614846659632e-01, + 5.000000000000000000e-01, 2.199401248396786190e-01, 2.199401248396786190e-01, 7.692346550528414983e-01, + 4.470952170364485972e-02, 4.470952170364485972e-02, 9.530899229693319263e-01 +}; +inline constexpr double pyramid_o7_weights[125] = { + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 6.162962610875639263e-03, 8.110055490713787008e-03, + 4.072629408426510847e-03, 7.298730392435259871e-04, 1.492980273981122173e-05, 7.325179555806502624e-03, + 9.639456934591433129e-03, 4.840649467568900952e-03, 8.675131431052775059e-04, 1.774527815711518676e-05, + 6.162962610875639263e-03, 8.110055490713787008e-03, 4.072629408426510847e-03, 7.298730392435259871e-04, + 1.492980273981122173e-05, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 7.325179555806502624e-03, + 9.639456934591433129e-03, 4.840649467568900952e-03, 8.675131431052775059e-04, 1.774527815711518676e-05, + 8.706568401066729773e-03, 1.145727425666046018e-02, 5.753503429355277983e-03, 1.031109539599387630e-03, + 2.109169842101819183e-05, 7.325179555806502624e-03, 9.639456934591433129e-03, 4.840649467568900952e-03, + 8.675131431052775059e-04, 1.774527815711518676e-05, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 6.162962610875639263e-03, 8.110055490713787008e-03, 4.072629408426510847e-03, 7.298730392435259871e-04, + 1.492980273981122173e-05, 7.325179555806502624e-03, 9.639456934591433129e-03, 4.840649467568900952e-03, + 8.675131431052775059e-04, 1.774527815711518676e-05, 6.162962610875639263e-03, 8.110055490713787008e-03, + 4.072629408426510847e-03, 7.298730392435259871e-04, 1.492980273981122173e-05, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06, 3.050739799162231845e-03, 4.014573934827006182e-03, 2.016000000000000431e-03, + 3.612958360685814756e-04, 7.390429942185235926e-06, 3.626051011160248434e-03, 4.771645841364306057e-03, + 2.396179064667037367e-03, 4.294293246720777405e-04, 8.784123763071767629e-06, 3.050739799162231845e-03, + 4.014573934827006182e-03, 2.016000000000000431e-03, 3.612958360685814756e-04, 7.390429942185235926e-06, + 1.510152488312770276e-03, 1.987261850014038472e-03, 9.979439797765091071e-04, 1.788457363705165510e-04, + 3.658350728553476718e-06 +}; + +// ---- order 8: 216 point(s), tdim=3 ---- +inline constexpr int pyramid_o8_npts = 216; +inline constexpr int pyramid_o8_tdim = 3; +inline constexpr double pyramid_o8_points[648] = { + 3.262515127043440211e-02, 3.262515127043440211e-02, 3.376524289842397497e-02, 2.804556921958764604e-02, + 2.804556921958764604e-02, 1.693953067668677037e-01, 2.091113883837367529e-02, 2.091113883837367529e-02, + 3.806904069584015060e-01, 1.285410406005029968e-02, 1.285410406005029968e-02, 6.193095930415984940e-01, + 5.719673678836328934e-03, 5.719673678836328934e-03, 8.306046932331323518e-01, 1.140091627989570696e-03, + 1.140091627989570696e-03, 9.662347571015760250e-01, 3.262515127043440211e-02, 1.636756330880313748e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 1.407005368122264954e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 1.049081384969455799e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 6.448716826992212381e-02, 6.193095930415984940e-01, 5.719673678836328934e-03, 2.869476995464120481e-02, + 8.306046932331323518e-01, 1.140091627989570696e-03, 5.719673678836330669e-03, 9.662347571015760250e-01, + 3.262515127043440211e-02, 3.678363028983512306e-01, 3.376524289842397497e-02, 2.804556921958764604e-02, + 3.162032386884793822e-01, 1.693953067668677037e-01, 2.091113883837367529e-02, 2.357652210082481492e-01, + 3.806904069584015060e-01, 1.285410406005029968e-02, 1.449251859501533568e-01, 6.193095930415984940e-01, + 5.719673678836328934e-03, 6.448716826992210993e-02, 8.306046932331323518e-01, 1.140091627989570696e-03, + 1.285410406005029968e-02, 9.662347571015760250e-01, 3.262515127043440211e-02, 5.983984542032247944e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 5.144014545446529141e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 3.835443720333503448e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 2.357652210082481492e-01, 6.193095930415984940e-01, 5.719673678836328934e-03, 1.049081384969455383e-01, + 8.306046932331323518e-01, 1.140091627989570696e-03, 2.091113883837367529e-02, 9.662347571015760250e-01, + 3.262515127043440211e-02, 8.025591240135446780e-01, 3.376524289842397497e-02, 2.804556921958764604e-02, + 6.899041564209058564e-01, 1.693953067668677037e-01, 2.091113883837367529e-02, 5.144014545446529141e-01, + 3.806904069584015060e-01, 1.285410406005029968e-02, 3.162032386884793822e-01, 6.193095930415984940e-01, + 5.719673678836328934e-03, 1.407005368122264399e-01, 8.306046932331323518e-01, 1.140091627989570696e-03, + 2.804556921958764604e-02, 9.662347571015760250e-01, 3.262515127043440211e-02, 9.336096058311416090e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 8.025591240135446780e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 5.983984542032247944e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 3.678363028983512306e-01, 6.193095930415984940e-01, 5.719673678836328934e-03, 1.636756330880313193e-01, + 8.306046932331323518e-01, 1.140091627989570696e-03, 3.262515127043440211e-02, 9.662347571015760250e-01, + 1.636756330880313748e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 1.407005368122264954e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 1.049081384969455799e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 6.448716826992212381e-02, 1.285410406005029968e-02, 6.193095930415984940e-01, + 2.869476995464120481e-02, 5.719673678836328934e-03, 8.306046932331323518e-01, 5.719673678836330669e-03, + 1.140091627989570696e-03, 9.662347571015760250e-01, 1.636756330880313748e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 6.448716826992212381e-02, 6.193095930415984940e-01, 2.869476995464120481e-02, 2.869476995464120481e-02, + 8.306046932331323518e-01, 5.719673678836330669e-03, 5.719673678836330669e-03, 9.662347571015760250e-01, + 1.636756330880313748e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 1.407005368122264954e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 1.049081384969455799e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 6.448716826992212381e-02, 1.449251859501533568e-01, 6.193095930415984940e-01, + 2.869476995464120481e-02, 6.448716826992210993e-02, 8.306046932331323518e-01, 5.719673678836330669e-03, + 1.285410406005029968e-02, 9.662347571015760250e-01, 1.636756330880313748e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 2.357652210082481492e-01, 6.193095930415984940e-01, 2.869476995464120481e-02, 1.049081384969455383e-01, + 8.306046932331323518e-01, 5.719673678836330669e-03, 2.091113883837367529e-02, 9.662347571015760250e-01, + 1.636756330880313748e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 1.407005368122264954e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 1.049081384969455799e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 6.448716826992212381e-02, 3.162032386884793822e-01, 6.193095930415984940e-01, + 2.869476995464120481e-02, 1.407005368122264399e-01, 8.306046932331323518e-01, 5.719673678836330669e-03, + 2.804556921958764604e-02, 9.662347571015760250e-01, 1.636756330880313748e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 3.678363028983512306e-01, 6.193095930415984940e-01, 2.869476995464120481e-02, 1.636756330880313193e-01, + 8.306046932331323518e-01, 5.719673678836330669e-03, 3.262515127043440211e-02, 9.662347571015760250e-01, + 3.678363028983512306e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 3.162032386884793822e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 2.357652210082481492e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 1.449251859501533568e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 6.448716826992210993e-02, 5.719673678836328934e-03, 8.306046932331323518e-01, 1.285410406005029968e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 3.678363028983512306e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 6.448716826992210993e-02, 2.869476995464120481e-02, + 8.306046932331323518e-01, 1.285410406005029968e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 3.678363028983512306e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 3.162032386884793822e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 2.357652210082481492e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 1.449251859501533568e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 6.448716826992210993e-02, 6.448716826992210993e-02, 8.306046932331323518e-01, 1.285410406005029968e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 3.678363028983512306e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 6.448716826992210993e-02, 1.049081384969455383e-01, + 8.306046932331323518e-01, 1.285410406005029968e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 3.678363028983512306e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 3.162032386884793822e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 2.357652210082481492e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 1.449251859501533568e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 6.448716826992210993e-02, 1.407005368122264399e-01, 8.306046932331323518e-01, 1.285410406005029968e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 3.678363028983512306e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 6.448716826992210993e-02, 1.636756330880313193e-01, + 8.306046932331323518e-01, 1.285410406005029968e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 5.983984542032247944e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 5.144014545446529141e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 3.835443720333503448e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 2.357652210082481492e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.049081384969455383e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 2.091113883837367529e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 5.983984542032247944e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.049081384969455383e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 2.091113883837367529e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 5.983984542032247944e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 5.144014545446529141e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 3.835443720333503448e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 2.357652210082481492e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.049081384969455383e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 2.091113883837367529e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 5.983984542032247944e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.049081384969455383e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 2.091113883837367529e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 5.983984542032247944e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 5.144014545446529141e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 3.835443720333503448e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 2.357652210082481492e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.049081384969455383e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 2.091113883837367529e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 5.983984542032247944e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.049081384969455383e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 2.091113883837367529e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 8.025591240135446780e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 6.899041564209058564e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 5.144014545446529141e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 3.162032386884793822e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.407005368122264399e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 2.804556921958764604e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 8.025591240135446780e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.407005368122264399e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 2.804556921958764604e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 8.025591240135446780e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 6.899041564209058564e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 5.144014545446529141e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 3.162032386884793822e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.407005368122264399e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 2.804556921958764604e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 8.025591240135446780e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.407005368122264399e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 2.804556921958764604e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 8.025591240135446780e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 6.899041564209058564e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 5.144014545446529141e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 3.162032386884793822e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.407005368122264399e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 2.804556921958764604e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 8.025591240135446780e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.407005368122264399e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 2.804556921958764604e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 9.336096058311416090e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 8.025591240135446780e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 5.983984542032247944e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 3.678363028983512306e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.636756330880313193e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 3.262515127043440211e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 9.336096058311416090e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.636756330880313193e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 3.262515127043440211e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 9.336096058311416090e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 8.025591240135446780e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 5.983984542032247944e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 3.678363028983512306e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.636756330880313193e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 3.262515127043440211e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 9.336096058311416090e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.636756330880313193e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 3.262515127043440211e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 9.336096058311416090e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 8.025591240135446780e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 5.983984542032247944e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 3.678363028983512306e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.636756330880313193e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 3.262515127043440211e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 9.336096058311416090e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.636756330880313193e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 3.262515127043440211e-02, 3.262515127043440211e-02, 9.662347571015760250e-01 +}; +inline constexpr double pyramid_o8_weights[216] = { + 5.868588869827762872e-04, 9.131832855597217307e-04, 6.584616920495327480e-04, 2.488048062220904293e-04, + 3.798148490871391193e-05, 7.166516921863735577e-07, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 5.868588869827762872e-04, 9.131832855597217307e-04, + 6.584616920495327480e-04, 2.488048062220904293e-04, 3.798148490871391193e-05, 7.166516921863735577e-07, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 2.602167941369070651e-03, 4.049110140420053051e-03, + 2.919659127052147678e-03, 1.103215619240795631e-03, 1.684122104773531025e-04, 3.177677121195249137e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 2.602167941369070651e-03, 4.049110140420053051e-03, 2.919659127052147678e-03, 1.103215619240795631e-03, + 1.684122104773531025e-04, 3.177677121195249137e-06, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 3.375056355302152693e-03, 5.251765151465610272e-03, + 3.786847856902281640e-03, 1.430889539369337680e-03, 2.184335193150613861e-04, 4.121501611208709204e-06, + 4.377505855933471086e-03, 6.811629283881954870e-03, 4.911606481200459472e-03, 1.855888221819769132e-03, + 2.833120129777017319e-04, 5.345658128037438073e-06, 4.377505855933468484e-03, 6.811629283881949666e-03, + 4.911606481200456002e-03, 1.855888221819768048e-03, 2.833120129777015150e-04, 5.345658128037433837e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 1.602803236324940386e-03, 2.494046112137892918e-03, + 1.798361675051130069e-03, 6.795247673188430483e-04, 1.037333646681237189e-04, 1.957287649607964959e-06, + 1.602803236324939302e-03, 2.494046112137891184e-03, 1.798361675051128768e-03, 6.795247673188425062e-04, + 1.037333646681236511e-04, 1.957287649607963689e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 4.377505855933468484e-03, 6.811629283881949666e-03, 4.911606481200456002e-03, 1.855888221819768048e-03, + 2.833120129777015150e-04, 5.345658128037433837e-06, 4.377505855933466750e-03, 6.811629283881946197e-03, + 4.911606481200454268e-03, 1.855888221819766964e-03, 2.833120129777014066e-04, 5.345658128037432143e-06, + 3.375056355302150958e-03, 5.251765151465607670e-03, 3.786847856902279905e-03, 1.430889539369337030e-03, + 2.184335193150612777e-04, 4.121501611208707510e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 2.602167941369070651e-03, 4.049110140420053051e-03, + 2.919659127052147678e-03, 1.103215619240795631e-03, 1.684122104773531025e-04, 3.177677121195249137e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 2.602167941369070651e-03, 4.049110140420053051e-03, 2.919659127052147678e-03, 1.103215619240795631e-03, + 1.684122104773531025e-04, 3.177677121195249137e-06, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 5.868588869827762872e-04, 9.131832855597217307e-04, 6.584616920495327480e-04, 2.488048062220904293e-04, + 3.798148490871391193e-05, 7.166516921863735577e-07, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 5.868588869827762872e-04, 9.131832855597217307e-04, + 6.584616920495327480e-04, 2.488048062220904293e-04, 3.798148490871391193e-05, 7.166516921863735577e-07 +}; + +// ---- order 9: 216 point(s), tdim=3 ---- +inline constexpr int pyramid_o9_npts = 216; +inline constexpr int pyramid_o9_tdim = 3; +inline constexpr double pyramid_o9_points[648] = { + 3.262515127043440211e-02, 3.262515127043440211e-02, 3.376524289842397497e-02, 2.804556921958764604e-02, + 2.804556921958764604e-02, 1.693953067668677037e-01, 2.091113883837367529e-02, 2.091113883837367529e-02, + 3.806904069584015060e-01, 1.285410406005029968e-02, 1.285410406005029968e-02, 6.193095930415984940e-01, + 5.719673678836328934e-03, 5.719673678836328934e-03, 8.306046932331323518e-01, 1.140091627989570696e-03, + 1.140091627989570696e-03, 9.662347571015760250e-01, 3.262515127043440211e-02, 1.636756330880313748e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 1.407005368122264954e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 1.049081384969455799e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 6.448716826992212381e-02, 6.193095930415984940e-01, 5.719673678836328934e-03, 2.869476995464120481e-02, + 8.306046932331323518e-01, 1.140091627989570696e-03, 5.719673678836330669e-03, 9.662347571015760250e-01, + 3.262515127043440211e-02, 3.678363028983512306e-01, 3.376524289842397497e-02, 2.804556921958764604e-02, + 3.162032386884793822e-01, 1.693953067668677037e-01, 2.091113883837367529e-02, 2.357652210082481492e-01, + 3.806904069584015060e-01, 1.285410406005029968e-02, 1.449251859501533568e-01, 6.193095930415984940e-01, + 5.719673678836328934e-03, 6.448716826992210993e-02, 8.306046932331323518e-01, 1.140091627989570696e-03, + 1.285410406005029968e-02, 9.662347571015760250e-01, 3.262515127043440211e-02, 5.983984542032247944e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 5.144014545446529141e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 3.835443720333503448e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 2.357652210082481492e-01, 6.193095930415984940e-01, 5.719673678836328934e-03, 1.049081384969455383e-01, + 8.306046932331323518e-01, 1.140091627989570696e-03, 2.091113883837367529e-02, 9.662347571015760250e-01, + 3.262515127043440211e-02, 8.025591240135446780e-01, 3.376524289842397497e-02, 2.804556921958764604e-02, + 6.899041564209058564e-01, 1.693953067668677037e-01, 2.091113883837367529e-02, 5.144014545446529141e-01, + 3.806904069584015060e-01, 1.285410406005029968e-02, 3.162032386884793822e-01, 6.193095930415984940e-01, + 5.719673678836328934e-03, 1.407005368122264399e-01, 8.306046932331323518e-01, 1.140091627989570696e-03, + 2.804556921958764604e-02, 9.662347571015760250e-01, 3.262515127043440211e-02, 9.336096058311416090e-01, + 3.376524289842397497e-02, 2.804556921958764604e-02, 8.025591240135446780e-01, 1.693953067668677037e-01, + 2.091113883837367529e-02, 5.983984542032247944e-01, 3.806904069584015060e-01, 1.285410406005029968e-02, + 3.678363028983512306e-01, 6.193095930415984940e-01, 5.719673678836328934e-03, 1.636756330880313193e-01, + 8.306046932331323518e-01, 1.140091627989570696e-03, 3.262515127043440211e-02, 9.662347571015760250e-01, + 1.636756330880313748e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 1.407005368122264954e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 1.049081384969455799e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 6.448716826992212381e-02, 1.285410406005029968e-02, 6.193095930415984940e-01, + 2.869476995464120481e-02, 5.719673678836328934e-03, 8.306046932331323518e-01, 5.719673678836330669e-03, + 1.140091627989570696e-03, 9.662347571015760250e-01, 1.636756330880313748e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 6.448716826992212381e-02, 6.193095930415984940e-01, 2.869476995464120481e-02, 2.869476995464120481e-02, + 8.306046932331323518e-01, 5.719673678836330669e-03, 5.719673678836330669e-03, 9.662347571015760250e-01, + 1.636756330880313748e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 1.407005368122264954e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 1.049081384969455799e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 6.448716826992212381e-02, 1.449251859501533568e-01, 6.193095930415984940e-01, + 2.869476995464120481e-02, 6.448716826992210993e-02, 8.306046932331323518e-01, 5.719673678836330669e-03, + 1.285410406005029968e-02, 9.662347571015760250e-01, 1.636756330880313748e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 2.357652210082481492e-01, 6.193095930415984940e-01, 2.869476995464120481e-02, 1.049081384969455383e-01, + 8.306046932331323518e-01, 5.719673678836330669e-03, 2.091113883837367529e-02, 9.662347571015760250e-01, + 1.636756330880313748e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 1.407005368122264954e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 1.049081384969455799e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 6.448716826992212381e-02, 3.162032386884793822e-01, 6.193095930415984940e-01, + 2.869476995464120481e-02, 1.407005368122264399e-01, 8.306046932331323518e-01, 5.719673678836330669e-03, + 2.804556921958764604e-02, 9.662347571015760250e-01, 1.636756330880313748e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 1.407005368122264954e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 1.049081384969455799e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 6.448716826992212381e-02, + 3.678363028983512306e-01, 6.193095930415984940e-01, 2.869476995464120481e-02, 1.636756330880313193e-01, + 8.306046932331323518e-01, 5.719673678836330669e-03, 3.262515127043440211e-02, 9.662347571015760250e-01, + 3.678363028983512306e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 3.162032386884793822e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 2.357652210082481492e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 1.449251859501533568e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 6.448716826992210993e-02, 5.719673678836328934e-03, 8.306046932331323518e-01, 1.285410406005029968e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 3.678363028983512306e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 6.448716826992210993e-02, 2.869476995464120481e-02, + 8.306046932331323518e-01, 1.285410406005029968e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 3.678363028983512306e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 3.162032386884793822e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 2.357652210082481492e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 1.449251859501533568e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 6.448716826992210993e-02, 6.448716826992210993e-02, 8.306046932331323518e-01, 1.285410406005029968e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 3.678363028983512306e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 6.448716826992210993e-02, 1.049081384969455383e-01, + 8.306046932331323518e-01, 1.285410406005029968e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 3.678363028983512306e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 3.162032386884793822e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 2.357652210082481492e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 1.449251859501533568e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 6.448716826992210993e-02, 1.407005368122264399e-01, 8.306046932331323518e-01, 1.285410406005029968e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 3.678363028983512306e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 3.162032386884793822e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 2.357652210082481492e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 1.449251859501533568e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 6.448716826992210993e-02, 1.636756330880313193e-01, + 8.306046932331323518e-01, 1.285410406005029968e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 5.983984542032247944e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 5.144014545446529141e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 3.835443720333503448e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 2.357652210082481492e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.049081384969455383e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 2.091113883837367529e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 5.983984542032247944e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.049081384969455383e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 2.091113883837367529e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 5.983984542032247944e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 5.144014545446529141e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 3.835443720333503448e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 2.357652210082481492e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.049081384969455383e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 2.091113883837367529e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 5.983984542032247944e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.049081384969455383e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 2.091113883837367529e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 5.983984542032247944e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 5.144014545446529141e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 3.835443720333503448e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 2.357652210082481492e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.049081384969455383e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 2.091113883837367529e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 5.983984542032247944e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 5.144014545446529141e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 3.835443720333503448e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 2.357652210082481492e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.049081384969455383e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 2.091113883837367529e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 8.025591240135446780e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 6.899041564209058564e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 5.144014545446529141e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 3.162032386884793822e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.407005368122264399e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 2.804556921958764604e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 8.025591240135446780e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.407005368122264399e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 2.804556921958764604e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 8.025591240135446780e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 6.899041564209058564e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 5.144014545446529141e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 3.162032386884793822e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.407005368122264399e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 2.804556921958764604e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 8.025591240135446780e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.407005368122264399e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 2.804556921958764604e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 8.025591240135446780e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 6.899041564209058564e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 5.144014545446529141e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 3.162032386884793822e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.407005368122264399e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 2.804556921958764604e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 8.025591240135446780e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 6.899041564209058564e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 5.144014545446529141e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 3.162032386884793822e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.407005368122264399e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 2.804556921958764604e-02, 3.262515127043440211e-02, 9.662347571015760250e-01, + 9.336096058311416090e-01, 3.262515127043440211e-02, 3.376524289842397497e-02, 8.025591240135446780e-01, + 2.804556921958764604e-02, 1.693953067668677037e-01, 5.983984542032247944e-01, 2.091113883837367529e-02, + 3.806904069584015060e-01, 3.678363028983512306e-01, 1.285410406005029968e-02, 6.193095930415984940e-01, + 1.636756330880313193e-01, 5.719673678836328934e-03, 8.306046932331323518e-01, 3.262515127043440211e-02, + 1.140091627989570696e-03, 9.662347571015760250e-01, 9.336096058311416090e-01, 1.636756330880313748e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 1.407005368122264954e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 1.049081384969455799e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 6.448716826992212381e-02, 6.193095930415984940e-01, 1.636756330880313193e-01, 2.869476995464120481e-02, + 8.306046932331323518e-01, 3.262515127043440211e-02, 5.719673678836330669e-03, 9.662347571015760250e-01, + 9.336096058311416090e-01, 3.678363028983512306e-01, 3.376524289842397497e-02, 8.025591240135446780e-01, + 3.162032386884793822e-01, 1.693953067668677037e-01, 5.983984542032247944e-01, 2.357652210082481492e-01, + 3.806904069584015060e-01, 3.678363028983512306e-01, 1.449251859501533568e-01, 6.193095930415984940e-01, + 1.636756330880313193e-01, 6.448716826992210993e-02, 8.306046932331323518e-01, 3.262515127043440211e-02, + 1.285410406005029968e-02, 9.662347571015760250e-01, 9.336096058311416090e-01, 5.983984542032247944e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 5.144014545446529141e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 3.835443720333503448e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 2.357652210082481492e-01, 6.193095930415984940e-01, 1.636756330880313193e-01, 1.049081384969455383e-01, + 8.306046932331323518e-01, 3.262515127043440211e-02, 2.091113883837367529e-02, 9.662347571015760250e-01, + 9.336096058311416090e-01, 8.025591240135446780e-01, 3.376524289842397497e-02, 8.025591240135446780e-01, + 6.899041564209058564e-01, 1.693953067668677037e-01, 5.983984542032247944e-01, 5.144014545446529141e-01, + 3.806904069584015060e-01, 3.678363028983512306e-01, 3.162032386884793822e-01, 6.193095930415984940e-01, + 1.636756330880313193e-01, 1.407005368122264399e-01, 8.306046932331323518e-01, 3.262515127043440211e-02, + 2.804556921958764604e-02, 9.662347571015760250e-01, 9.336096058311416090e-01, 9.336096058311416090e-01, + 3.376524289842397497e-02, 8.025591240135446780e-01, 8.025591240135446780e-01, 1.693953067668677037e-01, + 5.983984542032247944e-01, 5.983984542032247944e-01, 3.806904069584015060e-01, 3.678363028983512306e-01, + 3.678363028983512306e-01, 6.193095930415984940e-01, 1.636756330880313193e-01, 1.636756330880313193e-01, + 8.306046932331323518e-01, 3.262515127043440211e-02, 3.262515127043440211e-02, 9.662347571015760250e-01 +}; +inline constexpr double pyramid_o9_weights[216] = { + 5.868588869827762872e-04, 9.131832855597217307e-04, 6.584616920495327480e-04, 2.488048062220904293e-04, + 3.798148490871391193e-05, 7.166516921863735577e-07, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 5.868588869827762872e-04, 9.131832855597217307e-04, + 6.584616920495327480e-04, 2.488048062220904293e-04, 3.798148490871391193e-05, 7.166516921863735577e-07, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 2.602167941369070651e-03, 4.049110140420053051e-03, + 2.919659127052147678e-03, 1.103215619240795631e-03, 1.684122104773531025e-04, 3.177677121195249137e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 2.602167941369070651e-03, 4.049110140420053051e-03, 2.919659127052147678e-03, 1.103215619240795631e-03, + 1.684122104773531025e-04, 3.177677121195249137e-06, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 3.375056355302152693e-03, 5.251765151465610272e-03, + 3.786847856902281640e-03, 1.430889539369337680e-03, 2.184335193150613861e-04, 4.121501611208709204e-06, + 4.377505855933471086e-03, 6.811629283881954870e-03, 4.911606481200459472e-03, 1.855888221819769132e-03, + 2.833120129777017319e-04, 5.345658128037438073e-06, 4.377505855933468484e-03, 6.811629283881949666e-03, + 4.911606481200456002e-03, 1.855888221819768048e-03, 2.833120129777015150e-04, 5.345658128037433837e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 1.602803236324940386e-03, 2.494046112137892918e-03, + 1.798361675051130069e-03, 6.795247673188430483e-04, 1.037333646681237189e-04, 1.957287649607964959e-06, + 1.602803236324939302e-03, 2.494046112137891184e-03, 1.798361675051128768e-03, 6.795247673188425062e-04, + 1.037333646681236511e-04, 1.957287649607963689e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 4.377505855933468484e-03, 6.811629283881949666e-03, 4.911606481200456002e-03, 1.855888221819768048e-03, + 2.833120129777015150e-04, 5.345658128037433837e-06, 4.377505855933466750e-03, 6.811629283881946197e-03, + 4.911606481200454268e-03, 1.855888221819766964e-03, 2.833120129777014066e-04, 5.345658128037432143e-06, + 3.375056355302150958e-03, 5.251765151465607670e-03, 3.786847856902279905e-03, 1.430889539369337030e-03, + 2.184335193150612777e-04, 4.121501611208707510e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 2.602167941369070651e-03, 4.049110140420053051e-03, + 2.919659127052147678e-03, 1.103215619240795631e-03, 1.684122104773531025e-04, 3.177677121195249137e-06, + 3.375056355302152693e-03, 5.251765151465610272e-03, 3.786847856902281640e-03, 1.430889539369337680e-03, + 2.184335193150613861e-04, 4.121501611208709204e-06, 3.375056355302150958e-03, 5.251765151465607670e-03, + 3.786847856902279905e-03, 1.430889539369337030e-03, 2.184335193150612777e-04, 4.121501611208707510e-06, + 2.602167941369070651e-03, 4.049110140420053051e-03, 2.919659127052147678e-03, 1.103215619240795631e-03, + 1.684122104773531025e-04, 3.177677121195249137e-06, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 5.868588869827762872e-04, 9.131832855597217307e-04, 6.584616920495327480e-04, 2.488048062220904293e-04, + 3.798148490871391193e-05, 7.166516921863735577e-07, 1.235761053688825021e-03, 1.922909176644068937e-03, + 1.386536580479079579e-03, 5.239134932089359147e-04, 7.997840852810675523e-05, 1.509068482914708443e-06, + 1.602803236324940386e-03, 2.494046112137892918e-03, 1.798361675051130069e-03, 6.795247673188430483e-04, + 1.037333646681237189e-04, 1.957287649607964959e-06, 1.602803236324939302e-03, 2.494046112137891184e-03, + 1.798361675051128768e-03, 6.795247673188425062e-04, 1.037333646681236511e-04, 1.957287649607963689e-06, + 1.235761053688825021e-03, 1.922909176644068937e-03, 1.386536580479079579e-03, 5.239134932089359147e-04, + 7.997840852810675523e-05, 1.509068482914708443e-06, 5.868588869827762872e-04, 9.131832855597217307e-04, + 6.584616920495327480e-04, 2.488048062220904293e-04, 3.798148490871391193e-05, 7.166516921863735577e-07 +}; + +// ---- order 10: 343 point(s), tdim=3 ---- +inline constexpr int pyramid_o10_npts = 343; +inline constexpr int pyramid_o10_tdim = 3; +inline constexpr double pyramid_o10_points[1029] = { + 2.479854268209261484e-02, 2.479854268209261484e-02, 2.544604382862070135e-02, 2.215753943883598184e-02, + 2.215753943883598184e-02, 1.292344072003027700e-01, 1.788659866910157631e-02, 1.788659866910157631e-02, + 2.970774243113014079e-01, 1.272302191431035068e-02, 1.272302191431035068e-02, 5.000000000000000000e-01, + 7.559445159519125913e-03, 7.559445159519125913e-03, 7.029225756886985366e-01, 3.288504389784719078e-03, + 3.288504389784719078e-03, 8.707655927996972300e-01, 6.475011465280884702e-04, 6.475011465280884702e-04, + 9.745539561713791876e-01, 2.479854268209261484e-02, 1.259459028105180400e-01, 2.544604382862070135e-02, + 2.215753943883598184e-02, 1.125328751958891010e-01, 1.292344072003027700e-01, 1.788659866910157631e-02, + 9.084178237683891710e-02, 2.970774243113014079e-01, 1.272302191431035068e-02, 6.461720360015138498e-02, + 5.000000000000000000e-01, 7.559445159519125913e-03, 3.839262482346385980e-02, 7.029225756886985366e-01, + 3.288504389784719078e-03, 1.670153200441366892e-02, 8.707655927996972300e-01, 6.475011465280884702e-04, + 3.288504389784733389e-03, 9.745539561713791876e-01, 2.479854268209261484e-02, 2.895179791517822898e-01, + 2.544604382862070135e-02, 2.215753943883598184e-02, 2.586847994878375689e-01, 1.292344072003027700e-01, + 1.788659866910157631e-02, 2.088224282758643635e-01, 2.970774243113014079e-01, 1.272302191431035068e-02, + 1.485387121556507040e-01, 5.000000000000000000e-01, 7.559445159519125913e-03, 8.825499603543703053e-02, + 7.029225756886985366e-01, 3.288504389784719078e-03, 3.839262482346385286e-02, 8.707655927996972300e-01, + 6.475011465280884702e-04, 7.559445159519158006e-03, 9.745539561713791876e-01, 2.479854268209261484e-02, + 4.872769780856896493e-01, 2.544604382862070135e-02, 2.215753943883598184e-02, 4.353827963998486150e-01, + 1.292344072003027700e-01, 1.788659866910157631e-02, 3.514612878443492683e-01, 2.970774243113014079e-01, + 1.272302191431035068e-02, 2.500000000000000000e-01, 5.000000000000000000e-01, 7.559445159519125913e-03, + 1.485387121556507317e-01, 7.029225756886985366e-01, 3.288504389784719078e-03, 6.461720360015138498e-02, + 8.707655927996972300e-01, 6.475011465280884702e-04, 1.272302191431040619e-02, 9.745539561713791876e-01, + 2.479854268209261484e-02, 6.850359770195969533e-01, 2.544604382862070135e-02, 2.215753943883598184e-02, + 6.120807933118596056e-01, 1.292344072003027700e-01, 1.788659866910157631e-02, 4.941001474128341453e-01, + 2.970774243113014079e-01, 1.272302191431035068e-02, 3.514612878443492683e-01, 5.000000000000000000e-01, + 7.559445159519125913e-03, 2.088224282758644190e-01, 7.029225756886985366e-01, 3.288504389784719078e-03, + 9.084178237683891710e-02, 8.707655927996972300e-01, 6.475011465280884702e-04, 1.788659866910165264e-02, + 9.745539561713791876e-01, 2.479854268209261484e-02, 8.486080533608612031e-01, 2.544604382862070135e-02, + 2.215753943883598184e-02, 7.582327176038081706e-01, 1.292344072003027700e-01, 1.788659866910157631e-02, + 6.120807933118596056e-01, 2.970774243113014079e-01, 1.272302191431035068e-02, 4.353827963998486150e-01, + 5.000000000000000000e-01, 7.559445159519125913e-03, 2.586847994878376245e-01, 7.029225756886985366e-01, + 3.288504389784719078e-03, 1.125328751958891010e-01, 8.707655927996972300e-01, 6.475011465280884702e-04, + 2.215753943883607899e-02, 9.745539561713791876e-01, 2.479854268209261484e-02, 9.497554134892866040e-01, + 2.544604382862070135e-02, 2.215753943883598184e-02, 8.486080533608612031e-01, 1.292344072003027700e-01, + 1.788659866910157631e-02, 6.850359770195968423e-01, 2.970774243113014079e-01, 1.272302191431035068e-02, + 4.872769780856895938e-01, 5.000000000000000000e-01, 7.559445159519125913e-03, 2.895179791517822898e-01, + 7.029225756886985366e-01, 3.288504389784719078e-03, 1.259459028105180400e-01, 8.707655927996972300e-01, + 6.475011465280884702e-04, 2.479854268209272239e-02, 9.745539561713791876e-01, 1.259459028105180400e-01, + 2.479854268209261484e-02, 2.544604382862070135e-02, 1.125328751958891010e-01, 2.215753943883598184e-02, + 1.292344072003027700e-01, 9.084178237683891710e-02, 1.788659866910157631e-02, 2.970774243113014079e-01, + 6.461720360015138498e-02, 1.272302191431035068e-02, 5.000000000000000000e-01, 3.839262482346385980e-02, + 7.559445159519125913e-03, 7.029225756886985366e-01, 1.670153200441366892e-02, 3.288504389784719078e-03, + 8.707655927996972300e-01, 3.288504389784733389e-03, 6.475011465280884702e-04, 9.745539561713791876e-01, + 1.259459028105180400e-01, 1.259459028105180400e-01, 2.544604382862070135e-02, 1.125328751958891010e-01, + 1.125328751958891010e-01, 1.292344072003027700e-01, 9.084178237683891710e-02, 9.084178237683891710e-02, + 2.970774243113014079e-01, 6.461720360015138498e-02, 6.461720360015138498e-02, 5.000000000000000000e-01, + 3.839262482346385980e-02, 3.839262482346385980e-02, 7.029225756886985366e-01, 1.670153200441366892e-02, + 1.670153200441366892e-02, 8.707655927996972300e-01, 3.288504389784733389e-03, 3.288504389784733389e-03, + 9.745539561713791876e-01, 1.259459028105180400e-01, 2.895179791517822898e-01, 2.544604382862070135e-02, + 1.125328751958891010e-01, 2.586847994878375689e-01, 1.292344072003027700e-01, 9.084178237683891710e-02, + 2.088224282758643635e-01, 2.970774243113014079e-01, 6.461720360015138498e-02, 1.485387121556507040e-01, + 5.000000000000000000e-01, 3.839262482346385980e-02, 8.825499603543703053e-02, 7.029225756886985366e-01, + 1.670153200441366892e-02, 3.839262482346385286e-02, 8.707655927996972300e-01, 3.288504389784733389e-03, + 7.559445159519158006e-03, 9.745539561713791876e-01, 1.259459028105180400e-01, 4.872769780856896493e-01, + 2.544604382862070135e-02, 1.125328751958891010e-01, 4.353827963998486150e-01, 1.292344072003027700e-01, + 9.084178237683891710e-02, 3.514612878443492683e-01, 2.970774243113014079e-01, 6.461720360015138498e-02, + 2.500000000000000000e-01, 5.000000000000000000e-01, 3.839262482346385980e-02, 1.485387121556507317e-01, + 7.029225756886985366e-01, 1.670153200441366892e-02, 6.461720360015138498e-02, 8.707655927996972300e-01, + 3.288504389784733389e-03, 1.272302191431040619e-02, 9.745539561713791876e-01, 1.259459028105180400e-01, + 6.850359770195969533e-01, 2.544604382862070135e-02, 1.125328751958891010e-01, 6.120807933118596056e-01, + 1.292344072003027700e-01, 9.084178237683891710e-02, 4.941001474128341453e-01, 2.970774243113014079e-01, + 6.461720360015138498e-02, 3.514612878443492683e-01, 5.000000000000000000e-01, 3.839262482346385980e-02, + 2.088224282758644190e-01, 7.029225756886985366e-01, 1.670153200441366892e-02, 9.084178237683891710e-02, + 8.707655927996972300e-01, 3.288504389784733389e-03, 1.788659866910165264e-02, 9.745539561713791876e-01, + 1.259459028105180400e-01, 8.486080533608612031e-01, 2.544604382862070135e-02, 1.125328751958891010e-01, + 7.582327176038081706e-01, 1.292344072003027700e-01, 9.084178237683891710e-02, 6.120807933118596056e-01, + 2.970774243113014079e-01, 6.461720360015138498e-02, 4.353827963998486150e-01, 5.000000000000000000e-01, + 3.839262482346385980e-02, 2.586847994878376245e-01, 7.029225756886985366e-01, 1.670153200441366892e-02, + 1.125328751958891010e-01, 8.707655927996972300e-01, 3.288504389784733389e-03, 2.215753943883607899e-02, + 9.745539561713791876e-01, 1.259459028105180400e-01, 9.497554134892866040e-01, 2.544604382862070135e-02, + 1.125328751958891010e-01, 8.486080533608612031e-01, 1.292344072003027700e-01, 9.084178237683891710e-02, + 6.850359770195968423e-01, 2.970774243113014079e-01, 6.461720360015138498e-02, 4.872769780856895938e-01, + 5.000000000000000000e-01, 3.839262482346385980e-02, 2.895179791517822898e-01, 7.029225756886985366e-01, + 1.670153200441366892e-02, 1.259459028105180400e-01, 8.707655927996972300e-01, 3.288504389784733389e-03, + 2.479854268209272239e-02, 9.745539561713791876e-01, 2.895179791517822898e-01, 2.479854268209261484e-02, + 2.544604382862070135e-02, 2.586847994878375689e-01, 2.215753943883598184e-02, 1.292344072003027700e-01, + 2.088224282758643635e-01, 1.788659866910157631e-02, 2.970774243113014079e-01, 1.485387121556507040e-01, + 1.272302191431035068e-02, 5.000000000000000000e-01, 8.825499603543703053e-02, 7.559445159519125913e-03, + 7.029225756886985366e-01, 3.839262482346385286e-02, 3.288504389784719078e-03, 8.707655927996972300e-01, + 7.559445159519158006e-03, 6.475011465280884702e-04, 9.745539561713791876e-01, 2.895179791517822898e-01, + 1.259459028105180400e-01, 2.544604382862070135e-02, 2.586847994878375689e-01, 1.125328751958891010e-01, + 1.292344072003027700e-01, 2.088224282758643635e-01, 9.084178237683891710e-02, 2.970774243113014079e-01, + 1.485387121556507040e-01, 6.461720360015138498e-02, 5.000000000000000000e-01, 8.825499603543703053e-02, + 3.839262482346385980e-02, 7.029225756886985366e-01, 3.839262482346385286e-02, 1.670153200441366892e-02, + 8.707655927996972300e-01, 7.559445159519158006e-03, 3.288504389784733389e-03, 9.745539561713791876e-01, + 2.895179791517822898e-01, 2.895179791517822898e-01, 2.544604382862070135e-02, 2.586847994878375689e-01, + 2.586847994878375689e-01, 1.292344072003027700e-01, 2.088224282758643635e-01, 2.088224282758643635e-01, + 2.970774243113014079e-01, 1.485387121556507040e-01, 1.485387121556507040e-01, 5.000000000000000000e-01, + 8.825499603543703053e-02, 8.825499603543703053e-02, 7.029225756886985366e-01, 3.839262482346385286e-02, + 3.839262482346385286e-02, 8.707655927996972300e-01, 7.559445159519158006e-03, 7.559445159519158006e-03, + 9.745539561713791876e-01, 2.895179791517822898e-01, 4.872769780856896493e-01, 2.544604382862070135e-02, + 2.586847994878375689e-01, 4.353827963998486150e-01, 1.292344072003027700e-01, 2.088224282758643635e-01, + 3.514612878443492683e-01, 2.970774243113014079e-01, 1.485387121556507040e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 8.825499603543703053e-02, 1.485387121556507317e-01, 7.029225756886985366e-01, + 3.839262482346385286e-02, 6.461720360015138498e-02, 8.707655927996972300e-01, 7.559445159519158006e-03, + 1.272302191431040619e-02, 9.745539561713791876e-01, 2.895179791517822898e-01, 6.850359770195969533e-01, + 2.544604382862070135e-02, 2.586847994878375689e-01, 6.120807933118596056e-01, 1.292344072003027700e-01, + 2.088224282758643635e-01, 4.941001474128341453e-01, 2.970774243113014079e-01, 1.485387121556507040e-01, + 3.514612878443492683e-01, 5.000000000000000000e-01, 8.825499603543703053e-02, 2.088224282758644190e-01, + 7.029225756886985366e-01, 3.839262482346385286e-02, 9.084178237683891710e-02, 8.707655927996972300e-01, + 7.559445159519158006e-03, 1.788659866910165264e-02, 9.745539561713791876e-01, 2.895179791517822898e-01, + 8.486080533608612031e-01, 2.544604382862070135e-02, 2.586847994878375689e-01, 7.582327176038081706e-01, + 1.292344072003027700e-01, 2.088224282758643635e-01, 6.120807933118596056e-01, 2.970774243113014079e-01, + 1.485387121556507040e-01, 4.353827963998486150e-01, 5.000000000000000000e-01, 8.825499603543703053e-02, + 2.586847994878376245e-01, 7.029225756886985366e-01, 3.839262482346385286e-02, 1.125328751958891010e-01, + 8.707655927996972300e-01, 7.559445159519158006e-03, 2.215753943883607899e-02, 9.745539561713791876e-01, + 2.895179791517822898e-01, 9.497554134892866040e-01, 2.544604382862070135e-02, 2.586847994878375689e-01, + 8.486080533608612031e-01, 1.292344072003027700e-01, 2.088224282758643635e-01, 6.850359770195968423e-01, + 2.970774243113014079e-01, 1.485387121556507040e-01, 4.872769780856895938e-01, 5.000000000000000000e-01, + 8.825499603543703053e-02, 2.895179791517822898e-01, 7.029225756886985366e-01, 3.839262482346385286e-02, + 1.259459028105180400e-01, 8.707655927996972300e-01, 7.559445159519158006e-03, 2.479854268209272239e-02, + 9.745539561713791876e-01, 4.872769780856896493e-01, 2.479854268209261484e-02, 2.544604382862070135e-02, + 4.353827963998486150e-01, 2.215753943883598184e-02, 1.292344072003027700e-01, 3.514612878443492683e-01, + 1.788659866910157631e-02, 2.970774243113014079e-01, 2.500000000000000000e-01, 1.272302191431035068e-02, + 5.000000000000000000e-01, 1.485387121556507317e-01, 7.559445159519125913e-03, 7.029225756886985366e-01, + 6.461720360015138498e-02, 3.288504389784719078e-03, 8.707655927996972300e-01, 1.272302191431040619e-02, + 6.475011465280884702e-04, 9.745539561713791876e-01, 4.872769780856896493e-01, 1.259459028105180400e-01, + 2.544604382862070135e-02, 4.353827963998486150e-01, 1.125328751958891010e-01, 1.292344072003027700e-01, + 3.514612878443492683e-01, 9.084178237683891710e-02, 2.970774243113014079e-01, 2.500000000000000000e-01, + 6.461720360015138498e-02, 5.000000000000000000e-01, 1.485387121556507317e-01, 3.839262482346385980e-02, + 7.029225756886985366e-01, 6.461720360015138498e-02, 1.670153200441366892e-02, 8.707655927996972300e-01, + 1.272302191431040619e-02, 3.288504389784733389e-03, 9.745539561713791876e-01, 4.872769780856896493e-01, + 2.895179791517822898e-01, 2.544604382862070135e-02, 4.353827963998486150e-01, 2.586847994878375689e-01, + 1.292344072003027700e-01, 3.514612878443492683e-01, 2.088224282758643635e-01, 2.970774243113014079e-01, + 2.500000000000000000e-01, 1.485387121556507040e-01, 5.000000000000000000e-01, 1.485387121556507317e-01, + 8.825499603543703053e-02, 7.029225756886985366e-01, 6.461720360015138498e-02, 3.839262482346385286e-02, + 8.707655927996972300e-01, 1.272302191431040619e-02, 7.559445159519158006e-03, 9.745539561713791876e-01, + 4.872769780856896493e-01, 4.872769780856896493e-01, 2.544604382862070135e-02, 4.353827963998486150e-01, + 4.353827963998486150e-01, 1.292344072003027700e-01, 3.514612878443492683e-01, 3.514612878443492683e-01, + 2.970774243113014079e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, + 1.485387121556507317e-01, 1.485387121556507317e-01, 7.029225756886985366e-01, 6.461720360015138498e-02, + 6.461720360015138498e-02, 8.707655927996972300e-01, 1.272302191431040619e-02, 1.272302191431040619e-02, + 9.745539561713791876e-01, 4.872769780856896493e-01, 6.850359770195969533e-01, 2.544604382862070135e-02, + 4.353827963998486150e-01, 6.120807933118596056e-01, 1.292344072003027700e-01, 3.514612878443492683e-01, + 4.941001474128341453e-01, 2.970774243113014079e-01, 2.500000000000000000e-01, 3.514612878443492683e-01, + 5.000000000000000000e-01, 1.485387121556507317e-01, 2.088224282758644190e-01, 7.029225756886985366e-01, + 6.461720360015138498e-02, 9.084178237683891710e-02, 8.707655927996972300e-01, 1.272302191431040619e-02, + 1.788659866910165264e-02, 9.745539561713791876e-01, 4.872769780856896493e-01, 8.486080533608612031e-01, + 2.544604382862070135e-02, 4.353827963998486150e-01, 7.582327176038081706e-01, 1.292344072003027700e-01, + 3.514612878443492683e-01, 6.120807933118596056e-01, 2.970774243113014079e-01, 2.500000000000000000e-01, + 4.353827963998486150e-01, 5.000000000000000000e-01, 1.485387121556507317e-01, 2.586847994878376245e-01, + 7.029225756886985366e-01, 6.461720360015138498e-02, 1.125328751958891010e-01, 8.707655927996972300e-01, + 1.272302191431040619e-02, 2.215753943883607899e-02, 9.745539561713791876e-01, 4.872769780856896493e-01, + 9.497554134892866040e-01, 2.544604382862070135e-02, 4.353827963998486150e-01, 8.486080533608612031e-01, + 1.292344072003027700e-01, 3.514612878443492683e-01, 6.850359770195968423e-01, 2.970774243113014079e-01, + 2.500000000000000000e-01, 4.872769780856895938e-01, 5.000000000000000000e-01, 1.485387121556507317e-01, + 2.895179791517822898e-01, 7.029225756886985366e-01, 6.461720360015138498e-02, 1.259459028105180400e-01, + 8.707655927996972300e-01, 1.272302191431040619e-02, 2.479854268209272239e-02, 9.745539561713791876e-01, + 6.850359770195969533e-01, 2.479854268209261484e-02, 2.544604382862070135e-02, 6.120807933118596056e-01, + 2.215753943883598184e-02, 1.292344072003027700e-01, 4.941001474128341453e-01, 1.788659866910157631e-02, + 2.970774243113014079e-01, 3.514612878443492683e-01, 1.272302191431035068e-02, 5.000000000000000000e-01, + 2.088224282758644190e-01, 7.559445159519125913e-03, 7.029225756886985366e-01, 9.084178237683891710e-02, + 3.288504389784719078e-03, 8.707655927996972300e-01, 1.788659866910165264e-02, 6.475011465280884702e-04, + 9.745539561713791876e-01, 6.850359770195969533e-01, 1.259459028105180400e-01, 2.544604382862070135e-02, + 6.120807933118596056e-01, 1.125328751958891010e-01, 1.292344072003027700e-01, 4.941001474128341453e-01, + 9.084178237683891710e-02, 2.970774243113014079e-01, 3.514612878443492683e-01, 6.461720360015138498e-02, + 5.000000000000000000e-01, 2.088224282758644190e-01, 3.839262482346385980e-02, 7.029225756886985366e-01, + 9.084178237683891710e-02, 1.670153200441366892e-02, 8.707655927996972300e-01, 1.788659866910165264e-02, + 3.288504389784733389e-03, 9.745539561713791876e-01, 6.850359770195969533e-01, 2.895179791517822898e-01, + 2.544604382862070135e-02, 6.120807933118596056e-01, 2.586847994878375689e-01, 1.292344072003027700e-01, + 4.941001474128341453e-01, 2.088224282758643635e-01, 2.970774243113014079e-01, 3.514612878443492683e-01, + 1.485387121556507040e-01, 5.000000000000000000e-01, 2.088224282758644190e-01, 8.825499603543703053e-02, + 7.029225756886985366e-01, 9.084178237683891710e-02, 3.839262482346385286e-02, 8.707655927996972300e-01, + 1.788659866910165264e-02, 7.559445159519158006e-03, 9.745539561713791876e-01, 6.850359770195969533e-01, + 4.872769780856896493e-01, 2.544604382862070135e-02, 6.120807933118596056e-01, 4.353827963998486150e-01, + 1.292344072003027700e-01, 4.941001474128341453e-01, 3.514612878443492683e-01, 2.970774243113014079e-01, + 3.514612878443492683e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, 2.088224282758644190e-01, + 1.485387121556507317e-01, 7.029225756886985366e-01, 9.084178237683891710e-02, 6.461720360015138498e-02, + 8.707655927996972300e-01, 1.788659866910165264e-02, 1.272302191431040619e-02, 9.745539561713791876e-01, + 6.850359770195969533e-01, 6.850359770195969533e-01, 2.544604382862070135e-02, 6.120807933118596056e-01, + 6.120807933118596056e-01, 1.292344072003027700e-01, 4.941001474128341453e-01, 4.941001474128341453e-01, + 2.970774243113014079e-01, 3.514612878443492683e-01, 3.514612878443492683e-01, 5.000000000000000000e-01, + 2.088224282758644190e-01, 2.088224282758644190e-01, 7.029225756886985366e-01, 9.084178237683891710e-02, + 9.084178237683891710e-02, 8.707655927996972300e-01, 1.788659866910165264e-02, 1.788659866910165264e-02, + 9.745539561713791876e-01, 6.850359770195969533e-01, 8.486080533608612031e-01, 2.544604382862070135e-02, + 6.120807933118596056e-01, 7.582327176038081706e-01, 1.292344072003027700e-01, 4.941001474128341453e-01, + 6.120807933118596056e-01, 2.970774243113014079e-01, 3.514612878443492683e-01, 4.353827963998486150e-01, + 5.000000000000000000e-01, 2.088224282758644190e-01, 2.586847994878376245e-01, 7.029225756886985366e-01, + 9.084178237683891710e-02, 1.125328751958891010e-01, 8.707655927996972300e-01, 1.788659866910165264e-02, + 2.215753943883607899e-02, 9.745539561713791876e-01, 6.850359770195969533e-01, 9.497554134892866040e-01, + 2.544604382862070135e-02, 6.120807933118596056e-01, 8.486080533608612031e-01, 1.292344072003027700e-01, + 4.941001474128341453e-01, 6.850359770195968423e-01, 2.970774243113014079e-01, 3.514612878443492683e-01, + 4.872769780856895938e-01, 5.000000000000000000e-01, 2.088224282758644190e-01, 2.895179791517822898e-01, + 7.029225756886985366e-01, 9.084178237683891710e-02, 1.259459028105180400e-01, 8.707655927996972300e-01, + 1.788659866910165264e-02, 2.479854268209272239e-02, 9.745539561713791876e-01, 8.486080533608612031e-01, + 2.479854268209261484e-02, 2.544604382862070135e-02, 7.582327176038081706e-01, 2.215753943883598184e-02, + 1.292344072003027700e-01, 6.120807933118596056e-01, 1.788659866910157631e-02, 2.970774243113014079e-01, + 4.353827963998486150e-01, 1.272302191431035068e-02, 5.000000000000000000e-01, 2.586847994878376245e-01, + 7.559445159519125913e-03, 7.029225756886985366e-01, 1.125328751958891010e-01, 3.288504389784719078e-03, + 8.707655927996972300e-01, 2.215753943883607899e-02, 6.475011465280884702e-04, 9.745539561713791876e-01, + 8.486080533608612031e-01, 1.259459028105180400e-01, 2.544604382862070135e-02, 7.582327176038081706e-01, + 1.125328751958891010e-01, 1.292344072003027700e-01, 6.120807933118596056e-01, 9.084178237683891710e-02, + 2.970774243113014079e-01, 4.353827963998486150e-01, 6.461720360015138498e-02, 5.000000000000000000e-01, + 2.586847994878376245e-01, 3.839262482346385980e-02, 7.029225756886985366e-01, 1.125328751958891010e-01, + 1.670153200441366892e-02, 8.707655927996972300e-01, 2.215753943883607899e-02, 3.288504389784733389e-03, + 9.745539561713791876e-01, 8.486080533608612031e-01, 2.895179791517822898e-01, 2.544604382862070135e-02, + 7.582327176038081706e-01, 2.586847994878375689e-01, 1.292344072003027700e-01, 6.120807933118596056e-01, + 2.088224282758643635e-01, 2.970774243113014079e-01, 4.353827963998486150e-01, 1.485387121556507040e-01, + 5.000000000000000000e-01, 2.586847994878376245e-01, 8.825499603543703053e-02, 7.029225756886985366e-01, + 1.125328751958891010e-01, 3.839262482346385286e-02, 8.707655927996972300e-01, 2.215753943883607899e-02, + 7.559445159519158006e-03, 9.745539561713791876e-01, 8.486080533608612031e-01, 4.872769780856896493e-01, + 2.544604382862070135e-02, 7.582327176038081706e-01, 4.353827963998486150e-01, 1.292344072003027700e-01, + 6.120807933118596056e-01, 3.514612878443492683e-01, 2.970774243113014079e-01, 4.353827963998486150e-01, + 2.500000000000000000e-01, 5.000000000000000000e-01, 2.586847994878376245e-01, 1.485387121556507317e-01, + 7.029225756886985366e-01, 1.125328751958891010e-01, 6.461720360015138498e-02, 8.707655927996972300e-01, + 2.215753943883607899e-02, 1.272302191431040619e-02, 9.745539561713791876e-01, 8.486080533608612031e-01, + 6.850359770195969533e-01, 2.544604382862070135e-02, 7.582327176038081706e-01, 6.120807933118596056e-01, + 1.292344072003027700e-01, 6.120807933118596056e-01, 4.941001474128341453e-01, 2.970774243113014079e-01, + 4.353827963998486150e-01, 3.514612878443492683e-01, 5.000000000000000000e-01, 2.586847994878376245e-01, + 2.088224282758644190e-01, 7.029225756886985366e-01, 1.125328751958891010e-01, 9.084178237683891710e-02, + 8.707655927996972300e-01, 2.215753943883607899e-02, 1.788659866910165264e-02, 9.745539561713791876e-01, + 8.486080533608612031e-01, 8.486080533608612031e-01, 2.544604382862070135e-02, 7.582327176038081706e-01, + 7.582327176038081706e-01, 1.292344072003027700e-01, 6.120807933118596056e-01, 6.120807933118596056e-01, + 2.970774243113014079e-01, 4.353827963998486150e-01, 4.353827963998486150e-01, 5.000000000000000000e-01, + 2.586847994878376245e-01, 2.586847994878376245e-01, 7.029225756886985366e-01, 1.125328751958891010e-01, + 1.125328751958891010e-01, 8.707655927996972300e-01, 2.215753943883607899e-02, 2.215753943883607899e-02, + 9.745539561713791876e-01, 8.486080533608612031e-01, 9.497554134892866040e-01, 2.544604382862070135e-02, + 7.582327176038081706e-01, 8.486080533608612031e-01, 1.292344072003027700e-01, 6.120807933118596056e-01, + 6.850359770195968423e-01, 2.970774243113014079e-01, 4.353827963998486150e-01, 4.872769780856895938e-01, + 5.000000000000000000e-01, 2.586847994878376245e-01, 2.895179791517822898e-01, 7.029225756886985366e-01, + 1.125328751958891010e-01, 1.259459028105180400e-01, 8.707655927996972300e-01, 2.215753943883607899e-02, + 2.479854268209272239e-02, 9.745539561713791876e-01, 9.497554134892866040e-01, 2.479854268209261484e-02, + 2.544604382862070135e-02, 8.486080533608612031e-01, 2.215753943883598184e-02, 1.292344072003027700e-01, + 6.850359770195968423e-01, 1.788659866910157631e-02, 2.970774243113014079e-01, 4.872769780856895938e-01, + 1.272302191431035068e-02, 5.000000000000000000e-01, 2.895179791517822898e-01, 7.559445159519125913e-03, + 7.029225756886985366e-01, 1.259459028105180400e-01, 3.288504389784719078e-03, 8.707655927996972300e-01, + 2.479854268209272239e-02, 6.475011465280884702e-04, 9.745539561713791876e-01, 9.497554134892866040e-01, + 1.259459028105180400e-01, 2.544604382862070135e-02, 8.486080533608612031e-01, 1.125328751958891010e-01, + 1.292344072003027700e-01, 6.850359770195968423e-01, 9.084178237683891710e-02, 2.970774243113014079e-01, + 4.872769780856895938e-01, 6.461720360015138498e-02, 5.000000000000000000e-01, 2.895179791517822898e-01, + 3.839262482346385980e-02, 7.029225756886985366e-01, 1.259459028105180400e-01, 1.670153200441366892e-02, + 8.707655927996972300e-01, 2.479854268209272239e-02, 3.288504389784733389e-03, 9.745539561713791876e-01, + 9.497554134892866040e-01, 2.895179791517822898e-01, 2.544604382862070135e-02, 8.486080533608612031e-01, + 2.586847994878375689e-01, 1.292344072003027700e-01, 6.850359770195968423e-01, 2.088224282758643635e-01, + 2.970774243113014079e-01, 4.872769780856895938e-01, 1.485387121556507040e-01, 5.000000000000000000e-01, + 2.895179791517822898e-01, 8.825499603543703053e-02, 7.029225756886985366e-01, 1.259459028105180400e-01, + 3.839262482346385286e-02, 8.707655927996972300e-01, 2.479854268209272239e-02, 7.559445159519158006e-03, + 9.745539561713791876e-01, 9.497554134892866040e-01, 4.872769780856896493e-01, 2.544604382862070135e-02, + 8.486080533608612031e-01, 4.353827963998486150e-01, 1.292344072003027700e-01, 6.850359770195968423e-01, + 3.514612878443492683e-01, 2.970774243113014079e-01, 4.872769780856895938e-01, 2.500000000000000000e-01, + 5.000000000000000000e-01, 2.895179791517822898e-01, 1.485387121556507317e-01, 7.029225756886985366e-01, + 1.259459028105180400e-01, 6.461720360015138498e-02, 8.707655927996972300e-01, 2.479854268209272239e-02, + 1.272302191431040619e-02, 9.745539561713791876e-01, 9.497554134892866040e-01, 6.850359770195969533e-01, + 2.544604382862070135e-02, 8.486080533608612031e-01, 6.120807933118596056e-01, 1.292344072003027700e-01, + 6.850359770195968423e-01, 4.941001474128341453e-01, 2.970774243113014079e-01, 4.872769780856895938e-01, + 3.514612878443492683e-01, 5.000000000000000000e-01, 2.895179791517822898e-01, 2.088224282758644190e-01, + 7.029225756886985366e-01, 1.259459028105180400e-01, 9.084178237683891710e-02, 8.707655927996972300e-01, + 2.479854268209272239e-02, 1.788659866910165264e-02, 9.745539561713791876e-01, 9.497554134892866040e-01, + 8.486080533608612031e-01, 2.544604382862070135e-02, 8.486080533608612031e-01, 7.582327176038081706e-01, + 1.292344072003027700e-01, 6.850359770195968423e-01, 6.120807933118596056e-01, 2.970774243113014079e-01, + 4.872769780856895938e-01, 4.353827963998486150e-01, 5.000000000000000000e-01, 2.895179791517822898e-01, + 2.586847994878376245e-01, 7.029225756886985366e-01, 1.259459028105180400e-01, 1.125328751958891010e-01, + 8.707655927996972300e-01, 2.479854268209272239e-02, 2.215753943883607899e-02, 9.745539561713791876e-01, + 9.497554134892866040e-01, 9.497554134892866040e-01, 2.544604382862070135e-02, 8.486080533608612031e-01, + 8.486080533608612031e-01, 1.292344072003027700e-01, 6.850359770195968423e-01, 6.850359770195968423e-01, + 2.970774243113014079e-01, 4.872769780856895938e-01, 4.872769780856895938e-01, 5.000000000000000000e-01, + 2.895179791517822898e-01, 2.895179791517822898e-01, 7.029225756886985366e-01, 1.259459028105180400e-01, + 1.259459028105180400e-01, 8.707655927996972300e-01, 2.479854268209272239e-02, 2.479854268209272239e-02, + 9.745539561713791876e-01 +}; +inline constexpr double pyramid_o10_weights[343] = { + 2.577388186715318021e-04, 4.444798385185597198e-04, 3.953973886067965586e-04, 2.189891456490211641e-04, + 7.062494343835647019e-05, 9.790522191912680791e-06, 1.757149032522950112e-07, 5.567514076073230801e-04, + 9.601377744485532599e-04, 8.541129109140622490e-04, 4.730467677288396184e-04, 1.525596216905281870e-04, + 2.114887869686736161e-05, 3.795684337638560425e-07, 7.600297475626251490e-04, 1.310698563790889956e-03, + 1.165962422729679591e-03, 6.457632806127400118e-04, 2.082614416010263665e-04, 2.887065343272472059e-05, + 5.181545963863137413e-07, 8.319445063022067998e-04, 1.434718144994942025e-03, 1.276286928578200834e-03, + 7.068660343892551202e-04, 2.279673430812328646e-04, 3.160242292323061600e-05, 5.671828915397961299e-07, + 7.600297475626254743e-04, 1.310698563790890390e-03, 1.165962422729680025e-03, 6.457632806127403370e-04, + 2.082614416010264478e-04, 2.887065343272473075e-05, 5.181545963863139530e-07, 5.567514076073230801e-04, + 9.601377744485532599e-04, 8.541129109140622490e-04, 4.730467677288396184e-04, 1.525596216905281870e-04, + 2.114887869686736161e-05, 3.795684337638560425e-07, 2.577388186715329947e-04, 4.444798385185618340e-04, + 3.953973886067984017e-04, 2.189891456490221941e-04, 7.062494343835679545e-05, 9.790522191912728224e-06, + 1.757149032522958053e-07, 5.567514076073230801e-04, 9.601377744485532599e-04, 8.541129109140622490e-04, + 4.730467677288396184e-04, 1.525596216905281870e-04, 2.114887869686736161e-05, 3.795684337638560425e-07, + 1.202659853375719285e-03, 2.074030059486100652e-03, 1.845001726391151849e-03, 1.021846282816907779e-03, + 3.295498309414108379e-04, 4.568449581823890153e-05, 8.199201845906319763e-07, 1.641769113244050593e-03, + 2.831289730048243934e-03, 2.518639696643006336e-03, 1.394937779708141107e-03, 4.498734469233012039e-04, + 6.236459459254939223e-05, 1.119285416078259340e-06, 1.797114913936298432e-03, 3.099189135974454810e-03, + 2.756955850342096385e-03, 1.526928157987608118e-03, 4.924409128712935527e-04, 6.826559358423091935e-05, + 1.225193297863330996e-06, 1.641769113244051677e-03, 2.831289730048245669e-03, 2.518639696643007637e-03, + 1.394937779708141758e-03, 4.498734469233015292e-04, 6.236459459254941934e-05, 1.119285416078259975e-06, + 1.202659853375719285e-03, 2.074030059486100652e-03, 1.845001726391151849e-03, 1.021846282816907779e-03, + 3.295498309414108379e-04, 4.568449581823890153e-05, 8.199201845906319763e-07, 5.567514076073255738e-04, + 9.601377744485577051e-04, 8.541129109140661521e-04, 4.730467677288417868e-04, 1.525596216905289188e-04, + 2.114887869686745987e-05, 3.795684337638577895e-07, 7.600297475626251490e-04, 1.310698563790889956e-03, + 1.165962422729679591e-03, 6.457632806127400118e-04, 2.082614416010263665e-04, 2.887065343272472059e-05, + 5.181545963863137413e-07, 1.641769113244050593e-03, 2.831289730048243934e-03, 2.518639696643006336e-03, + 1.394937779708141107e-03, 4.498734469233012039e-04, 6.236459459254939223e-05, 1.119285416078259340e-06, + 2.241203789780195441e-03, 3.865036332917421141e-03, 3.438233054618347346e-03, 1.904250611836625327e-03, + 6.141290307098765241e-04, 8.513484912183872093e-05, 1.527953410820075491e-06, 2.453268686378204909e-03, + 4.230749854385304487e-03, 3.763562032077824582e-03, 2.084432668879922582e-03, 6.722385207924800200e-04, + 9.319039188784505813e-05, 1.672529858329970760e-06, 2.241203789780196309e-03, 3.865036332917422442e-03, + 3.438233054618348647e-03, 1.904250611836625978e-03, 6.141290307098768494e-04, 8.513484912183874804e-05, + 1.527953410820076126e-06, 1.641769113244050593e-03, 2.831289730048243934e-03, 2.518639696643006336e-03, + 1.394937779708141107e-03, 4.498734469233012039e-04, 6.236459459254939223e-05, 1.119285416078259340e-06, + 7.600297475626287269e-04, 1.310698563790896028e-03, 1.165962422729685012e-03, 6.457632806127430476e-04, + 2.082614416010273423e-04, 2.887065343272485611e-05, 5.181545963863161765e-07, 8.319445063022067998e-04, + 1.434718144994942025e-03, 1.276286928578200834e-03, 7.068660343892551202e-04, 2.279673430812328646e-04, + 3.160242292323061600e-05, 5.671828915397961299e-07, 1.797114913936298432e-03, 3.099189135974454810e-03, + 2.756955850342096385e-03, 1.526928157987608118e-03, 4.924409128712935527e-04, 6.826559358423091935e-05, + 1.225193297863330996e-06, 2.453268686378204909e-03, 4.230749854385304487e-03, 3.763562032077824582e-03, + 2.084432668879922582e-03, 6.722385207924800200e-04, 9.319039188784505813e-05, 1.672529858329970760e-06, + 2.685399371091597327e-03, 4.631067547266884583e-03, 4.119673955862789005e-03, 2.281663728548479538e-03, + 7.358463877125326130e-04, 1.020081579962817917e-04, 1.830786270835240204e-06, 2.453268686378205776e-03, + 4.230749854385306222e-03, 3.763562032077826750e-03, 2.084432668879923883e-03, 6.722385207924803452e-04, + 9.319039188784509879e-05, 1.672529858329971607e-06, 1.797114913936298432e-03, 3.099189135974454810e-03, + 2.756955850342096385e-03, 1.526928157987608118e-03, 4.924409128712935527e-04, 6.826559358423091935e-05, + 1.225193297863330996e-06, 8.319445063022108114e-04, 1.434718144994948747e-03, 1.276286928578206905e-03, + 7.068660343892584812e-04, 2.279673430812339488e-04, 3.160242292323076508e-05, 5.671828915397988828e-07, + 7.600297475626254743e-04, 1.310698563790890390e-03, 1.165962422729680025e-03, 6.457632806127403370e-04, + 2.082614416010264478e-04, 2.887065343272473075e-05, 5.181545963863139530e-07, 1.641769113244051677e-03, + 2.831289730048245669e-03, 2.518639696643007637e-03, 1.394937779708141758e-03, 4.498734469233015292e-04, + 6.236459459254941934e-05, 1.119285416078259975e-06, 2.241203789780196309e-03, 3.865036332917422442e-03, + 3.438233054618348647e-03, 1.904250611836625978e-03, 6.141290307098768494e-04, 8.513484912183874804e-05, + 1.527953410820076126e-06, 2.453268686378205776e-03, 4.230749854385306222e-03, 3.763562032077826750e-03, + 2.084432668879923883e-03, 6.722385207924803452e-04, 9.319039188784509879e-05, 1.672529858329971607e-06, + 2.241203789780197610e-03, 3.865036332917425044e-03, 3.438233054618350815e-03, 1.904250611836627062e-03, + 6.141290307098771746e-04, 8.513484912183880225e-05, 1.527953410820076973e-06, 1.641769113244051677e-03, + 2.831289730048245669e-03, 2.518639696643007637e-03, 1.394937779708141758e-03, 4.498734469233015292e-04, + 6.236459459254941934e-05, 1.119285416078259975e-06, 7.600297475626290522e-04, 1.310698563790896462e-03, + 1.165962422729685446e-03, 6.457632806127432644e-04, 2.082614416010273965e-04, 2.887065343272486628e-05, + 5.181545963863163882e-07, 5.567514076073230801e-04, 9.601377744485532599e-04, 8.541129109140622490e-04, + 4.730467677288396184e-04, 1.525596216905281870e-04, 2.114887869686736161e-05, 3.795684337638560425e-07, + 1.202659853375719285e-03, 2.074030059486100652e-03, 1.845001726391151849e-03, 1.021846282816907779e-03, + 3.295498309414108379e-04, 4.568449581823890153e-05, 8.199201845906319763e-07, 1.641769113244050593e-03, + 2.831289730048243934e-03, 2.518639696643006336e-03, 1.394937779708141107e-03, 4.498734469233012039e-04, + 6.236459459254939223e-05, 1.119285416078259340e-06, 1.797114913936298432e-03, 3.099189135974454810e-03, + 2.756955850342096385e-03, 1.526928157987608118e-03, 4.924409128712935527e-04, 6.826559358423091935e-05, + 1.225193297863330996e-06, 1.641769113244051677e-03, 2.831289730048245669e-03, 2.518639696643007637e-03, + 1.394937779708141758e-03, 4.498734469233015292e-04, 6.236459459254941934e-05, 1.119285416078259975e-06, + 1.202659853375719285e-03, 2.074030059486100652e-03, 1.845001726391151849e-03, 1.021846282816907779e-03, + 3.295498309414108379e-04, 4.568449581823890153e-05, 8.199201845906319763e-07, 5.567514076073255738e-04, + 9.601377744485577051e-04, 8.541129109140661521e-04, 4.730467677288417868e-04, 1.525596216905289188e-04, + 2.114887869686745987e-05, 3.795684337638577895e-07, 2.577388186715329947e-04, 4.444798385185618340e-04, + 3.953973886067984017e-04, 2.189891456490221941e-04, 7.062494343835679545e-05, 9.790522191912728224e-06, + 1.757149032522958053e-07, 5.567514076073255738e-04, 9.601377744485577051e-04, 8.541129109140661521e-04, + 4.730467677288417868e-04, 1.525596216905289188e-04, 2.114887869686745987e-05, 3.795684337638577895e-07, + 7.600297475626287269e-04, 1.310698563790896028e-03, 1.165962422729685012e-03, 6.457632806127430476e-04, + 2.082614416010273423e-04, 2.887065343272485611e-05, 5.181545963863161765e-07, 8.319445063022108114e-04, + 1.434718144994948747e-03, 1.276286928578206905e-03, 7.068660343892584812e-04, 2.279673430812339488e-04, + 3.160242292323076508e-05, 5.671828915397988828e-07, 7.600297475626290522e-04, 1.310698563790896462e-03, + 1.165962422729685446e-03, 6.457632806127432644e-04, 2.082614416010273965e-04, 2.887065343272486628e-05, + 5.181545963863163882e-07, 5.567514076073255738e-04, 9.601377744485577051e-04, 8.541129109140661521e-04, + 4.730467677288417868e-04, 1.525596216905289188e-04, 2.114887869686745987e-05, 3.795684337638577895e-07, + 2.577388186715341874e-04, 4.444798385185638398e-04, 3.953973886068001906e-04, 2.189891456490231970e-04, + 7.062494343835712071e-05, 9.790522191912770576e-06, 1.757149032522966258e-07 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_quadrilateral.h b/cpp/src/generated/quadrature_tables_quadrilateral.h new file mode 100644 index 0000000..d289d99 --- /dev/null +++ b/cpp/src/generated/quadrature_tables_quadrilateral.h @@ -0,0 +1,207 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'quadrilateral', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o1_npts = 1; +inline constexpr int quadrilateral_o1_tdim = 2; +inline constexpr double quadrilateral_o1_points[2] = { + 5.000000000000000000e-01, 5.000000000000000000e-01 +}; +inline constexpr double quadrilateral_o1_weights[1] = { + 1.000000000000000000e+00 +}; + +// ---- order 2: 4 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o2_npts = 4; +inline constexpr int quadrilateral_o2_tdim = 2; +inline constexpr double quadrilateral_o2_points[8] = { + 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 7.886751345948128655e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01 +}; +inline constexpr double quadrilateral_o2_weights[4] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01 +}; + +// ---- order 3: 4 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o3_npts = 4; +inline constexpr int quadrilateral_o3_tdim = 2; +inline constexpr double quadrilateral_o3_points[8] = { + 2.113248654051871345e-01, 2.113248654051871345e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, + 7.886751345948128655e-01, 2.113248654051871345e-01, 7.886751345948128655e-01, 7.886751345948128655e-01 +}; +inline constexpr double quadrilateral_o3_weights[4] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01 +}; + +// ---- order 4: 9 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o4_npts = 9; +inline constexpr int quadrilateral_o4_tdim = 2; +inline constexpr double quadrilateral_o4_points[18] = { + 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01 +}; +inline constexpr double quadrilateral_o4_weights[9] = { + 7.716049382716043403e-02, 1.234567901234567416e-01, 7.716049382716043403e-02, 1.234567901234567416e-01, + 1.975308641975308532e-01, 1.234567901234567416e-01, 7.716049382716043403e-02, 1.234567901234567416e-01, + 7.716049382716043403e-02 +}; + +// ---- order 5: 9 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o5_npts = 9; +inline constexpr int quadrilateral_o5_tdim = 2; +inline constexpr double quadrilateral_o5_points[18] = { + 1.127016653792582979e-01, 1.127016653792582979e-01, 1.127016653792582979e-01, 5.000000000000000000e-01, + 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, 1.127016653792582979e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 8.872983346207417021e-01, + 8.872983346207417021e-01, 1.127016653792582979e-01, 8.872983346207417021e-01, 5.000000000000000000e-01, + 8.872983346207417021e-01, 8.872983346207417021e-01 +}; +inline constexpr double quadrilateral_o5_weights[9] = { + 7.716049382716043403e-02, 1.234567901234567416e-01, 7.716049382716043403e-02, 1.234567901234567416e-01, + 1.975308641975308532e-01, 1.234567901234567416e-01, 7.716049382716043403e-02, 1.234567901234567416e-01, + 7.716049382716043403e-02 +}; + +// ---- order 6: 16 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o6_npts = 16; +inline constexpr int quadrilateral_o6_tdim = 2; +inline constexpr double quadrilateral_o6_points[32] = { + 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.943184420297371373e-02, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, 3.300094782075718713e-01, + 3.300094782075718713e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, 3.300094782075718713e-01, + 9.305681557970262308e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01 +}; +inline constexpr double quadrilateral_o6_weights[16] = { + 3.025074832140046965e-02, 5.671296296296293726e-02, 5.671296296296292339e-02, 3.025074832140046965e-02, + 5.671296296296293726e-02, 1.063233257526735864e-01, 1.063233257526735587e-01, 5.671296296296293726e-02, + 5.671296296296292339e-02, 1.063233257526735587e-01, 1.063233257526735170e-01, 5.671296296296292339e-02, + 3.025074832140046965e-02, 5.671296296296293726e-02, 5.671296296296292339e-02, 3.025074832140046965e-02 +}; + +// ---- order 7: 16 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o7_npts = 16; +inline constexpr int quadrilateral_o7_tdim = 2; +inline constexpr double quadrilateral_o7_points[32] = { + 6.943184420297371373e-02, 6.943184420297371373e-02, 6.943184420297371373e-02, 3.300094782075718713e-01, + 6.943184420297371373e-02, 6.699905217924281287e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, + 3.300094782075718713e-01, 6.943184420297371373e-02, 3.300094782075718713e-01, 3.300094782075718713e-01, + 3.300094782075718713e-01, 6.699905217924281287e-01, 3.300094782075718713e-01, 9.305681557970262308e-01, + 6.699905217924281287e-01, 6.943184420297371373e-02, 6.699905217924281287e-01, 3.300094782075718713e-01, + 6.699905217924281287e-01, 6.699905217924281287e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, + 9.305681557970262308e-01, 6.943184420297371373e-02, 9.305681557970262308e-01, 3.300094782075718713e-01, + 9.305681557970262308e-01, 6.699905217924281287e-01, 9.305681557970262308e-01, 9.305681557970262308e-01 +}; +inline constexpr double quadrilateral_o7_weights[16] = { + 3.025074832140046965e-02, 5.671296296296293726e-02, 5.671296296296292339e-02, 3.025074832140046965e-02, + 5.671296296296293726e-02, 1.063233257526735864e-01, 1.063233257526735587e-01, 5.671296296296293726e-02, + 5.671296296296292339e-02, 1.063233257526735587e-01, 1.063233257526735170e-01, 5.671296296296292339e-02, + 3.025074832140046965e-02, 5.671296296296293726e-02, 5.671296296296292339e-02, 3.025074832140046965e-02 +}; + +// ---- order 8: 25 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o8_npts = 25; +inline constexpr int quadrilateral_o8_tdim = 2; +inline constexpr double quadrilateral_o8_points[50] = { + 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01 +}; +inline constexpr double quadrilateral_o8_weights[25] = { + 1.403358721560716248e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02, 2.835000000000001408e-02, 5.727135105599782255e-02, 6.807163313768768709e-02, + 5.727135105599782255e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 6.807163313768768709e-02, + 8.090864197530861501e-02, 6.807163313768768709e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 5.727135105599782255e-02, 6.807163313768768709e-02, 5.727135105599782255e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02 +}; + +// ---- order 9: 25 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o9_npts = 25; +inline constexpr int quadrilateral_o9_tdim = 2; +inline constexpr double quadrilateral_o9_points[50] = { + 4.691007703066801815e-02, 4.691007703066801815e-02, 4.691007703066801815e-02, 2.307653449471584461e-01, + 4.691007703066801815e-02, 5.000000000000000000e-01, 4.691007703066801815e-02, 7.692346550528414983e-01, + 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, 4.691007703066801815e-02, + 2.307653449471584461e-01, 2.307653449471584461e-01, 2.307653449471584461e-01, 5.000000000000000000e-01, + 2.307653449471584461e-01, 7.692346550528414983e-01, 2.307653449471584461e-01, 9.530899229693319263e-01, + 5.000000000000000000e-01, 4.691007703066801815e-02, 5.000000000000000000e-01, 2.307653449471584461e-01, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, + 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, 4.691007703066801815e-02, + 7.692346550528414983e-01, 2.307653449471584461e-01, 7.692346550528414983e-01, 5.000000000000000000e-01, + 7.692346550528414983e-01, 7.692346550528414983e-01, 7.692346550528414983e-01, 9.530899229693319263e-01, + 9.530899229693319263e-01, 4.691007703066801815e-02, 9.530899229693319263e-01, 2.307653449471584461e-01, + 9.530899229693319263e-01, 5.000000000000000000e-01, 9.530899229693319263e-01, 7.692346550528414983e-01, + 9.530899229693319263e-01, 9.530899229693319263e-01 +}; +inline constexpr double quadrilateral_o9_weights[25] = { + 1.403358721560716248e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02, 2.835000000000001408e-02, 5.727135105599782255e-02, 6.807163313768768709e-02, + 5.727135105599782255e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 6.807163313768768709e-02, + 8.090864197530861501e-02, 6.807163313768768709e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 5.727135105599782255e-02, 6.807163313768768709e-02, 5.727135105599782255e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02, 2.835000000000001408e-02, 3.369626809688022057e-02, 2.835000000000001408e-02, + 1.403358721560716248e-02 +}; + +// ---- order 10: 36 point(s), tdim=2 ---- +inline constexpr int quadrilateral_o10_npts = 36; +inline constexpr int quadrilateral_o10_tdim = 2; +inline constexpr double quadrilateral_o10_points[72] = { + 3.376524289842397497e-02, 3.376524289842397497e-02, 3.376524289842397497e-02, 1.693953067668677037e-01, + 3.376524289842397497e-02, 3.806904069584015060e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, + 3.376524289842397497e-02, 8.306046932331323518e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, + 1.693953067668677037e-01, 3.376524289842397497e-02, 1.693953067668677037e-01, 1.693953067668677037e-01, + 1.693953067668677037e-01, 3.806904069584015060e-01, 1.693953067668677037e-01, 6.193095930415984940e-01, + 1.693953067668677037e-01, 8.306046932331323518e-01, 1.693953067668677037e-01, 9.662347571015760250e-01, + 3.806904069584015060e-01, 3.376524289842397497e-02, 3.806904069584015060e-01, 1.693953067668677037e-01, + 3.806904069584015060e-01, 3.806904069584015060e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, + 3.806904069584015060e-01, 8.306046932331323518e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, + 6.193095930415984940e-01, 3.376524289842397497e-02, 6.193095930415984940e-01, 1.693953067668677037e-01, + 6.193095930415984940e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, 6.193095930415984940e-01, + 6.193095930415984940e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, 9.662347571015760250e-01, + 8.306046932331323518e-01, 3.376524289842397497e-02, 8.306046932331323518e-01, 1.693953067668677037e-01, + 8.306046932331323518e-01, 3.806904069584015060e-01, 8.306046932331323518e-01, 6.193095930415984940e-01, + 8.306046932331323518e-01, 8.306046932331323518e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, + 9.662347571015760250e-01, 3.376524289842397497e-02, 9.662347571015760250e-01, 1.693953067668677037e-01, + 9.662347571015760250e-01, 3.806904069584015060e-01, 9.662347571015760250e-01, 6.193095930415984940e-01, + 9.662347571015760250e-01, 8.306046932331323518e-01, 9.662347571015760250e-01, 9.662347571015760250e-01 +}; +inline constexpr double quadrilateral_o10_weights[36] = { + 7.338020422245066822e-03, 1.545182334309579825e-02, 2.004127932945162088e-02, 2.004127932945160701e-02, + 1.545182334309579825e-02, 7.338020422245066822e-03, 1.545182334309579825e-02, 3.253722814704185923e-02, + 4.220134177189697328e-02, 4.220134177189695246e-02, 3.253722814704185923e-02, 1.545182334309579825e-02, + 2.004127932945162088e-02, 4.220134177189697328e-02, 5.473586254182419980e-02, 5.473586254182416511e-02, + 4.220134177189697328e-02, 2.004127932945162088e-02, 2.004127932945160701e-02, 4.220134177189695246e-02, + 5.473586254182416511e-02, 5.473586254182413735e-02, 4.220134177189695246e-02, 2.004127932945160701e-02, + 1.545182334309579825e-02, 3.253722814704185923e-02, 4.220134177189697328e-02, 4.220134177189695246e-02, + 3.253722814704185923e-02, 1.545182334309579825e-02, 7.338020422245066822e-03, 1.545182334309579825e-02, + 2.004127932945162088e-02, 2.004127932945160701e-02, 1.545182334309579825e-02, 7.338020422245066822e-03 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_tetrahedron.h b/cpp/src/generated/quadrature_tables_tetrahedron.h new file mode 100644 index 0000000..8f20710 --- /dev/null +++ b/cpp/src/generated/quadrature_tables_tetrahedron.h @@ -0,0 +1,370 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'tetrahedron', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o1_npts = 1; +inline constexpr int tetrahedron_o1_tdim = 3; +inline constexpr double tetrahedron_o1_points[3] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01 +}; +inline constexpr double tetrahedron_o1_weights[1] = { + 1.666666666666666574e-01 +}; + +// ---- order 2: 4 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o2_npts = 4; +inline constexpr int tetrahedron_o2_tdim = 3; +inline constexpr double tetrahedron_o2_points[12] = { + 5.854101966249689593e-01, 1.381966011250110038e-01, 1.381966011250110038e-01, 1.381966011250110038e-01, + 5.854101966249689593e-01, 1.381966011250110038e-01, 1.381966011250110038e-01, 1.381966011250110038e-01, + 5.854101966249689593e-01, 1.381966011250110038e-01, 1.381966011250110038e-01, 1.381966011250110038e-01 +}; +inline constexpr double tetrahedron_o2_weights[4] = { + 4.166666666666666435e-02, 4.166666666666666435e-02, 4.166666666666666435e-02, 4.166666666666666435e-02 +}; + +// ---- order 3: 5 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o3_npts = 5; +inline constexpr int tetrahedron_o3_tdim = 3; +inline constexpr double tetrahedron_o3_points[15] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 5.000000000000000000e-01, + 1.666666666666666019e-01, 1.666666666666666019e-01, 1.666666666666666019e-01, 5.000000000000000000e-01, + 1.666666666666666019e-01, 1.666666666666666019e-01, 1.666666666666666019e-01, 5.000000000000000000e-01, + 1.666666666666666019e-01, 1.666666666666666019e-01, 1.666666666666666019e-01 +}; +inline constexpr double tetrahedron_o3_weights[5] = { + -1.333333333333333315e-01, 7.499999999999999722e-02, 7.499999999999999722e-02, 7.499999999999999722e-02, + 7.499999999999999722e-02 +}; + +// ---- order 4: 14 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o4_npts = 14; +inline constexpr int tetrahedron_o4_tdim = 3; +inline constexpr double tetrahedron_o4_points[42] = { + 0.000000000000000000e+00, 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 0.000000000000000000e+00, 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, + 0.000000000000000000e+00, 5.000000000000000000e-01, 0.000000000000000000e+00, 0.000000000000000000e+00, + 0.000000000000000000e+00, 5.000000000000000000e-01, 0.000000000000000000e+00, 0.000000000000000000e+00, + 0.000000000000000000e+00, 5.000000000000000000e-01, 6.984197043243866032e-01, 1.005267652252044980e-01, + 1.005267652252044980e-01, 1.005267652252044980e-01, 1.005267652252044980e-01, 1.005267652252044980e-01, + 1.005267652252044980e-01, 1.005267652252044980e-01, 6.984197043243866032e-01, 1.005267652252044980e-01, + 6.984197043243866032e-01, 1.005267652252044980e-01, 5.688137952042340156e-02, 3.143728734931922064e-01, + 3.143728734931922064e-01, 3.143728734931922064e-01, 3.143728734931922064e-01, 3.143728734931922064e-01, + 3.143728734931922064e-01, 3.143728734931922064e-01, 5.688137952042340156e-02, 3.143728734931922064e-01, + 5.688137952042340156e-02, 3.143728734931922064e-01 +}; +inline constexpr double tetrahedron_o4_weights[14] = { + 3.174603174603166794e-03, 3.174603174603166794e-03, 3.174603174603166794e-03, 3.174603174603166794e-03, + 3.174603174603166794e-03, 3.174603174603166794e-03, 1.476497079049678314e-02, 1.476497079049678314e-02, + 1.476497079049678314e-02, 1.476497079049678314e-02, 2.213979111426511714e-02, 2.213979111426511714e-02, + 2.213979111426511714e-02, 2.213979111426511714e-02 +}; + +// ---- order 5: 15 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o5_npts = 15; +inline constexpr int tetrahedron_o5_tdim = 3; +inline constexpr double tetrahedron_o5_points[45] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 0.000000000000000000e+00, + 3.333333333333333148e-01, 3.333333333333333148e-01, 3.333333333333333148e-01, 3.333333333333333148e-01, + 3.333333333333333148e-01, 3.333333333333333148e-01, 3.333333333333333148e-01, 0.000000000000000000e+00, + 3.333333333333333148e-01, 0.000000000000000000e+00, 3.333333333333333148e-01, 7.272727272727272929e-01, + 9.090909090909089774e-02, 9.090909090909089774e-02, 9.090909090909089774e-02, 9.090909090909089774e-02, + 9.090909090909089774e-02, 9.090909090909089774e-02, 9.090909090909089774e-02, 7.272727272727272929e-01, + 9.090909090909089774e-02, 7.272727272727272929e-01, 9.090909090909089774e-02, 4.334498464263357165e-01, + 6.655015357366429740e-02, 6.655015357366429740e-02, 6.655015357366429740e-02, 4.334498464263357165e-01, + 6.655015357366429740e-02, 6.655015357366429740e-02, 6.655015357366429740e-02, 4.334498464263357165e-01, + 6.655015357366429740e-02, 4.334498464263357165e-01, 4.334498464263357165e-01, 4.334498464263357165e-01, + 6.655015357366429740e-02, 4.334498464263357165e-01, 4.334498464263357165e-01, 4.334498464263357165e-01, + 6.655015357366429740e-02 +}; +inline constexpr double tetrahedron_o5_weights[15] = { + 3.028367809708918182e-02, 6.026785714285717160e-03, 6.026785714285717160e-03, 6.026785714285717160e-03, + 6.026785714285717160e-03, 1.164524908602896681e-02, 1.164524908602896681e-02, 1.164524908602896681e-02, + 1.164524908602896681e-02, 1.094914156138644852e-02, 1.094914156138644852e-02, 1.094914156138644852e-02, + 1.094914156138644852e-02, 1.094914156138644852e-02, 1.094914156138644852e-02 +}; + +// ---- order 6: 24 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o6_npts = 24; +inline constexpr int tetrahedron_o6_tdim = 3; +inline constexpr double tetrahedron_o6_points[72] = { + 3.561913862225449257e-01, 2.146028712591517007e-01, 2.146028712591517007e-01, 2.146028712591517007e-01, + 2.146028712591517007e-01, 2.146028712591517007e-01, 2.146028712591517007e-01, 2.146028712591517007e-01, + 3.561913862225449257e-01, 2.146028712591517007e-01, 3.561913862225449257e-01, 2.146028712591517007e-01, + 8.779781243961659598e-01, 4.067395853461130278e-02, 4.067395853461130278e-02, 4.067395853461130278e-02, + 4.067395853461130278e-02, 4.067395853461130278e-02, 4.067395853461130278e-02, 4.067395853461130278e-02, + 8.779781243961659598e-01, 4.067395853461130278e-02, 8.779781243961659598e-01, 4.067395853461130278e-02, + 3.298632957317310099e-02, 3.223378901422757070e-01, 3.223378901422757070e-01, 3.223378901422757070e-01, + 3.223378901422757070e-01, 3.223378901422757070e-01, 3.223378901422757070e-01, 3.223378901422757070e-01, + 3.298632957317310099e-02, 3.223378901422757070e-01, 3.298632957317310099e-02, 3.223378901422757070e-01, + 2.696723314583158726e-01, 6.366100187501749774e-02, 6.366100187501749774e-02, 6.366100187501749774e-02, + 2.696723314583158726e-01, 6.366100187501749774e-02, 6.366100187501749774e-02, 6.366100187501749774e-02, + 2.696723314583158726e-01, 6.030056647916490764e-01, 6.366100187501749774e-02, 6.366100187501749774e-02, + 6.366100187501749774e-02, 6.030056647916490764e-01, 6.366100187501749774e-02, 6.366100187501749774e-02, + 6.366100187501749774e-02, 6.030056647916490764e-01, 6.366100187501749774e-02, 2.696723314583158726e-01, + 6.030056647916490764e-01, 2.696723314583158726e-01, 6.030056647916490764e-01, 6.366100187501749774e-02, + 6.030056647916490764e-01, 6.366100187501749774e-02, 2.696723314583158726e-01, 6.366100187501749774e-02, + 6.030056647916490764e-01, 2.696723314583158726e-01, 2.696723314583158726e-01, 6.366100187501749774e-02, + 6.030056647916490764e-01, 6.030056647916490764e-01, 2.696723314583158726e-01, 6.366100187501749774e-02 +}; +inline constexpr double tetrahedron_o6_weights[24] = { + 6.653791709694649448e-03, 6.653791709694649448e-03, 6.653791709694649448e-03, 6.653791709694649448e-03, + 1.679535175886783449e-03, 1.679535175886783449e-03, 1.679535175886783449e-03, 1.679535175886783449e-03, + 9.226196923942399028e-03, 9.226196923942399028e-03, 9.226196923942399028e-03, 9.226196923942399028e-03, + 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03, + 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03, + 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03, 8.035714285714283187e-03 +}; + +// ---- order 7: 31 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o7_npts = 31; +inline constexpr int tetrahedron_o7_tdim = 3; +inline constexpr double tetrahedron_o7_points[93] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 7.653604230090440552e-01, + 7.821319233031860663e-02, 7.821319233031860663e-02, 7.821319233031860663e-02, 7.821319233031860663e-02, + 7.821319233031860663e-02, 7.821319233031860663e-02, 7.821319233031860663e-02, 7.653604230090440552e-01, + 7.821319233031860663e-02, 7.653604230090440552e-01, 7.821319233031860663e-02, 6.344703500082867764e-01, + 1.218432166639043940e-01, 1.218432166639043940e-01, 1.218432166639043940e-01, 1.218432166639043940e-01, + 1.218432166639043940e-01, 1.218432166639043940e-01, 1.218432166639043940e-01, 6.344703500082867764e-01, + 1.218432166639043940e-01, 6.344703500082867764e-01, 1.218432166639043940e-01, 2.382506660738299958e-03, + 3.325391644464206209e-01, 3.325391644464206209e-01, 3.325391644464206209e-01, 3.325391644464206209e-01, + 3.325391644464206209e-01, 3.325391644464206209e-01, 3.325391644464206209e-01, 2.382506660738299958e-03, + 3.325391644464206209e-01, 2.382506660738299958e-03, 3.325391644464206209e-01, 0.000000000000000000e+00, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 0.000000000000000000e+00, + 5.000000000000000000e-01, 5.000000000000000000e-01, 5.000000000000000000e-01, 0.000000000000000000e+00, + 5.000000000000000000e-01, 0.000000000000000000e+00, 0.000000000000000000e+00, 0.000000000000000000e+00, + 5.000000000000000000e-01, 0.000000000000000000e+00, 0.000000000000000000e+00, 0.000000000000000000e+00, + 5.000000000000000000e-01, 2.000000000000000111e-01, 1.000000000000000056e-01, 1.000000000000000056e-01, + 1.000000000000000056e-01, 2.000000000000000111e-01, 1.000000000000000056e-01, 1.000000000000000056e-01, + 1.000000000000000056e-01, 2.000000000000000111e-01, 5.999999999999999778e-01, 1.000000000000000056e-01, + 1.000000000000000056e-01, 1.000000000000000056e-01, 5.999999999999999778e-01, 1.000000000000000056e-01, + 1.000000000000000056e-01, 1.000000000000000056e-01, 5.999999999999999778e-01, 1.000000000000000056e-01, + 2.000000000000000111e-01, 5.999999999999999778e-01, 2.000000000000000111e-01, 5.999999999999999778e-01, + 1.000000000000000056e-01, 5.999999999999999778e-01, 1.000000000000000056e-01, 2.000000000000000111e-01, + 1.000000000000000056e-01, 5.999999999999999778e-01, 2.000000000000000111e-01, 2.000000000000000111e-01, + 1.000000000000000056e-01, 5.999999999999999778e-01, 5.999999999999999778e-01, 2.000000000000000111e-01, + 1.000000000000000056e-01 +}; +inline constexpr double tetrahedron_o7_weights[31] = { + 1.826422346610880043e-02, 1.059994152441416648e-02, 1.059994152441416648e-02, 1.059994152441416648e-02, + 1.059994152441416648e-02, -6.251774011432995048e-02, -6.251774011432995048e-02, -6.251774011432995048e-02, + -6.251774011432995048e-02, 4.891425263073533904e-03, 4.891425263073533904e-03, 4.891425263073533904e-03, + 4.891425263073533904e-03, 9.700176366843000011e-04, 9.700176366843000011e-04, 9.700176366843000011e-04, + 9.700176366843000011e-04, 9.700176366843000011e-04, 9.700176366843000011e-04, 2.755731922398508074e-02, + 2.755731922398508074e-02, 2.755731922398508074e-02, 2.755731922398508074e-02, 2.755731922398508074e-02, + 2.755731922398508074e-02, 2.755731922398508074e-02, 2.755731922398508074e-02, 2.755731922398508074e-02, + 2.755731922398508074e-02, 2.755731922398508074e-02, 2.755731922398508074e-02 +}; + +// ---- order 8: 45 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o8_npts = 45; +inline constexpr int tetrahedron_o8_tdim = 3; +inline constexpr double tetrahedron_o8_points[135] = { + 2.500000000000000000e-01, 2.500000000000000000e-01, 2.500000000000000000e-01, 6.175871903000830221e-01, + 1.274709365666389926e-01, 1.274709365666389926e-01, 1.274709365666389926e-01, 1.274709365666389926e-01, + 1.274709365666389926e-01, 1.274709365666389926e-01, 1.274709365666389926e-01, 6.175871903000830221e-01, + 1.274709365666389926e-01, 6.175871903000830221e-01, 1.274709365666389926e-01, 9.037635088221031010e-01, + 3.207883039263229735e-02, 3.207883039263229735e-02, 3.207883039263229735e-02, 3.207883039263229735e-02, + 3.207883039263229735e-02, 3.207883039263229735e-02, 3.207883039263229735e-02, 9.037635088221031010e-01, + 3.207883039263229735e-02, 9.037635088221031010e-01, 3.207883039263229735e-02, 4.502229043567190225e-01, + 4.977709564328099828e-02, 4.977709564328099828e-02, 4.977709564328099828e-02, 4.502229043567190225e-01, + 4.977709564328099828e-02, 4.977709564328099828e-02, 4.977709564328099828e-02, 4.502229043567190225e-01, + 4.977709564328099828e-02, 4.502229043567190225e-01, 4.502229043567190225e-01, 4.502229043567190225e-01, + 4.977709564328099828e-02, 4.502229043567190225e-01, 4.502229043567190225e-01, 4.502229043567190225e-01, + 4.977709564328099828e-02, 3.162695526014501213e-01, 1.837304473985499065e-01, 1.837304473985499065e-01, + 1.837304473985499065e-01, 3.162695526014501213e-01, 1.837304473985499065e-01, 1.837304473985499065e-01, + 1.837304473985499065e-01, 3.162695526014501213e-01, 1.837304473985499065e-01, 3.162695526014501213e-01, + 3.162695526014501213e-01, 3.162695526014501213e-01, 1.837304473985499065e-01, 3.162695526014501213e-01, + 3.162695526014501213e-01, 3.162695526014501213e-01, 1.837304473985499065e-01, 2.291778784481710018e-02, + 2.319010893971509002e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, 2.291778784481710018e-02, + 2.319010893971509002e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, 2.291778784481710018e-02, + 5.132800333608811272e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, + 5.132800333608811272e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, 2.319010893971509002e-01, + 5.132800333608811272e-01, 2.319010893971509002e-01, 2.291778784481710018e-02, 5.132800333608811272e-01, + 2.291778784481710018e-02, 5.132800333608811272e-01, 2.319010893971509002e-01, 5.132800333608811272e-01, + 2.319010893971509002e-01, 2.291778784481710018e-02, 2.319010893971509002e-01, 5.132800333608811272e-01, + 2.291778784481710018e-02, 2.291778784481710018e-02, 2.319010893971509002e-01, 5.132800333608811272e-01, + 5.132800333608811272e-01, 2.291778784481710018e-02, 2.319010893971509002e-01, 7.303134278075383845e-01, + 3.797004847182860326e-02, 3.797004847182860326e-02, 3.797004847182860326e-02, 7.303134278075383845e-01, + 3.797004847182860326e-02, 3.797004847182860326e-02, 3.797004847182860326e-02, 7.303134278075383845e-01, + 1.937464752488043951e-01, 3.797004847182860326e-02, 3.797004847182860326e-02, 3.797004847182860326e-02, + 1.937464752488043951e-01, 3.797004847182860326e-02, 3.797004847182860326e-02, 3.797004847182860326e-02, + 1.937464752488043951e-01, 3.797004847182860326e-02, 7.303134278075383845e-01, 1.937464752488043951e-01, + 7.303134278075383845e-01, 1.937464752488043951e-01, 3.797004847182860326e-02, 1.937464752488043951e-01, + 3.797004847182860326e-02, 7.303134278075383845e-01, 3.797004847182860326e-02, 1.937464752488043951e-01, + 7.303134278075383845e-01, 7.303134278075383845e-01, 3.797004847182860326e-02, 1.937464752488043951e-01, + 1.937464752488043951e-01, 7.303134278075383845e-01, 3.797004847182860326e-02 +}; +inline constexpr double tetrahedron_o8_weights[45] = { + -3.932700664129262086e-02, 4.081316059342699788e-03, 4.081316059342699788e-03, 4.081316059342699788e-03, + 4.081316059342699788e-03, 6.580867733043499175e-04, 6.580867733043499175e-04, 6.580867733043499175e-04, + 6.580867733043499175e-04, 4.384258825122849638e-03, 4.384258825122849638e-03, 4.384258825122849638e-03, + 4.384258825122849638e-03, 4.384258825122849638e-03, 4.384258825122849638e-03, 1.383006384250981720e-02, + 1.383006384250981720e-02, 1.383006384250981720e-02, 1.383006384250981720e-02, 1.383006384250981720e-02, + 1.383006384250981720e-02, 4.240437424683716691e-03, 4.240437424683716691e-03, 4.240437424683716691e-03, + 4.240437424683716691e-03, 4.240437424683716691e-03, 4.240437424683716691e-03, 4.240437424683716691e-03, + 4.240437424683716691e-03, 4.240437424683716691e-03, 4.240437424683716691e-03, 4.240437424683716691e-03, + 4.240437424683716691e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, + 2.238739739614200187e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, + 2.238739739614200187e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, 2.238739739614200187e-03, + 2.238739739614200187e-03 +}; + +// ---- order 9: 57 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o9_npts = 57; +inline constexpr int tetrahedron_o9_tdim = 3; +inline constexpr double tetrahedron_o9_points[171] = { + 6.891366839295158878e-03, 7.203807203359198219e-01, 2.440786635406753113e-01, 1.555789083027258879e-01, + 7.830238380676567411e-01, 3.722731406074435145e-02, 8.847735952623144717e-01, 3.746715582104113290e-02, + 4.313984945003825761e-02, 4.038577127605769146e-02, 2.233476678136009086e-01, 7.237240948070670088e-01, + 5.152850615572770465e-01, 4.405214048473231059e-01, 3.307435692542832345e-02, 3.474022441842899295e-02, + 8.739842844692826684e-01, 4.802973388725061826e-02, 2.737549691522870865e-01, 5.866194063678511483e-01, + 1.331737132533926116e-01, 3.138783669262656950e-02, 2.302476689782933031e-02, 2.010151099960877930e-01, + 4.195473200768754657e-02, 4.206648540987850682e-02, 8.751960265284055573e-01, 2.133537804622215872e-01, + 1.492382397847685956e-02, 7.276112298306399717e-01, 5.137368203860852300e-02, 7.378984493750679885e-01, + 1.052487006572960966e-02, 1.167918776735336027e-02, 3.967712051591504308e-02, 4.731716414318899999e-01, + 6.123408021820864189e-01, 1.432143403978234086e-01, 6.662179087437572001e-04, 4.585901947602266659e-02, + 4.589832979249514749e-02, 3.930065125704704232e-02, 1.030115138063945057e-01, 2.313142488715317913e-01, + 2.864577988396536153e-03, 8.373268951130939317e-03, 2.183035505483950089e-01, 6.383760811337779728e-02, + 7.518893444883035793e-01, 3.020971497018418161e-02, 3.931328269724702201e-02, 2.307731120717098905e-01, + 3.171156153538288308e-02, 2.914913102984440993e-02, 5.807381258551988328e-03, 4.350661767994387086e-01, + 4.471841503422259168e-01, 3.807370349415272748e-01, 1.946216802654708866e-01, 4.205051742028664830e-01, + 2.760774398234713267e-01, 6.335347557774518856e-01, 2.029485866160777099e-02, 6.107660031735782924e-02, + 4.443452478104635839e-01, 4.748568828672349862e-01, 4.011137824044635342e-02, 3.313166695541810980e-02, + 7.136911240616969909e-01, 2.545934399020729003e-02, 1.809521324516701890e-01, 6.820586129684939047e-01, + 6.044262999606022646e-01, 1.758681325508172980e-02, 2.037168634602604944e-01, 7.098027195586388283e-01, + 4.483105915111013778e-02, 2.113179923591710085e-01, 7.304989942620458976e-01, 1.871472921145495016e-01, + 3.706986589769196289e-02, 5.007701133228332768e-01, 2.448223828136365104e-02, 4.933213970351708888e-02, + 3.332720320375656686e-02, 4.837323966974713119e-01, 3.722853145238250172e-02, 3.567521368987188191e-01, + 1.328834330395907981e-01, 2.051457140178871916e-02, 1.838549978176257926e-01, 2.094427630108402008e-02, + 1.704389178072653055e-01, 2.268373199643007876e-01, 4.167820833252521839e-01, 7.798392818596212861e-03, + 1.923717952701476941e-01, 1.242856487305182972e-01, 6.561382235043771161e-01, 3.619508813743886888e-01, + 1.552011599218285944e-02, 3.924336805270702833e-01, 1.965056042084037044e-02, 6.443583098389682462e-01, + 1.340945354491865082e-01, 4.582446695135359849e-01, 3.667546489013540079e-02, 4.635857264004116240e-01, + 1.538959981066759068e-02, 3.394322940952808065e-01, 2.418695119980842911e-01, 1.242356229882020935e-01, + 2.743271521385678979e-02, 4.125431242926855258e-01, 5.655163520825289281e-01, 2.158455335267193131e-01, + 1.861909388269384069e-01, 1.031571468282740933e-01, 6.454494634370022688e-01, 2.025517153954042027e-01, + 4.580873781449242704e-02, 1.306656875755305980e-01, 2.354771046476908936e-01, 2.222475737162643017e-01, + 3.744969790769868134e-01, 3.639253527626089002e-01, 4.587456757955503117e-01, 3.601567132115269798e-01, + 3.689983110212465001e-02, 3.633566801123643275e-01, 4.147619222990370780e-01, 1.625263637506013104e-01, + 1.839853909173692903e-01, 5.281258992885370929e-02, 5.791440724832207509e-01, 3.912033521689178839e-02, + 1.799045015970502925e-01, 4.765094010040438754e-01, 1.524683171735234932e-01, 1.476870681014710862e-01, + 9.407076219108001514e-02, 1.668749728706787894e-01, 5.729563685802239181e-01, 7.774426835124648161e-02, + 1.417161598745217965e-01, 2.271598693150327086e-01, 5.175989795285014239e-01, 3.516091771883740891e-01, + 5.503739382968048099e-02, 1.969410619376470994e-01, 3.364345271215076760e-01, 2.289840090706933007e-01, + 8.273350534732909489e-02, 1.096124583285899984e-01, 3.537003584345383977e-01, 1.179344061919235936e-01, + 5.571603219749478031e-01, 1.325043854190557091e-01, 1.224035951881698031e-01, 8.342378852063631056e-02, + 4.222286016719212776e-01, 3.031418645744978768e-01, 2.709662131081064795e-01, 3.336191905680228054e-01, + 1.998230301014543020e-01, 3.787443941211687259e-01, 1.434829224197070041e-01, 3.469408062547597038e-01, + 1.819772234774806041e-01, 1.650628409073056913e-01, 3.092464405790882886e-01 +}; +inline constexpr double tetrahedron_o9_weights[57] = { + 7.162016044045417919e-04, 9.077771144629986693e-04, 9.168420416400241803e-04, 9.750266138313077625e-04, + 1.100190092573179830e-03, 1.123566736937567714e-03, 1.171941882444346023e-03, 1.174462115726251314e-03, + 1.182281355818436434e-03, 1.184296682706247050e-03, 1.190006619366603541e-03, 1.199964370174360544e-03, + 1.313356829704150986e-03, 1.336868422542667259e-03, 1.366383380621510315e-03, 1.382705960475030154e-03, + 1.524428447483084856e-03, 1.527532399215677074e-03, 1.629900429024984691e-03, 1.637560184133700402e-03, + 1.718580977267058227e-03, 1.937492188901679966e-03, 1.960422756541076606e-03, 2.095484520412313392e-03, + 2.162543037223973313e-03, 2.164588522968261420e-03, 2.180978257180039934e-03, 2.205183817607956915e-03, + 2.212319341124884767e-03, 2.214375065161266771e-03, 2.279539234758831429e-03, 2.377741014223799711e-03, + 2.482591391785385088e-03, 2.519796498600750025e-03, 2.563955465818866693e-03, 2.619562878906951751e-03, + 2.745898463014619789e-03, 3.314801767761809889e-03, 3.559049951653051394e-03, 3.730118502877720083e-03, + 3.834914870295061742e-03, 4.215437802807641375e-03, 4.232085234391223458e-03, 4.275313663630222159e-03, + 4.380082113958086443e-03, 4.395931402994279931e-03, 4.687869774289419797e-03, 5.307550768351452196e-03, + 5.439951471336866400e-03, 5.520179070379666562e-03, 5.870060438901905132e-03, 5.879894123846905046e-03, + 5.952755449705821739e-03, 6.370180985658903915e-03, 6.979224618881529375e-03, 7.535230513202574587e-03, + 8.183687426958101072e-03 +}; + +// ---- order 10: 74 point(s), tdim=3 ---- +inline constexpr int tetrahedron_o10_npts = 74; +inline constexpr int tetrahedron_o10_tdim = 3; +inline constexpr double tetrahedron_o10_points[222] = { + 4.844889527768170973e-03, 4.844889527573281729e-03, 9.854653314167374223e-01, 9.467198482923540048e-01, + 6.667080448351053908e-03, 8.067505561904879385e-03, 6.667080448658991611e-03, 3.854556569737443289e-02, + 8.067505561779885620e-03, 3.854556569748009837e-02, 9.467198482918521840e-01, 8.067505562173938466e-03, + 3.826891933029690201e-03, 8.740251327651676894e-01, 1.855431505435713863e-02, 1.035936602480028029e-01, + 3.826891933043923173e-03, 1.855431505433684930e-02, 8.740251327649323221e-01, 1.035936602475548002e-01, + 1.855431505434555067e-02, 1.735404744568466023e-02, 6.202583012900508841e-01, 3.240346370091629763e-02, + 6.202583012898867931e-01, 3.299841875634281774e-01, 3.240346370091293227e-02, 3.299841875637113953e-01, + 1.735404744578351172e-02, 3.240346370092631739e-02, 3.118131875220203977e-02, 1.266394908785818951e-01, + 8.114772294984898826e-01, 1.266394908788713025e-01, 3.070196087069637994e-02, 8.114772294981942302e-01, + 3.070196087074503893e-02, 3.118131875221085911e-02, 8.114772294979708533e-01, 7.805722458243883333e-01, + 3.518958226876812284e-02, 3.307359982554328171e-02, 3.518958226870846917e-02, 1.511645720816398863e-01, + 3.307359982557703942e-02, 1.511645720815177063e-01, 7.805722458241508566e-01, 3.307359982557291772e-02, + 8.149022829407979174e-01, 3.351563550542004805e-02, 1.193767951360694968e-01, 3.351563550541748759e-02, + 3.220528641772837930e-02, 1.193767951358614965e-01, 3.220528641771443906e-02, 8.149022829405031532e-01, + 1.193767951363346042e-01, 5.596600888554595921e-01, 1.807602495702272105e-01, 7.438795731167320915e-03, + 1.807602495700562084e-01, 2.521408658433209049e-01, 7.438795731199615395e-03, 2.521408658430335237e-01, + 5.596600888557026199e-01, 7.438795731214366616e-03, 3.029393775770430897e-02, 3.510309266619137114e-01, + 3.266058005326467201e-02, 3.510309266617718804e-01, 5.860145555272517903e-01, 3.266058005328976305e-02, + 5.860145555274774987e-01, 3.029393775772133007e-02, 3.266058005327201336e-02, 3.114461249008677879e-02, + 5.992738011392267916e-01, 3.365975263830275899e-01, 3.298405998767924702e-02, 3.114461249007869151e-02, + 3.365975263825117247e-01, 5.992738011395161157e-01, 3.298405998767670738e-02, 3.365975263827066799e-01, + 1.267643164854560973e-02, 1.645472158899153881e-01, 4.090752240119477912e-01, 1.645472158900110893e-01, + 4.137011284494345187e-01, 4.090752240120062999e-01, 4.137011284495361041e-01, 1.267643164855133953e-02, + 4.090752240118714078e-01, 3.573908441154557275e-02, 3.377912559111834190e-02, 5.925328701707139878e-01, + 3.379489198264274963e-01, 3.573908441154142329e-02, 5.925328701708977297e-01, 3.377912559113958879e-02, + 3.379489198260952065e-01, 5.925328701712223589e-01, 1.336599495323922970e-01, 1.336599495326110942e-01, + 5.990201514025275120e-01, 1.669597693172062969e-01, 1.669597693175803033e-01, 4.991206920481123244e-01, + 1.228042856243440062e-02, 4.126008361791699275e-01, 3.979043621857172863e-01, 1.772143730727422095e-01, + 1.228042856244396068e-02, 3.979043621855937185e-01, 4.126008361792021240e-01, 1.772143730727064881e-01, + 3.979043621855659074e-01, 3.696126448561478095e-01, 1.145379863160246037e-01, 2.495402206716512000e-02, + 1.145379863158415001e-01, 4.908953467609971089e-01, 2.495402206723796104e-02, 4.908953467605529086e-01, + 3.696126448562169764e-01, 2.495402206723979985e-02, 8.302494688196357386e-02, 7.282094809974271055e-01, + 3.849678186899387761e-02, 7.282094809970688365e-01, 1.502687902517552132e-01, 3.849678186899534171e-02, + 1.502687902517038099e-01, 8.302494688216775776e-02, 3.849678186896474813e-02, 1.680959146959895914e-02, + 4.043770144883662154e-01, 1.613371421710750875e-01, 4.174762518709079773e-01, 1.680959146964861040e-02, + 1.613371421710811937e-01, 4.043770144883683804e-01, 4.174762518708102776e-01, 1.613371421711606024e-01, + 1.696138475604022922e-01, 6.339280343903017645e-01, 1.733608592737586884e-01, 2.309725877555647913e-02, + 1.696138475603946871e-01, 1.733608592737317933e-01, 6.339280343903569426e-01, 2.309725877556989895e-02, + 1.733608592736602116e-01, 2.434102832026447880e-02, 6.335875653142608588e-01, 1.650530553465074934e-01, + 1.770183510189466958e-01, 2.434102832027353058e-02, 1.650530553463749051e-01, 6.335875653143980824e-01, + 1.770183510189176912e-01, 1.650530553463994132e-01, 2.935511686037215948e-02, 1.740037654545786017e-01, + 6.208415104357217773e-01, 1.740037654546278123e-01, 1.757996072492150896e-01, 6.208415104357764003e-01, + 1.757996072493022977e-01, 2.935511686041814006e-02, 6.208415104356415082e-01, 1.121243864649791033e-01, + 5.325889236500517265e-01, 2.362419588457611019e-01, 1.190447310392824948e-01, 1.121243864650229988e-01, + 2.362419588456987907e-01, 5.325889236499357082e-01, 1.190447310392760000e-01, 2.362419588456607933e-01, + 1.193225934917415965e-01, 4.534859243589937083e-01, 1.285563923599583047e-01, 2.986350897893351175e-01, + 1.193225934919169978e-01, 1.285563923597632940e-01, 4.534859243587894828e-01, 2.986350897893639833e-01, + 1.285563923599528091e-01, 3.020019073266451137e-01, 3.283455214738412731e-01, 3.030261289420949078e-01, + 3.283455214738281169e-01, 6.662644225740220472e-02, 3.030261289418548776e-01, 6.662644225731254033e-02, + 3.020019073267440901e-01, 3.030261289420781989e-01, 5.159168166142744649e-01, 1.324670107447714906e-01, + 1.074464615850584948e-01, 1.324670107446420941e-01, 2.441697110559039874e-01, 1.074464615851540988e-01, + 2.441697110556645955e-01, 5.159168166144414425e-01, 1.074464615851919019e-01, 3.182062008203240255e-01, + 3.182062008205924220e-01, 4.538139753862874964e-02, 1.207131116358086953e-01, 3.395130626211381175e-01, + 4.110926574948671197e-01, 3.395130626209016955e-01, 1.286811682481305008e-01, 4.110926574949583245e-01, + 1.286811682481935060e-01, 1.207131116358556022e-01, 4.110926574950111156e-01, 2.570719807624278208e-01, + 2.570719807625507225e-01, 2.287840577124626018e-01 +}; +inline constexpr double tetrahedron_o10_weights[74] = { + 6.365271938317999887e-05, 1.316831857909543505e-04, 1.316831857919753166e-04, 1.316831857927753494e-04, + 2.820943065241141826e-04, 2.820943065245265047e-04, 2.820943065259753240e-04, 1.023192373304246264e-03, + 1.023192373307521205e-03, 1.023192373307959873e-03, 1.103390983747504781e-03, 1.103390983748027366e-03, + 1.103390983748753782e-03, 1.164160167499589957e-03, 1.164160167500396820e-03, 1.164160167500605204e-03, + 1.191461909106608999e-03, 1.191461909106978278e-03, 1.191461909107789695e-03, 1.368674554135069056e-03, + 1.368674554136908731e-03, 1.368674554137219246e-03, 1.522996656033314029e-03, 1.522996656034059960e-03, + 1.522996656034905421e-03, 1.695831015223938287e-03, 1.695831015224264849e-03, 1.695831015225120067e-03, + 1.873698558482353276e-03, 1.873698558483606613e-03, 1.873698558484433209e-03, 1.898703097503021782e-03, + 1.898703097503796552e-03, 1.898703097504038330e-03, 1.902828894582963117e-03, 1.959184315344778601e-03, + 1.969493864818561927e-03, 1.969493864819973124e-03, 1.969493864825328216e-03, 2.064660777657845083e-03, + 2.064660777661681424e-03, 2.064660777662241740e-03, 2.083913166714165214e-03, 2.083913166715284978e-03, + 2.083913166716659746e-03, 2.094685984369893100e-03, 2.094685984373114915e-03, 2.094685984375275079e-03, + 2.191419694439080031e-03, 2.191419694439428276e-03, 2.191419694440678145e-03, 2.284450780288395030e-03, + 2.284450780289186498e-03, 2.284450780289794952e-03, 2.777404909175530245e-03, 2.777404909176179899e-03, + 2.777404909179498425e-03, 3.445098889335028100e-03, 3.445098889341516400e-03, 3.445098889342071511e-03, + 4.234512641858648001e-03, 4.234512641859506689e-03, 4.234512641860642933e-03, 4.471157472950426127e-03, + 4.471157472951478237e-03, 4.471157472952013399e-03, 4.553883199161186564e-03, 4.553883199161758155e-03, + 4.553883199163368846e-03, 4.651927948037352760e-03, 4.682093742486136828e-03, 4.682093742490215163e-03, + 4.682093742491071249e-03, 7.763086997403201468e-03 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/generated/quadrature_tables_triangle.h b/cpp/src/generated/quadrature_tables_triangle.h new file mode 100644 index 0000000..728d4e9 --- /dev/null +++ b/cpp/src/generated/quadrature_tables_triangle.h @@ -0,0 +1,181 @@ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix 0.11.0.dev0 quadrature rules for 'triangle', orders 1..10. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py 10 +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{ +// ---- order 1: 1 point(s), tdim=2 ---- +inline constexpr int triangle_o1_npts = 1; +inline constexpr int triangle_o1_tdim = 2; +inline constexpr double triangle_o1_points[2] = { + 3.333333333333333148e-01, 3.333333333333333148e-01 +}; +inline constexpr double triangle_o1_weights[1] = { + 5.000000000000000000e-01 +}; + +// ---- order 2: 3 point(s), tdim=2 ---- +inline constexpr int triangle_o2_npts = 3; +inline constexpr int triangle_o2_tdim = 2; +inline constexpr double triangle_o2_points[6] = { + 1.666666666666666574e-01, 1.666666666666666574e-01, 1.666666666666666574e-01, 6.666666666666666297e-01, + 6.666666666666666297e-01, 1.666666666666666574e-01 +}; +inline constexpr double triangle_o2_weights[3] = { + 1.666666666666666574e-01, 1.666666666666666574e-01, 1.666666666666666574e-01 +}; + +// ---- order 3: 6 point(s), tdim=2 ---- +inline constexpr int triangle_o3_npts = 6; +inline constexpr int triangle_o3_tdim = 2; +inline constexpr double triangle_o3_points[12] = { + 6.590276223740919592e-01, 2.319333685530310107e-01, 6.590276223740919592e-01, 1.090390090728770023e-01, + 2.319333685530310107e-01, 6.590276223740919592e-01, 2.319333685530310107e-01, 1.090390090728770023e-01, + 1.090390090728770023e-01, 6.590276223740919592e-01, 1.090390090728770023e-01, 2.319333685530310107e-01 +}; +inline constexpr double triangle_o3_weights[6] = { + 8.333333333333332871e-02, 8.333333333333332871e-02, 8.333333333333332871e-02, 8.333333333333332871e-02, + 8.333333333333332871e-02, 8.333333333333332871e-02 +}; + +// ---- order 4: 6 point(s), tdim=2 ---- +inline constexpr int triangle_o4_npts = 6; +inline constexpr int triangle_o4_tdim = 2; +inline constexpr double triangle_o4_points[12] = { + 8.168475729804589580e-01, 9.157621350977100672e-02, 9.157621350977100672e-02, 8.168475729804589580e-01, + 9.157621350977100672e-02, 9.157621350977100672e-02, 1.081030181680699975e-01, 4.459484909159650012e-01, + 4.459484909159650012e-01, 1.081030181680699975e-01, 4.459484909159650012e-01, 4.459484909159650012e-01 +}; +inline constexpr double triangle_o4_weights[6] = { + 5.497587182766099784e-02, 5.497587182766099784e-02, 5.497587182766099784e-02, 1.116907948390055000e-01, + 1.116907948390055000e-01, 1.116907948390055000e-01 +}; + +// ---- order 5: 7 point(s), tdim=2 ---- +inline constexpr int triangle_o5_npts = 7; +inline constexpr int triangle_o5_tdim = 2; +inline constexpr double triangle_o5_points[14] = { + 3.333333333333333148e-01, 3.333333333333333148e-01, 7.974269853530872032e-01, 1.012865073234563290e-01, + 1.012865073234563290e-01, 7.974269853530872032e-01, 1.012865073234563290e-01, 1.012865073234563290e-01, + 5.971587178976980881e-02, 4.701420641051150540e-01, 4.701420641051150540e-01, 5.971587178976980881e-02, + 4.701420641051150540e-01, 4.701420641051150540e-01 +}; +inline constexpr double triangle_o5_weights[7] = { + 1.125000000000000028e-01, 6.296959027241358364e-02, 6.296959027241358364e-02, 6.296959027241358364e-02, + 6.619707639425308210e-02, 6.619707639425308210e-02, 6.619707639425308210e-02 +}; + +// ---- order 6: 12 point(s), tdim=2 ---- +inline constexpr int triangle_o6_npts = 12; +inline constexpr int triangle_o6_tdim = 2; +inline constexpr double triangle_o6_points[24] = { + 8.738219710169959908e-01, 6.308901449150200458e-02, 6.308901449150200458e-02, 8.738219710169959908e-01, + 6.308901449150200458e-02, 6.308901449150200458e-02, 5.014265096581790315e-01, 2.492867451709100124e-01, + 2.492867451709100124e-01, 5.014265096581790315e-01, 2.492867451709100124e-01, 2.492867451709100124e-01, + 6.365024991213990013e-01, 3.103524510337850040e-01, 6.365024991213990013e-01, 5.314504984481600164e-02, + 3.103524510337850040e-01, 6.365024991213990013e-01, 3.103524510337850040e-01, 5.314504984481600164e-02, + 5.314504984481600164e-02, 6.365024991213990013e-01, 5.314504984481600164e-02, 3.103524510337850040e-01 +}; +inline constexpr double triangle_o6_weights[12] = { + 2.542245318510349961e-02, 2.542245318510349961e-02, 2.542245318510349961e-02, 5.839313786318949678e-02, + 5.839313786318949678e-02, 5.839313786318949678e-02, 4.142553780918700052e-02, 4.142553780918700052e-02, + 4.142553780918700052e-02, 4.142553780918700052e-02, 4.142553780918700052e-02, 4.142553780918700052e-02 +}; + +// ---- order 7: 15 point(s), tdim=2 ---- +inline constexpr int triangle_o7_npts = 15; +inline constexpr int triangle_o7_tdim = 2; +inline constexpr double triangle_o7_points[30] = { + 4.731956536892510390e-01, 4.731956536892510390e-01, 5.779764005450649389e-02, 5.779764005450649389e-02, + 2.416636063972474324e-01, 2.416636063972474324e-01, 4.731956536892510390e-01, 5.360869262149792203e-02, + 5.779764005450649389e-02, 8.844047198909870122e-01, 2.416636063972474324e-01, 5.166727872055051352e-01, + 5.360869262149792203e-02, 4.731956536892510390e-01, 8.844047198909870122e-01, 5.779764005450649389e-02, + 5.166727872055051352e-01, 2.416636063972474324e-01, 4.697120613008553408e-02, 2.593390118657856735e-01, + 6.936897820041287854e-01, 4.697120613008553408e-02, 2.593390118657856735e-01, 6.936897820041287854e-01, + 2.593390118657856735e-01, 4.697120613008553408e-02, 6.936897820041287854e-01, 2.593390118657856735e-01, + 4.697120613008553408e-02, 6.936897820041287854e-01 +}; +inline constexpr double triangle_o7_weights[15] = { + 2.659041664838023011e-02, 2.045908519702843362e-02, 6.386262428056692364e-02, 2.659041664838023011e-02, + 2.045908519702843362e-02, 6.386262428056692364e-02, 2.659041664838023011e-02, 2.045908519702843362e-02, + 6.386262428056692364e-02, 2.787727027034554717e-02, 2.787727027034554717e-02, 2.787727027034554717e-02, + 2.787727027034554717e-02, 2.787727027034554717e-02, 2.787727027034554717e-02 +}; + +// ---- order 8: 16 point(s), tdim=2 ---- +inline constexpr int triangle_o8_npts = 16; +inline constexpr int triangle_o8_tdim = 2; +inline constexpr double triangle_o8_points[32] = { + 3.333333333333333148e-01, 3.333333333333333148e-01, 1.705693077517602685e-01, 1.705693077517602685e-01, + 4.592925882927231251e-01, 4.592925882927231251e-01, 5.054722831703106767e-02, 5.054722831703106767e-02, + 1.705693077517602685e-01, 6.588613844964794630e-01, 4.592925882927231251e-01, 8.141482341455374971e-02, + 5.054722831703106767e-02, 8.989055433659378647e-01, 6.588613844964794630e-01, 1.705693077517602685e-01, + 8.141482341455374971e-02, 4.592925882927231251e-01, 8.989055433659378647e-01, 5.054722831703106767e-02, + 8.394777409957674547e-03, 2.631128296346380568e-01, 7.284923929554043553e-01, 8.394777409957674547e-03, + 2.631128296346380568e-01, 7.284923929554043553e-01, 2.631128296346380568e-01, 8.394777409957674547e-03, + 7.284923929554043553e-01, 2.631128296346380568e-01, 8.394777409957674547e-03, 7.284923929554043553e-01 +}; +inline constexpr double triangle_o8_weights[16] = { + 7.215780383889359995e-02, 5.160868526735912232e-02, 4.754581713364231660e-02, 1.622924881159903965e-02, + 5.160868526735912232e-02, 4.754581713364231660e-02, 1.622924881159903965e-02, 5.160868526735912232e-02, + 4.754581713364231660e-02, 1.622924881159903965e-02, 1.361515708721749811e-02, 1.361515708721749811e-02, + 1.361515708721749811e-02, 1.361515708721749811e-02, 1.361515708721749811e-02, 1.361515708721749811e-02 +}; + +// ---- order 9: 19 point(s), tdim=2 ---- +inline constexpr int triangle_o9_npts = 19; +inline constexpr int triangle_o9_tdim = 2; +inline constexpr double triangle_o9_points[38] = { + 3.333333333333333148e-01, 3.333333333333333148e-01, 4.896825191987376202e-01, 4.896825191987376202e-01, + 1.882035356190328024e-01, 1.882035356190328024e-01, 4.370895914929366355e-01, 4.370895914929366355e-01, + 4.472951339445274677e-02, 4.472951339445274677e-02, 4.896825191987376202e-01, 2.063496160252475953e-02, + 1.882035356190328024e-01, 6.235929287619343953e-01, 4.370895914929366355e-01, 1.258208170141267290e-01, + 4.472951339445274677e-02, 9.105409732110945065e-01, 2.063496160252475953e-02, 4.896825191987376202e-01, + 6.235929287619343953e-01, 1.882035356190328024e-01, 1.258208170141267290e-01, 4.370895914929366355e-01, + 9.105409732110945065e-01, 4.472951339445274677e-02, 3.683841205473629976e-02, 2.219629891607657057e-01, + 7.411985987844980084e-01, 3.683841205473629976e-02, 2.219629891607657057e-01, 7.411985987844980084e-01, + 2.219629891607657057e-01, 3.683841205473629976e-02, 7.411985987844980084e-01, 2.219629891607657057e-01, + 3.683841205473629976e-02, 7.411985987844980084e-01 +}; +inline constexpr double triangle_o9_weights[19] = { + 4.856789814139941819e-02, 1.566735011356953575e-02, 3.982386946360513130e-02, 3.891377050238713914e-02, + 1.278883782934901736e-02, 1.566735011356953575e-02, 3.982386946360513130e-02, 3.891377050238713914e-02, + 1.278883782934901736e-02, 1.566735011356953575e-02, 3.982386946360513130e-02, 3.891377050238713914e-02, + 1.278883782934901736e-02, 2.164176968864468809e-02, 2.164176968864468809e-02, 2.164176968864468809e-02, + 2.164176968864468809e-02, 2.164176968864468809e-02, 2.164176968864468809e-02 +}; + +// ---- order 10: 25 point(s), tdim=2 ---- +inline constexpr int triangle_o10_npts = 25; +inline constexpr int triangle_o10_tdim = 2; +inline constexpr double triangle_o10_points[50] = { + 3.333333333333333148e-01, 3.333333333333333148e-01, 4.951734598011705013e-01, 4.951734598011705013e-01, + 1.913941524284129558e-02, 1.913941524284129558e-02, 1.844850126852465300e-01, 1.844850126852465300e-01, + 4.282348209437188413e-01, 4.282348209437188413e-01, 4.951734598011705013e-01, 9.653080397658997391e-03, + 1.913941524284129558e-02, 9.617211695143174088e-01, 1.844850126852465300e-01, 6.310299746295069401e-01, + 4.282348209437188413e-01, 1.435303581125623174e-01, 9.653080397658997391e-03, 4.951734598011705013e-01, + 9.617211695143174088e-01, 1.913941524284129558e-02, 6.310299746295069401e-01, 1.844850126852465300e-01, + 1.435303581125623174e-01, 4.282348209437188413e-01, 3.472362048232748022e-02, 1.337347551008691293e-01, + 3.758272734119168929e-02, 3.266931362813368933e-01, 8.315416244168034599e-01, 3.472362048232748022e-02, + 6.357241363774713827e-01, 3.758272734119168929e-02, 1.337347551008691293e-01, 8.315416244168034599e-01, + 3.266931362813368933e-01, 6.357241363774713827e-01, 1.337347551008691293e-01, 3.472362048232748022e-02, + 3.266931362813368933e-01, 3.758272734119168929e-02, 8.315416244168034599e-01, 1.337347551008691293e-01, + 6.357241363774713827e-01, 3.266931362813368933e-01, 3.472362048232748022e-02, 8.315416244168034599e-01, + 3.758272734119168929e-02, 6.357241363774713827e-01 +}; +inline constexpr double triangle_o10_weights[25] = { + 4.180743718698696348e-02, 4.896295249209151740e-03, 3.192679615059327170e-03, 3.931688487318863584e-02, + 3.762366398427199193e-02, 4.896295249209151740e-03, 3.192679615059327170e-03, 3.931688487318863584e-02, + 3.762366398427199193e-02, 4.896295249209151740e-03, 3.192679615059327170e-03, 3.931688487318863584e-02, + 3.762366398427199193e-02, 1.448114073162817123e-02, 1.936952454300945245e-02, 1.448114073162817123e-02, + 1.936952454300945245e-02, 1.448114073162817123e-02, 1.936952454300945245e-02, 1.448114073162817123e-02, + 1.936952454300945245e-02, 1.448114073162817123e-02, 1.936952454300945245e-02, 1.448114073162817123e-02, + 1.936952454300945245e-02 +}; + +} // namespace cutcells::quadrature::generated diff --git a/cpp/src/mapping.cpp b/cpp/src/mapping.cpp new file mode 100644 index 0000000..531d6d1 --- /dev/null +++ b/cpp/src/mapping.cpp @@ -0,0 +1,367 @@ +// Copyright (c) 2024 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT + +#include "mapping.h" + +#include +#include +#include +#include + +namespace cutcells::cell +{ + +namespace +{ + // ------------------------------------------------------------------------- + // Per-cell-type helpers + // ------------------------------------------------------------------------- + + /// Number of first-order vertices per parent cell type (VTK convention). + inline int num_parent_vertices(type cell_type) + { + switch (cell_type) + { + case type::interval: return 2; + case type::triangle: return 3; + case type::tetrahedron: return 4; + case type::quadrilateral: return 4; + case type::hexahedron: return 8; + case type::prism: return 6; + case type::pyramid: return 5; + default: + throw std::invalid_argument("mapping: unsupported parent cell type"); + } + } + + // jacobian_col_indices is now a public inline in mapping.h + + // ------------------------------------------------------------------------- + // Small dense matrix-vector multiply y = J * x + // J is stored column-major: col k occupies J[k*gdim .. (k+1)*gdim - 1] + // ------------------------------------------------------------------------- + + template + inline void mat_vec_1x1(const T* J, const T* x, T* y) + { + y[0] = J[0] * x[0]; + } + + template + inline void mat_vec_2x2(const T* J, const T* x, T* y) + { + // J = | J[0] J[2] | + // | J[1] J[3] | + y[0] = J[0]*x[0] + J[2]*x[1]; + y[1] = J[1]*x[0] + J[3]*x[1]; + } + + template + inline void mat_vec_3x3(const T* J, const T* x, T* y) + { + // J = | J[0] J[3] J[6] | + // | J[1] J[4] J[7] | + // | J[2] J[5] J[8] | + y[0] = J[0]*x[0] + J[3]*x[1] + J[6]*x[2]; + y[1] = J[1]*x[0] + J[4]*x[1] + J[7]*x[2]; + y[2] = J[2]*x[0] + J[5]*x[1] + J[8]*x[2]; + } + + // ------------------------------------------------------------------------- + // Solve J * y = x for y (J column-major, same layout as above) + // ------------------------------------------------------------------------- + + template + inline void solve_1x1(const T* J, const T* x, T* y) + { + y[0] = x[0] / J[0]; + } + + template + inline void solve_2x2(const T* J, const T* x, T* y) + { + // J = | a b | det = a*d - b*c + // | c d | + // column-major: a=J[0] c=J[1] b=J[2] d=J[3] + const T a = J[0], c = J[1], b = J[2], d = J[3]; + const T inv_det = T(1) / (a*d - b*c); + y[0] = inv_det * ( d*x[0] - b*x[1]); + y[1] = inv_det * (-c*x[0] + a*x[1]); + } + + template + inline void solve_3x3(const T* J, const T* x, T* y) + { + // Column-major layout: + // J = | J[0] J[3] J[6] | + // | J[1] J[4] J[7] | + // | J[2] J[5] J[8] | + // + // Cofactors of column 0 (used for det and first row of J^{-1}): + const T C00 = J[4]*J[8] - J[7]*J[5]; // cof(0,0) + const T C10 = -(J[3]*J[8] - J[6]*J[5]); // cof(1,0) + const T C20 = J[3]*J[7] - J[6]*J[4]; // cof(2,0) + + const T det = J[0]*C00 + J[1]*C10 + J[2]*C20; + const T inv_det = T(1) / det; + + // Remaining cofactors: + const T C01 = -(J[1]*J[8] - J[7]*J[2]); // cof(0,1) + const T C11 = J[0]*J[8] - J[6]*J[2]; // cof(1,1) + const T C21 = -(J[0]*J[7] - J[6]*J[1]); // cof(2,1) + const T C02 = J[1]*J[5] - J[4]*J[2]; // cof(0,2) + const T C12 = -(J[0]*J[5] - J[3]*J[2]); // cof(1,2) + const T C22 = J[0]*J[4] - J[3]*J[1]; // cof(2,2) + + // J^{-1}[row i, col j] = cof(j, i) / det + // y = J^{-1} * x + y[0] = inv_det * (C00*x[0] + C10*x[1] + C20*x[2]); + y[1] = inv_det * (C01*x[0] + C11*x[1] + C21*x[2]); + y[2] = inv_det * (C02*x[0] + C12*x[1] + C22*x[2]); + } + + // ------------------------------------------------------------------------- + // Build the column-major Jacobian matrix (gdim x gdim) for the affine map. + // J_out must point to storage for at least gdim*gdim values. + // ------------------------------------------------------------------------- + template + void build_jacobian(type parent_type, + const std::vector& pv, + int gdim, + T* J_out) + { + const auto cols = jacobian_col_indices(parent_type); + const T* v0 = pv.data(); + for (int col = 0; col < gdim; ++col) + { + const T* vi = pv.data() + cols[col] * gdim; + for (int row = 0; row < gdim; ++row) + J_out[col * gdim + row] = vi[row] - v0[row]; + } + } + +} // anonymous namespace + +// ============================================================================= +// Public API implementations +// ============================================================================= + +template +void push_forward_affine(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span X_ref, + std::span x_phys) +{ + assert(X_ref.size() == x_phys.size()); + const int n = static_cast(X_ref.size()) / gdim; + const T* x0 = parent_vertex_coords.data(); + + T J[9] = {}; + build_jacobian(parent_type, parent_vertex_coords, gdim, J); + + if (gdim == 1) + { + for (int i = 0; i < n; ++i) + { + T y[1]; + mat_vec_1x1(J, X_ref.data() + i, y); + x_phys[i] = x0[0] + y[0]; + } + } + else if (gdim == 2) + { + for (int i = 0; i < n; ++i) + { + T y[2]; + mat_vec_2x2(J, X_ref.data() + 2*i, y); + x_phys[2*i ] = x0[0] + y[0]; + x_phys[2*i + 1] = x0[1] + y[1]; + } + } + else // gdim == 3 + { + for (int i = 0; i < n; ++i) + { + T y[3]; + mat_vec_3x3(J, X_ref.data() + 3*i, y); + x_phys[3*i ] = x0[0] + y[0]; + x_phys[3*i + 1] = x0[1] + y[1]; + x_phys[3*i + 2] = x0[2] + y[2]; + } + } +} + +template +void pull_back_affine(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span x_phys, + std::span X_ref) +{ + assert(x_phys.size() == X_ref.size()); + const int n = static_cast(x_phys.size()) / gdim; + const T* x0 = parent_vertex_coords.data(); + + T J[9] = {}; + build_jacobian(parent_type, parent_vertex_coords, gdim, J); + + if (gdim == 1) + { + for (int i = 0; i < n; ++i) + { + T rhs = x_phys[i] - x0[0]; + solve_1x1(J, &rhs, X_ref.data() + i); + } + } + else if (gdim == 2) + { + T rhs[2]; + for (int i = 0; i < n; ++i) + { + rhs[0] = x_phys[2*i ] - x0[0]; + rhs[1] = x_phys[2*i + 1] - x0[1]; + solve_2x2(J, rhs, X_ref.data() + 2*i); + } + } + else // gdim == 3 + { + T rhs[3]; + for (int i = 0; i < n; ++i) + { + rhs[0] = x_phys[3*i ] - x0[0]; + rhs[1] = x_phys[3*i + 1] - x0[1]; + rhs[2] = x_phys[3*i + 2] - x0[2]; + solve_3x3(J, rhs, X_ref.data() + 3*i); + } + } +} + +template +void compute_physical_cut_vertices(CutCell& cut_cell) +{ + const int gdim = cut_cell._gdim; + const int n_verts = static_cast(cut_cell._vertex_coords.size()) / gdim; + cut_cell._vertex_coords_phys.resize(n_verts * gdim); + + push_forward_affine(cut_cell._parent_cell_type, + cut_cell._parent_vertex_coords, + gdim, + std::span(cut_cell._vertex_coords), + std::span(cut_cell._vertex_coords_phys)); +} + +template +void complete_from_physical(CutCell& cut_cell) +{ + const int gdim = cut_cell._gdim; + const int n_verts = static_cast(cut_cell._vertex_coords.size()) / gdim; + + // Step 1: move the raw physical output from the cutter into _vertex_coords_phys. + cut_cell._vertex_coords_phys = std::move(cut_cell._vertex_coords); + + // Step 2: pull back the physical coords to reference space and store in _vertex_coords. + cut_cell._vertex_coords.resize(n_verts * gdim); + pull_back_affine(cut_cell._parent_cell_type, + cut_cell._parent_vertex_coords, + gdim, + std::span(cut_cell._vertex_coords_phys), + std::span(cut_cell._vertex_coords)); +} + +// ============================================================================= +// affine_volume_factor +// ============================================================================= + +template +T affine_volume_factor(type cell_type, const T* phys_verts, int gdim) +{ + const int tdim = get_tdim(cell_type); + const auto cols = jacobian_col_indices(cell_type); + const T* v0 = phys_verts; + + if (tdim == gdim) + { + // Square Jacobian — build column-major gdim×gdim matrix and take |det|. + T J[9] = {}; + for (int c = 0; c < tdim; ++c) + { + const T* vi = phys_verts + cols[c] * gdim; + for (int r = 0; r < gdim; ++r) + J[c * gdim + r] = vi[r] - v0[r]; + } + if (gdim == 1) + { + return std::abs(J[0]); + } + else if (gdim == 2) + { + // det of 2×2 column-major J: col0=(J[0],J[1]), col1=(J[2],J[3]) + const T det = J[0]*J[3] - J[2]*J[1]; + return std::abs(det); + } + else // gdim == 3 + { + // det of 3×3 column-major J by cofactor expansion along first row + const T det = J[0]*(J[4]*J[8] - J[7]*J[5]) + - J[1]*(J[3]*J[8] - J[6]*J[5]) + + J[2]*(J[3]*J[7] - J[6]*J[4]); + return std::abs(det); + } + } + else if (tdim == 1) + { + // Embedded edge: J is a single column vector of length gdim. + const T* vi = phys_verts + cols[0] * gdim; + T norm2 = T(0); + for (int r = 0; r < gdim; ++r) + { + const T d = vi[r] - v0[r]; + norm2 += d * d; + } + return std::sqrt(norm2); + } + else if (tdim == 2 && gdim == 3) + { + // Embedded surface: J has two columns (3-vectors). Volume factor = |j0 × j1|. + const T* vi0 = phys_verts + cols[0] * gdim; + const T* vi1 = phys_verts + cols[1] * gdim; + const T j0[3] = {vi0[0]-v0[0], vi0[1]-v0[1], vi0[2]-v0[2]}; + const T j1[3] = {vi1[0]-v0[0], vi1[1]-v0[1], vi1[2]-v0[2]}; + const T cx = j0[1]*j1[2] - j0[2]*j1[1]; + const T cy = j0[2]*j1[0] - j0[0]*j1[2]; + const T cz = j0[0]*j1[1] - j0[1]*j1[0]; + return std::sqrt(cx*cx + cy*cy + cz*cz); + } + else + { + throw std::invalid_argument("affine_volume_factor: unsupported tdim/gdim combination"); + } +} + +// ============================================================================= +// Explicit instantiations for double and float +// ============================================================================= +template void push_forward_affine(type, const std::vector&, int, + std::span, std::span); +template void push_forward_affine(type, const std::vector&, int, + std::span, std::span); + +template void pull_back_affine(type, const std::vector&, int, + std::span, std::span); +template void pull_back_affine(type, const std::vector&, int, + std::span, std::span); + +template void compute_physical_cut_vertices(CutCell&); +template void compute_physical_cut_vertices(CutCell&); + +template void complete_from_physical(CutCell&); +template void complete_from_physical(CutCell&); + +template double affine_volume_factor(type, const double*, int); +template float affine_volume_factor(type, const float*, int); + +} // namespace cutcells::cell diff --git a/cpp/src/mapping.h b/cpp/src/mapping.h new file mode 100644 index 0000000..466a720 --- /dev/null +++ b/cpp/src/mapping.h @@ -0,0 +1,140 @@ +// Copyright (c) 2024 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "cut_cell.h" +#include "cell_types.h" + +#include +#include +#include +#include +#include + +namespace cutcells::cell +{ + +/// @brief Indices into parent_vertex_coords for the Jacobian columns. +/// +/// Column k of J = parent_vertex[ col[k] ] - parent_vertex[0]. +/// Up to tdim columns; unused entries are -1. +/// +/// Choices follow VTK first-order vertex ordering so that J is diagonal +/// (and thus the identity) for the unit reference cell. +/// +/// interval: v0=0 v1=1 → col[0]=1 +/// triangle: v0=(0,0) v1=(1,0) v2=(0,1) → col={1,2} +/// tetrahedron: v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(0,0,1) → col={1,2,3} +/// quadrilateral: v0=(0,0) v1=(1,0) v2=(1,1) v3=(0,1) → col={1,3} +/// hexahedron: v0=(0,0,0) v1=(1,0,0) v3=(0,1,0) v4=(0,0,1) → col={1,3,4} +/// prism: v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(0,0,1) → col={1,2,3} +/// pyramid: v0=(0,0,0) v1=(1,0,0) v3=(0,1,0) v4=apex → col={1,3,4} +inline std::array jacobian_col_indices(type cell_type) +{ + switch (cell_type) + { + case type::interval: return {1, -1, -1}; + case type::triangle: return {1, 2, -1}; + case type::tetrahedron: return {1, 2, 3}; + case type::quadrilateral: return {1, 3, -1}; + case type::hexahedron: return {1, 3, 4}; + case type::prism: return {1, 2, 3}; + case type::pyramid: return {1, 3, 4}; + default: + throw std::invalid_argument("mapping: unsupported parent cell type"); + } +} + +/// @brief Compute the affine volume scaling factor |det J| (or √det(JᵀJ) for +/// embedded cells) for a single cell. +/// +/// For volume cells (tdim == gdim) returns |det J|, the absolute value of +/// the Jacobian determinant of the affine map from the reference element to +/// physical space. +/// +/// For surface/embedded cells (tdim < gdim) returns the Gramian root +/// √(det(JᵀJ)) which gives the correct area / length scaling factor. +/// +/// @param cell_type VTK cell type +/// @param phys_verts flat physical vertex coordinates in VTK ordering +/// (nv * gdim values) +/// @param gdim geometric dimension of the embedding space (1, 2, or 3) +/// @returns volume scaling factor ≥ 0 +template +T affine_volume_factor(type cell_type, const T* phys_verts, int gdim); + +/// @brief Push all cut vertices from parent reference space into physical space. +/// +/// Uses the affine map x_phys = x0 + J * X_ref where J is built from the +/// first-order parent cell vertex differences (VTK ordering) and x0 is the first +/// parent physical vertex. +/// +/// Precondition : _vertex_coords holds reference coordinates in the parent +/// reference cell; _parent_vertex_coords and _parent_cell_type +/// are set. +/// Postcondition : _vertex_coords_phys is allocated and filled. +/// +/// Note: supported for gdim == topological dimension of parent (volume cells). +template +void compute_physical_cut_vertices(CutCell& cut_cell); + +/// @brief Complete a cut cell that was computed in physical space. +/// +/// The cutting routine wrote physical coordinates into _vertex_coords. +/// This function: +/// 1. moves _vertex_coords → _vertex_coords_phys +/// 2. pulls _vertex_coords_phys back through J * X = x - x0 to overwrite +/// _vertex_coords with the reference coordinates. +/// +/// After this call _vertex_coords is always in reference space and +/// _vertex_coords_phys is always in physical space. +/// +/// Precondition : _vertex_coords holds raw physical coordinates from the cutter; +/// _parent_vertex_coords and _parent_cell_type are set. +/// Postcondition : _vertex_coords = reference coordinates (parent ref. space) +/// _vertex_coords_phys = physical coordinates +/// +/// Note: supported for gdim == topological dimension of parent (volume cells). +template +void complete_from_physical(CutCell& cut_cell); + +/// @brief Affine push-forward for a batch of n reference points. +/// +/// Maps X_ref (flat, n*gdim) from parent reference space to physical space +/// x_phys (flat, n*gdim) via x = x0 + J * X where J is derived from +/// parent_vertex_coords and parent_type in VTK vertex ordering. +/// +/// @param parent_type cell type of the parent element +/// @param parent_vertex_coords flat physical vertex coords in VTK ordering +/// @param gdim geometric / topological dimension (must equal +/// the topological dimension of parent_type) +/// @param X_ref flat input: n * gdim reference coordinates +/// @param x_phys flat output: n * gdim physical coordinates +template +void push_forward_affine(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span X_ref, + std::span x_phys); + +/// @brief Affine pullback for a batch of n physical points. +/// +/// Maps x_phys (flat, n*gdim) from physical space to parent reference space +/// X_ref (flat, n*gdim) by solving J * X = x - x0. +/// +/// @param parent_type cell type of the parent element +/// @param parent_vertex_coords flat physical vertex coords in VTK ordering +/// @param gdim geometric / topological dimension +/// @param x_phys flat input: n * gdim physical coordinates +/// @param X_ref flat output: n * gdim reference coordinates +template +void pull_back_affine(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span x_phys, + std::span X_ref); + +} // namespace cutcells::cell diff --git a/cpp/src/quadrature.cpp b/cpp/src/quadrature.cpp new file mode 100644 index 0000000..a6cea0d --- /dev/null +++ b/cpp/src/quadrature.cpp @@ -0,0 +1,685 @@ +// Copyright (c) 2024 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT + +#include "quadrature.h" +#include "quadrature_tables.h" +#include "cut_cell.h" +#include "triangulation.h" +#include "cell_flags.h" +#include "mapping.h" + +#include +#include +#include + +namespace cutcells::quadrature +{ + +// ============================================================================= +// Degree-1 shape functions and their Jacobians on canonical cut-subcell types. +// +// For each subcell type the canonical reference vertices are (Basix ordering): +// +// interval: v0=(0), v1=(1) +// triangle: v0=(0,0), v1=(1,0), v2=(0,1) +// tetrahedron: v0=(0,0,0), v1=(1,0,0), v2=(0,1,0), v3=(0,0,1) +// +// Degree-1 shape functions on the canonical cell: +// interval: N0=1-s, N1=s +// triangle: N0=1-s-t, N1=s, N2=t +// tetrahedron: N0=1-s-t-u, N1=s, N2=t, N3=u +// +// The mapped point at canonical coordinate (s,[t,[u]]) in an actual subcell +// with real vertices V0..Vn is: +// x = sum_i N_i(s,t,...) * Vi +// +// The Jacobian of that map is: +// J = [V1-V0, V2-V0, ...] (column-major, one column per coordinate) +// +// Note: quad, hex, prism, pyramid subcells do NOT appear in the CutCells +// output (the library always triangulates cut regions into simplices). +// We only need interval, triangle, tetrahedron. +// ============================================================================= + +namespace +{ + +// --------------------------------------------------------------------------- +// Map a batch of n canonical quadrature points to actual subcell coordinates. +// +// can_pts : n * tdim canonical reference points (flat row-major) +// verts : nv * gdim actual subcell vertex coordinates (flat row-major, +// positions in whichever space we are mapping in) +// cell_type : subcell type (determines basis) +// out_pts : n * gdim output points (flat row-major) +// +// For interval subcells embedded in higher-dimensional space the "gdim" output +// dimension follows the vertex dimension; tdim == 1 but vertices live in gdim. +// --------------------------------------------------------------------------- +template +void map_canonical_to_subcell(const T* can_pts, int n, + int tdim, + const T* verts, int gdim, + cell::type cell_type, + T* out_pts) +{ + // N_i are standard Lagrange-1 basis functions on the canonical simplex. + // J = [v1-v0 | v2-v0 | ...] (each column = vertex difference) + // + // x = v0 + J * can_pt + // + // This is identical to the affine push-forward used in mapping.cpp but + // here the "parent" is the canonical subcell itself. + + const T* v0 = verts; + + for (int q = 0; q < n; ++q) + { + const T* X = can_pts + q * tdim; + T* x = out_pts + q * gdim; + + // Start at v0 + for (int d = 0; d < gdim; ++d) + x[d] = v0[d]; + + // Add contributions from each basis function N_i = X[i-1], i >= 1 + for (int i = 1; i <= tdim; ++i) + { + const T* vi = verts + i * gdim; + for (int d = 0; d < gdim; ++d) + x[d] += X[i - 1] * (vi[d] - v0[d]); + } + } +} + +// --------------------------------------------------------------------------- +// Compute |det J| for the simplex spanned by subcell vertices. +// +// verts: nv * gdim (row-major) +// cell_type: interval(1D→1D/2D/3D), triangle(2D→2D/3D), tetrahedron(3D→3D) +// +// For embedded simplices (tdim < gdim) we compute the Gramian determinant +// sqrt(det(J^T J)). +// --------------------------------------------------------------------------- +template +T simplex_physical_volume_factor(const T* verts, int tdim, int gdim) +{ + // Build J: j[col*gdim + row] = v_{col+1} - v_0, col in [0, tdim) + T J[9] = {}; + const T* v0 = verts; + for (int col = 0; col < tdim; ++col) + { + const T* vi = verts + (col + 1) * gdim; + for (int row = 0; row < gdim; ++row) + J[col * gdim + row] = vi[row] - v0[row]; + } + + if (tdim == gdim) + { + // Square Jacobian: use direct det + if (tdim == 1) + { + return std::abs(J[0]); + } + else if (tdim == 2) + { + return std::abs(J[0] * J[3] - J[2] * J[1]); + } + else // tdim == 3 + { + const T det = + J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + return std::abs(det); + } + } + else + { + // Non-square: Gramian sqrt(det(J^T J)) + // G[i][j] = sum_k J[i*gdim+k] * J[j*gdim+k] + T G[9] = {}; + for (int i = 0; i < tdim; ++i) + for (int j = 0; j < tdim; ++j) + { + T s = 0; + for (int k = 0; k < gdim; ++k) + s += J[i * gdim + k] * J[j * gdim + k]; + G[i * tdim + j] = s; + } + + if (tdim == 1) + return std::sqrt(G[0]); + else if (tdim == 2) + return std::sqrt(G[0] * G[3] - G[1] * G[2]); + else + { + const T det = + G[0] * (G[4] * G[8] - G[7] * G[5]) + - G[3] * (G[1] * G[8] - G[7] * G[2]) + + G[6] * (G[1] * G[5] - G[4] * G[2]); + return std::sqrt(det); + } + } +} + +// --------------------------------------------------------------------------- +// Gather vertex coordinates for subcell id from either the reference or +// physical vertex array of cut_cell. +// --------------------------------------------------------------------------- +template +void gather_subcell_verts(const cutcells::cell::CutCell& cut_cell, + int subcell_id, + const std::vector& coord_array, + int gdim, + std::vector& out) +{ + const auto verts = cutcells::cell::cell_vertices(cut_cell, subcell_id); + const int nv = static_cast(verts.size()); + out.resize(static_cast(nv) * gdim); + for (int j = 0; j < nv; ++j) + for (int d = 0; d < gdim; ++d) + out[static_cast(j) * gdim + d] = + coord_array[static_cast(verts[j]) * gdim + d]; +} + +} // anonymous namespace + +// ============================================================================= +// append_quadrature +// ============================================================================= +template +void append_quadrature(const cutcells::cell::CutCell& cut_cell, + int order, + QuadratureRules& rules) +{ + const int gdim = cut_cell._gdim; + const int tdim = cut_cell._tdim; // topological dim of the parent cell + const int nc = cutcells::cell::num_cells(cut_cell); + + if (nc == 0) + return; + + // Initialise _tdim on first call + if (rules._tdim == 0) + rules._tdim = tdim; + + // Ensure offset is initialised + if (rules._offset.empty()) + rules._offset.push_back(0); + + const int32_t pts_before = static_cast(rules._weights.size()); + + // Scratch buffers + std::vector ref_verts_sub; // reference subcell vertices + std::vector phys_verts_sub; // physical subcell vertices + std::vector mapped_ref_pts; // canonical pts mapped to ref subcell + + // Helper: process one simplex sub-sub-cell (triangle or tetrahedron). + // ref_verts_sub / phys_verts_sub must already contain the nv*gdim vertex + // coordinates of the simplex. stype must be interval/triangle/tetrahedron. + auto process_simplex = [&](cutcells::cell::type stype_) + { + const auto& ref_rule = get_reference_rule(stype_, order); + const int nq = ref_rule._num_points; + const int sdim = ref_rule._tdim; // canonical simplex tdim + + // ref_verts_sub contains vertices in the parent reference space, which + // has dimension gdim (= tdim for non-embedded cells). We must output + // gdim-dimensional parent reference coordinates, not sdim-dimensional + // canonical coordinates. For interface subcells sdim < gdim (e.g. + // interval sdim=1 from a triangle parent gdim=2), using sdim as the + // output stride would only read the first component of each vertex + // and produce wrong parent-reference points for physical_points. + mapped_ref_pts.resize(static_cast(nq) * gdim); + map_canonical_to_subcell(ref_rule._points.data(), nq, sdim, + ref_verts_sub.data(), gdim, + stype_, + mapped_ref_pts.data()); + + rules._points.insert(rules._points.end(), + mapped_ref_pts.begin(), mapped_ref_pts.end()); + + const T det_phys = simplex_physical_volume_factor( + phys_verts_sub.data(), sdim, gdim); + + for (int q = 0; q < nq; ++q) + rules._weights.push_back(ref_rule._weights[q] * det_phys); + }; + + // Helper: set ref_verts_sub and phys_verts_sub from a list of local vertex + // indices into an already-filled parent buffer (ref_verts_parent / + // phys_verts_parent), each of size nv_parent * gdim. + auto set_tet_verts = [&](const std::vector& ref_parent, + const std::vector& phys_parent, + const std::vector& tet_local_ids) + { + const int nv_tet = static_cast(tet_local_ids.size()); + ref_verts_sub.resize(static_cast(nv_tet) * gdim); + phys_verts_sub.resize(static_cast(nv_tet) * gdim); + for (int j = 0; j < nv_tet; ++j) + { + const int vi = tet_local_ids[j]; + for (int d = 0; d < gdim; ++d) + { + ref_verts_sub[j * gdim + d] = ref_parent[vi * gdim + d]; + phys_verts_sub[j * gdim + d] = phys_parent[vi * gdim + d]; + } + } + }; + + for (int s = 0; s < nc; ++s) + { + const cutcells::cell::type stype = cut_cell._types[s]; + + // For non-simplex types (prism, pyramid, quadrilateral) we + // sub-triangulate into simplices to keep the integration exact. + // The sub-triangulation uses local indices into the subcell vertex list. + if (stype == cutcells::cell::type::prism || + stype == cutcells::cell::type::pyramid || + stype == cutcells::cell::type::quadrilateral) + { + // Gather all vertices of this subcell into temporary arrays. + std::vector ref_parent, phys_parent; + gather_subcell_verts(cut_cell, s, cut_cell._vertex_coords, gdim, ref_parent); + gather_subcell_verts(cut_cell, s, cut_cell._vertex_coords_phys, gdim, phys_parent); + + const int nv_sub = cutcells::cell::num_cell_vertices(cut_cell, s); + std::vector local_ids(nv_sub); + for (int j = 0; j < nv_sub; ++j) local_ids[j] = j; + + // Obtain tet/triangle decomposition using existing triangulation.h + std::vector> sub_simplices; + cutcells::cell::triangulation(stype, local_ids.data(), sub_simplices); + + // Determine simplex type based on topological dimension + const cutcells::cell::type simplex_type = + (tdim == 3) ? cutcells::cell::type::tetrahedron + : cutcells::cell::type::triangle; + + for (const auto& simplex : sub_simplices) + { + set_tet_verts(ref_parent, phys_parent, simplex); + process_simplex(simplex_type); + } + continue; + } + + // Simplex path (interval, triangle, tetrahedron): + gather_subcell_verts(cut_cell, s, cut_cell._vertex_coords, gdim, ref_verts_sub); + gather_subcell_verts(cut_cell, s, cut_cell._vertex_coords_phys, gdim, phys_verts_sub); + process_simplex(stype); + } + + // One rule entry per CutCell: record how many new points were added + rules._parent_map.push_back(cut_cell._parent_cell_index); + rules._offset.push_back(static_cast(rules._weights.size())); +} + +// ============================================================================= +// make_quadrature (in-place) +// ============================================================================= +template +void make_quadrature(const std::vector>& cut_cells, + int order, + QuadratureRules& rules) +{ + rules._tdim = 0; + rules._points.clear(); + rules._weights.clear(); + rules._offset.clear(); + rules._parent_map.clear(); + + for (std::size_t i = 0; i < cut_cells.size(); ++i) + { + append_quadrature(cut_cells[i], order, rules); + // Override the parent_map entry written by append_quadrature to store + // the position in the input vector rather than _parent_cell_index + // (which may not be set by the caller). + if (!rules._parent_map.empty()) + rules._parent_map.back() = static_cast(i); + } +} + +// ============================================================================= +// make_quadrature (returns new object) +// ============================================================================= +template +QuadratureRules make_quadrature( + const std::vector>& cut_cells, int order) +{ + QuadratureRules rules; + make_quadrature(cut_cells, order, rules); + return rules; +} + +// ============================================================================= +// runtime_quadrature +// ============================================================================= +template +QuadratureRules runtime_quadrature( + std::span ls_vals, + std::span points, + std::span connectivity, + std::span offset, + std::span vtk_type, + const std::string& cut_type_str, + bool triangulate, + int order) +{ + using namespace cutcells::cell; + + const int ncells = static_cast(vtk_type.size()); + const cut_type ctype_enum = string_to_cut_type(cut_type_str); + + // Thread-local scratch (reused across calls, avoids repeated allocations) + thread_local std::vector tl_vtx_buf; + thread_local std::vector tl_ls_buf; + thread_local std::vector tl_vtx_buf_2d; // 2D projection for embedded 2D cells + thread_local CutCell tl_scratch; + thread_local std::vector tl_can_verts; + thread_local std::vector tl_ref_sub; + thread_local std::vector tl_phys_sub; + thread_local std::vector tl_mapped_ref; + + QuadratureRules rules; + rules._offset.push_back(0); + + for (int ci = 0; ci < ncells; ++ci) + { + const int coff = offset[ci]; + const type ctype = map_vtk_type_to_cell_type( + static_cast(vtk_type[ci])); + const int nv = get_num_vertices(ctype); + const int tdim = get_tdim(ctype); + + // Gather level-set values at the cell vertices + tl_ls_buf.resize(nv); + for (int j = 0; j < nv; ++j) + tl_ls_buf[j] = ls_vals[connectivity[coff + j]]; + + const domain dom = classify_cell_domain( + std::span(tl_ls_buf.data(), nv)); + + const bool is_full = (ctype_enum == cut_type::philt0 && dom == domain::inside) || + (ctype_enum == cut_type::phigt0 && dom == domain::outside); + const bool is_cut = (ctype_enum != cut_type::unset && dom == domain::intersected); + + if (!is_full && !is_cut) + continue; + + // Latch output dimension on first contributing cell + if (rules._tdim == 0) + rules._tdim = tdim; + + // Gather physical vertex coordinates (gdim = 3, VTK convention) + tl_vtx_buf.resize(nv * 3); + for (int j = 0; j < nv; ++j) + { + const int vid = connectivity[coff + j]; + tl_vtx_buf[j * 3 + 0] = points[vid * 3 + 0]; + tl_vtx_buf[j * 3 + 1] = points[vid * 3 + 1]; + tl_vtx_buf[j * 3 + 2] = points[vid * 3 + 2]; + } + + // ------------------------------------------------------------------ + // Full-cell path + // ------------------------------------------------------------------ + if (is_full) + { + const bool is_simplex = (ctype == type::interval || + ctype == type::triangle || + ctype == type::tetrahedron); + + if (!triangulate || is_simplex) + { + // Direct rule: reference points are canonical quadrature points + const auto& ref_rule = get_reference_rule(ctype, order); + const T det_J = affine_volume_factor(ctype, tl_vtx_buf.data(), 3); + const int nq = ref_rule._num_points; + + rules._points.insert(rules._points.end(), + ref_rule._points.begin(), ref_rule._points.end()); + for (int q = 0; q < nq; ++q) + rules._weights.push_back(ref_rule._weights[q] * det_J); + } + else + { + // Triangulate into simplices; integrate over each sub-simplex + tl_can_verts = canonical_vertices(ctype); + + std::vector local_ids(nv); + for (int j = 0; j < nv; ++j) local_ids[j] = j; + + std::vector> sub_simplices; + triangulation(ctype, local_ids.data(), sub_simplices); + + const type simplex_type = + (tdim == 3) ? type::tetrahedron : type::triangle; + const int sv = (tdim == 3) ? 4 : 3; // vertices per simplex + + const auto& ref_rule_s = get_reference_rule(simplex_type, order); + const int nq = ref_rule_s._num_points; + tl_mapped_ref.resize(nq * tdim); + + for (const auto& simplex : sub_simplices) + { + // Gather ref-space vertices of this sub-simplex (tdim-D coords) + tl_ref_sub.resize(sv * tdim); + for (int k = 0; k < sv; ++k) + { + const int vi = simplex[k]; + for (int d = 0; d < tdim; ++d) + tl_ref_sub[k * tdim + d] = tl_can_verts[vi * tdim + d]; + } + + // Gather physical vertices of this sub-simplex (3-D coords) + tl_phys_sub.resize(sv * 3); + for (int k = 0; k < sv; ++k) + { + const int vi = simplex[k]; + for (int d = 0; d < 3; ++d) + tl_phys_sub[k * 3 + d] = tl_vtx_buf[vi * 3 + d]; + } + + // Map canonical simplex quadrature points → parent ref space + map_canonical_to_subcell( + ref_rule_s._points.data(), nq, + tdim, + tl_ref_sub.data(), tdim, + simplex_type, + tl_mapped_ref.data()); + + // Physical volume factor for this sub-simplex + const T det = simplex_physical_volume_factor( + tl_phys_sub.data(), tdim, 3); + + rules._points.insert(rules._points.end(), + tl_mapped_ref.begin(), tl_mapped_ref.end()); + for (int q = 0; q < nq; ++q) + rules._weights.push_back(ref_rule_s._weights[q] * det); + } + } + + rules._parent_map.push_back(static_cast(ci)); + rules._offset.push_back(static_cast(rules._weights.size())); + } + // ------------------------------------------------------------------ + // Cut-cell path + // ------------------------------------------------------------------ + else // is_cut + { + if (tdim == 1) + continue; // embedded 1D edges cannot be cut + + if (tdim == 2) + { + // For 2D cells embedded in a 3D VTK mesh (z-plane), project + // vertices to 2D (x,y) so the cutter and pull-back work in a + // square (2×2) Jacobian system. + tl_vtx_buf_2d.resize(nv * 2); + for (int j = 0; j < nv; ++j) + { + tl_vtx_buf_2d[j * 2 + 0] = tl_vtx_buf[j * 3 + 0]; + tl_vtx_buf_2d[j * 2 + 1] = tl_vtx_buf[j * 3 + 1]; + } + cell::cut(ctype, + std::span(tl_vtx_buf_2d.data(), nv * 2), + 2, + std::span(tl_ls_buf.data(), nv), + cut_type_str, + tl_scratch, + triangulate); + if (cell::num_cells(tl_scratch) == 0) + continue; + tl_scratch._parent_cell_type = ctype; + tl_scratch._parent_vertex_coords = tl_vtx_buf_2d; // 2D parent + cell::complete_from_physical(tl_scratch); + append_quadrature(tl_scratch, order, rules); + } + else // tdim == 3 + { + cell::cut(ctype, + std::span(tl_vtx_buf.data(), nv * 3), + 3, + std::span(tl_ls_buf.data(), nv), + cut_type_str, + tl_scratch, + triangulate); + if (cell::num_cells(tl_scratch) == 0) + continue; + tl_scratch._parent_cell_type = ctype; + tl_scratch._parent_vertex_coords = tl_vtx_buf; + cell::complete_from_physical(tl_scratch); + append_quadrature(tl_scratch, order, rules); + } + + // append_quadrature records tl_scratch._parent_cell_index; override + // with the global mesh index + if (!rules._parent_map.empty()) + rules._parent_map.back() = static_cast(ci); + } + } + + return rules; +} + +// ============================================================================= +// physical_points +// ============================================================================= +template +std::vector physical_points( + const QuadratureRules& rules, + std::span points, + std::span connectivity, + std::span offset, + std::span vtk_type) +{ + using namespace cutcells::cell; + + const int nrules = static_cast(rules._parent_map.size()); + const int total_pts = static_cast(rules._weights.size()); + const int tdim = rules._tdim; + + std::vector out(static_cast(total_pts) * 3, T(0)); + + if (nrules == 0 || total_pts == 0 || tdim == 0) + return out; + + thread_local std::vector tl_phys_verts; + + for (int i = 0; i < nrules; ++i) + { + const int q_begin = rules._offset[i]; + const int q_end = rules._offset[i + 1]; + const int nq = q_end - q_begin; + if (nq == 0) + continue; + + const int ci = rules._parent_map[i]; + const int coff = offset[ci]; + const type ctype = map_vtk_type_to_cell_type( + static_cast(vtk_type[ci])); + const int nv = get_num_vertices(ctype); + + // Gather physical vertices for this cell + tl_phys_verts.resize(nv * 3); + for (int j = 0; j < nv; ++j) + { + const int vid = connectivity[coff + j]; + tl_phys_verts[j * 3 + 0] = points[vid * 3 + 0]; + tl_phys_verts[j * 3 + 1] = points[vid * 3 + 1]; + tl_phys_verts[j * 3 + 2] = points[vid * 3 + 2]; + } + + // Push ref → phys for all nq quadrature points of this rule. + // For tdim == gdim == 3 use the existing square-Jacobian routine; + // for tdim < 3 (embedded cells) apply the affine map manually: + // x_phys = v0 + sum_k (v_{col_k} - v0) * X_ref[k] + if (tdim == 3) + { + push_forward_affine( + ctype, + tl_phys_verts, + 3, + std::span(rules._points.data() + q_begin * tdim, + static_cast(nq) * tdim), + std::span(out.data() + q_begin * 3, + static_cast(nq) * 3)); + } + else + { + const auto cols = jacobian_col_indices(ctype); + const T* v0 = tl_phys_verts.data(); + const T* Xref = rules._points.data() + q_begin * tdim; + T* xout = out.data() + q_begin * 3; + for (int q = 0; q < nq; ++q) + { + for (int d = 0; d < 3; ++d) + xout[q * 3 + d] = v0[d]; + for (int k = 0; k < tdim; ++k) + { + const T* vk = tl_phys_verts.data() + cols[k] * 3; + const T xi_k = Xref[q * tdim + k]; + for (int d = 0; d < 3; ++d) + xout[q * 3 + d] += (vk[d] - v0[d]) * xi_k; + } + } + } + } + + return out; +} + +// ============================================================================= +// Explicit instantiations +// ============================================================================= +template void append_quadrature(const cutcells::cell::CutCell&, int, QuadratureRules&); +template void append_quadrature(const cutcells::cell::CutCell&, int, QuadratureRules&); + +template void make_quadrature(const std::vector>&, int, QuadratureRules&); +template void make_quadrature(const std::vector>&, int, QuadratureRules&); + +template QuadratureRules make_quadrature(const std::vector>&, int); +template QuadratureRules make_quadrature(const std::vector>&, int); + +template QuadratureRules runtime_quadrature( + std::span, std::span, + std::span, std::span, std::span, + const std::string&, bool, int); +template QuadratureRules runtime_quadrature( + std::span, std::span, + std::span, std::span, std::span, + const std::string&, bool, int); + +template std::vector physical_points( + const QuadratureRules&, + std::span, std::span, std::span, std::span); +template std::vector physical_points( + const QuadratureRules&, + std::span, std::span, std::span, std::span); + +} // namespace cutcells::quadrature diff --git a/cpp/src/quadrature.h b/cpp/src/quadrature.h new file mode 100644 index 0000000..7e49768 --- /dev/null +++ b/cpp/src/quadrature.h @@ -0,0 +1,146 @@ +// Copyright (c) 2024 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "cut_cell.h" +#include "cell_types.h" + +#include +#include +#include +#include +#include + +namespace cutcells::quadrature +{ + +/// Flat batch of quadrature rules for a collection of cut cells. +/// +/// Conventions: +/// - _points are in the **parent reference element** (dimension _tdim each). +/// - _weights are **physical integration weights** (|det J_phys| scaled). +/// - Rule i covers parent cell _parent_map[i]. +/// - Points for rule i live at _points[_offset[i]*_tdim .. _offset[i+1]*_tdim). +/// - Weights for rule i live at _weights[_offset[i] .. _offset[i+1]). +/// +/// Preconditions before calling make_quadrature / append_quadrature: +/// - cut_cell._vertex_coords must hold parent **reference** coordinates. +/// - cut_cell._vertex_coords_phys must hold **physical** coordinates. +/// - Both are filled after calling compute_physical_cut_vertices() or +/// complete_from_physical() from mapping.h. +template +struct QuadratureRules +{ + /// Topological dimension of the parent reference element (= dim of each point) + int _tdim = 0; + + /// Flat quadrature points in parent reference space. + /// Size: total_num_points * _tdim + std::vector _points; + + /// Physical integration weights. + /// Size: total_num_points + std::vector _weights; + + /// CSR offsets: rule i has points in [_offset[i], _offset[i+1]). + /// Size: num_rules + 1 (_offset[0] = 0 always) + std::vector _offset; + + /// Parent cell index for each rule. + /// Size: num_rules + std::vector _parent_map; +}; + +/// Append the quadrature contribution of one cut cell. +/// +/// Iterates over the subcells of cut_cell, fetches the canonical rule for +/// each subcell type at the given polynomial order, maps canonical points to +/// the parent reference element (using _vertex_coords), computes physical +/// weights (using _vertex_coords_phys), and appends everything to rules. +/// +/// @param cut_cell enriched cut cell (both coord frames must be set) +/// @param order polynomial order for the canonical quadrature rule +/// @param rules output accumulator (appended to, not reset) +template +void append_quadrature(const cutcells::cell::CutCell& cut_cell, + int order, + QuadratureRules& rules); + +/// Build QuadratureRules for a vector of cut cells. +/// rules._offset and rules._parent_map are built alongside the point/weight data. +/// @param cut_cells enriched cut-cell vector +/// @param order polynomial order +/// @param rules output (reset before use) +template +void make_quadrature(const std::vector>& cut_cells, + int order, + QuadratureRules& rules); + +/// Convenience overload that returns a freshly constructed QuadratureRules. +template +QuadratureRules make_quadrature( + const std::vector>& cut_cells, int order); + +/// @brief Generate flat quadrature rules for all mesh cells matching the +/// requested level-set domain. +/// +/// Iterates over all cells in the VTK-format mesh. For each cell the level-set +/// domain is classified: +/// - *Full cells* (entirely inside / outside): a standard reference-space +/// rule is used, scaled by |det J_phys|. When @p triangulate is true and +/// the cell is not a simplex, the cell is first decomposed into simplices. +/// - *Cut cells* (intersected): the cell is cut, the reference/physical +/// coordinates are completed via mapping.h, and append_quadrature is called. +/// +/// All input arrays are flat and follow VTK conventions: +/// @p points npts × 3 (gdim = 3 always for VTK meshes) +/// @p connectivity concatenated vertex-index lists for all cells +/// @p offset CSR offsets: cell i uses connectivity[offset[i]..offset[i+1]) +/// @p vtk_type VTK element-type code per cell +/// +/// @param ls_vals level-set values at all mesh vertices (size = npts) +/// @param points flat mesh vertex coordinates (size = npts * 3) +/// @param connectivity flat vertex connectivity list +/// @param offset CSR offsets (size = ncells + 1) +/// @param vtk_type VTK cell types (size = ncells) +/// @param cut_type_str one of "phi<0", "phi>0", "phi=0" +/// @param triangulate if true, non-simplex cells are split into simplices +/// @param order polynomial quadrature order +/// @returns flat QuadratureRules in parent reference space +template +QuadratureRules runtime_quadrature( + std::span ls_vals, + std::span points, + std::span connectivity, + std::span offset, + std::span vtk_type, + const std::string& cut_type_str, + bool triangulate, + int order); + +/// @brief Push reference-space quadrature points to physical space. +/// +/// For each rule i in @p rules, the reference-space points stored in +/// rules._points are mapped to physical space using the affine pushforward +/// of parent cell @c rules._parent_map[i]. +/// +/// @param rules quadrature rules (output of runtime_quadrature or +/// make_quadrature) +/// @param points flat mesh vertex coordinates (size = npts * 3) +/// @param connectivity flat vertex connectivity list +/// @param offset CSR offsets (size = ncells + 1) +/// @param vtk_type VTK cell types (size = ncells) +/// @returns flat physical point coordinates +/// (total_num_points * 3) +template +std::vector physical_points( + const QuadratureRules& rules, + std::span points, + std::span connectivity, + std::span offset, + std::span vtk_type); + +} // namespace cutcells::quadrature diff --git a/cpp/src/quadrature_tables.h b/cpp/src/quadrature_tables.h new file mode 100644 index 0000000..d86d02b --- /dev/null +++ b/cpp/src/quadrature_tables.h @@ -0,0 +1,125 @@ +// Copyright (c) 2024 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +// --------------------------------------------------------------------------- +// Thin lookup interface over Basix-generated quadrature tables. +// Generated headers live in cpp/src/generated/quadrature_tables_.h and +// are produced by tablegen/scripts/gen_quadrature_tables.py. +// --------------------------------------------------------------------------- + +#include "cell_types.h" + +#include "generated/quadrature_tables_interval.h" +#include "generated/quadrature_tables_triangle.h" +#include "generated/quadrature_tables_quadrilateral.h" +#include "generated/quadrature_tables_tetrahedron.h" +#include "generated/quadrature_tables_hexahedron.h" +#include "generated/quadrature_tables_prism.h" +#include "generated/quadrature_tables_pyramid.h" + +#include +#include +#include +#include + +namespace cutcells::quadrature +{ + +/// Reference quadrature rule for given cell type and polynomial order. +/// Points are in the Basix canonical reference element. +/// Weights are the canonical integration weights (sum = reference cell volume). +/// +/// The struct owns its data via std::vector. The template parameter T +/// is the requested floating-point precision; data is stored as double and +/// converted on construction. +template +struct ReferenceQuadratureRule +{ + int _tdim = 0; ///< topological dimension of the canonical cell + int _num_points = 0; ///< number of quadrature points + std::vector _points; ///< flat: _num_points * _tdim + std::vector _weights; ///< flat: _num_points +}; + +// --------------------------------------------------------------------------- +// Internal helper – copy double arrays to vector +// --------------------------------------------------------------------------- +namespace detail +{ + +template +ReferenceQuadratureRule make_rule(int tdim, int npts, + const double* pts_d, + const double* wts_d) +{ + ReferenceQuadratureRule r; + r._tdim = tdim; + r._num_points = npts; + r._points.resize(static_cast(npts) * tdim); + r._weights.resize(static_cast(npts)); + for (int i = 0; i < npts * tdim; ++i) + r._points[i] = static_cast(pts_d[i]); + for (int i = 0; i < npts; ++i) + r._weights[i] = static_cast(wts_d[i]); + return r; +} + +} // namespace detail + +// --------------------------------------------------------------------------- +// Public lookup function +// --------------------------------------------------------------------------- + +/// Return the canonical quadrature rule for (cell_type, order). +/// Supports cell types: interval, triangle, quadrilateral, tetrahedron, +/// hexahedron, prism, pyramid. +/// Supports orders 1..10 (as generated by gen_quadrature_tables.py). +/// Throws std::invalid_argument for unsupported combinations. +template +ReferenceQuadratureRule get_reference_rule(cell::type cell_type, int order) +{ + using namespace cutcells::quadrature::generated; + +#define CUTCELLS_DISPATCH_CELL(CNAME, CTYPE) \ + case cell::type::CTYPE: \ + switch (order) { \ + case 1: return detail::make_rule(CNAME##_o1_tdim, CNAME##_o1_npts, CNAME##_o1_points, CNAME##_o1_weights); \ + case 2: return detail::make_rule(CNAME##_o2_tdim, CNAME##_o2_npts, CNAME##_o2_points, CNAME##_o2_weights); \ + case 3: return detail::make_rule(CNAME##_o3_tdim, CNAME##_o3_npts, CNAME##_o3_points, CNAME##_o3_weights); \ + case 4: return detail::make_rule(CNAME##_o4_tdim, CNAME##_o4_npts, CNAME##_o4_points, CNAME##_o4_weights); \ + case 5: return detail::make_rule(CNAME##_o5_tdim, CNAME##_o5_npts, CNAME##_o5_points, CNAME##_o5_weights); \ + case 6: return detail::make_rule(CNAME##_o6_tdim, CNAME##_o6_npts, CNAME##_o6_points, CNAME##_o6_weights); \ + case 7: return detail::make_rule(CNAME##_o7_tdim, CNAME##_o7_npts, CNAME##_o7_points, CNAME##_o7_weights); \ + case 8: return detail::make_rule(CNAME##_o8_tdim, CNAME##_o8_npts, CNAME##_o8_points, CNAME##_o8_weights); \ + case 9: return detail::make_rule(CNAME##_o9_tdim, CNAME##_o9_npts, CNAME##_o9_points, CNAME##_o9_weights); \ + case 10: return detail::make_rule(CNAME##_o10_tdim, CNAME##_o10_npts, CNAME##_o10_points, CNAME##_o10_weights); \ + default: break; \ + } \ + break + + switch (cell_type) + { + CUTCELLS_DISPATCH_CELL(interval, interval); + CUTCELLS_DISPATCH_CELL(triangle, triangle); + CUTCELLS_DISPATCH_CELL(quadrilateral, quadrilateral); + CUTCELLS_DISPATCH_CELL(tetrahedron, tetrahedron); + CUTCELLS_DISPATCH_CELL(hexahedron, hexahedron); + CUTCELLS_DISPATCH_CELL(prism, prism); + CUTCELLS_DISPATCH_CELL(pyramid, pyramid); + default: + throw std::invalid_argument( + "get_reference_rule: unsupported cell type"); + } + +#undef CUTCELLS_DISPATCH_CELL + + throw std::invalid_argument( + "get_reference_rule: order " + std::to_string(order) + + " not available (max order is 10; regenerate tables with higher max_order)"); +} + +} // namespace cutcells::quadrature diff --git a/cpp/src/triangulation.h b/cpp/src/triangulation.h index 064b1d6..5d5821b 100644 --- a/cpp/src/triangulation.h +++ b/cpp/src/triangulation.h @@ -7,26 +7,115 @@ #include "cell_types.h" +#include +#include +#include + namespace cutcells::cell { + /// @brief Decompose a non-simplex cell into simplices (triangles or tetrahedra). + /// + /// Each entry in @p tris is a list of vertex indices (into the parent vertices[]) + /// array) that form one simplex. + /// + /// Supported cell types and their outputs: + /// quadrilateral → 2 triangles (3 vertices each) + /// hexahedron → 6 tetrahedra (4 vertices each) + /// prism → 3 tetrahedra (4 vertices each) + /// pyramid → 2 tetrahedra (4 vertices each) + /// + /// VTK vertex ordering is assumed for all cell types. inline void triangulation(const type cell_type, int* vertices, std::vector>& tris) { switch(cell_type) { - case type::quadrilateral: tris.resize(2, std::vector(3)); - tris = {{vertices[0],vertices[1],vertices[2]}, {vertices[0],vertices[2],vertices[3]}}; - break; - case type::prism: // Tetrahedron 0 (original vertices): { 0, 2, 1, 3 } - // Tetrahedron 1 (original vertices): { 1, 3, 5, 4 } - // Tetrahedron 2 (original vertices): { 1, 2, 5, 3 } - tris.resize(3, std::vector(3)); - tris = {{vertices[0],vertices[2],vertices[1],vertices[3]}, - {vertices[1],vertices[3],vertices[5],vertices[4]}, - {vertices[1],vertices[2],vertices[5],vertices[3]}}; - break; - - default: throw std::invalid_argument("triangulation not implemented for given cell type"); - break; + case type::quadrilateral: + tris.resize(2, std::vector(3)); + tris = {{vertices[0],vertices[1],vertices[2]}, {vertices[0],vertices[2],vertices[3]}}; + break; + + case type::hexahedron: + // VTK hex: v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) + // v4=(0,0,1) v5=(1,0,1) v6=(1,1,1) v7=(0,1,1) + // + // Kuhn triangulation (6 tets, fan through the v0→v6 diagonal). + // Each tet has |det J| = 1 on the unit cube → total volume = 6 × 1/6 = 1 ✓ + tris.resize(6, std::vector(4)); + tris = {{vertices[0],vertices[1],vertices[2],vertices[6]}, + {vertices[0],vertices[1],vertices[5],vertices[6]}, + {vertices[0],vertices[3],vertices[2],vertices[6]}, + {vertices[0],vertices[3],vertices[7],vertices[6]}, + {vertices[0],vertices[4],vertices[5],vertices[6]}, + {vertices[0],vertices[4],vertices[7],vertices[6]}}; + break; + + case type::prism: + // VTK wedge: v0,v1,v2 bottom △, v3,v4,v5 top △ + // Tetrahedron 0: { v0, v2, v1, v3 } + // Tetrahedron 1: { v1, v3, v5, v4 } + // Tetrahedron 2: { v1, v2, v5, v3 } + tris.resize(3, std::vector(4)); + tris = {{vertices[0],vertices[2],vertices[1],vertices[3]}, + {vertices[1],vertices[3],vertices[5],vertices[4]}, + {vertices[1],vertices[2],vertices[5],vertices[3]}}; + break; + + case type::pyramid: + // VTK pyramid: v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) v4=apex + // Tetrahedron 0: { v0, v1, v3, v4 } + // Tetrahedron 1: { v1, v2, v3, v4 } + tris.resize(2, std::vector(4)); + tris = {{vertices[0],vertices[1],vertices[3],vertices[4]}, + {vertices[1],vertices[2],vertices[3],vertices[4]}}; + break; + + default: + throw std::invalid_argument("triangulation not implemented for given cell type"); + break; } }; -} + + /// @brief Canonical reference vertices for a cell type (VTK ordering, flat). + /// + /// Returns a flat vector of nv * tdim values representing the reference-element + /// vertex coordinates for the given cell type. Coordinates are in the + /// topological-dimension space (i.e. tdim values per vertex, NOT padded to gdim). + /// + /// VTK canonical positions used (these make the affine Jacobian = I for the + /// unit reference cell): + /// interval v0=(0) v1=(1) + /// triangle v0=(0,0) v1=(1,0) v2=(0,1) + /// quadrilateral v0=(0,0) v1=(1,0) v2=(1,1) v3=(0,1) + /// tetrahedron v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(0,0,1) + /// hexahedron v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) + /// v4=(0,0,1) v5=(1,0,1) v6=(1,1,1) v7=(0,1,1) + /// prism v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) + /// v3=(0,0,1) v4=(1,0,1) v5=(0,1,1) + /// pyramid v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) v4=(0,0,1) + template + inline std::vector canonical_vertices(type cell_type) + { + switch (cell_type) + { + case type::interval: + return {T(0), T(1)}; + case type::triangle: + return {T(0),T(0), T(1),T(0), T(0),T(1)}; + case type::quadrilateral: + return {T(0),T(0), T(1),T(0), T(1),T(1), T(0),T(1)}; + case type::tetrahedron: + return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), T(0),T(0),T(1)}; + case type::hexahedron: + return {T(0),T(0),T(0), T(1),T(0),T(0), T(1),T(1),T(0), T(0),T(1),T(0), + T(0),T(0),T(1), T(1),T(0),T(1), T(1),T(1),T(1), T(0),T(1),T(1)}; + case type::prism: + return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), + T(0),T(0),T(1), T(1),T(0),T(1), T(0),T(1),T(1)}; + case type::pyramid: + return {T(0),T(0),T(0), T(1),T(0),T(0), T(1),T(1),T(0), T(0),T(1),T(0), T(0),T(0),T(1)}; + default: + throw std::invalid_argument("canonical_vertices: unsupported cell type"); + } + } + +} // namespace cutcells::cell diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 4b88d15..d149c96 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -23,6 +23,7 @@ else() # runtime library discovered on the default prefix path. if (NOT DEFINED CutCells_DIR) set(_local_cutcells_candidates + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release/prefix/lib/cmake/cutcells" "${CMAKE_CURRENT_LIST_DIR}/../cpp/build/prefix/lib/cmake/cutcells" "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir/prefix/lib/cmake/cutcells" "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release" diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 4966e6d..19bb392 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -12,10 +12,17 @@ CutCells_float64, CutMesh_float32, CutMesh_float64, + QuadratureRules_float32, + QuadratureRules_float64, cut, higher_order_cut, create_cut_mesh, locate_cells, cut_vtk_mesh, csr_to_vtk_cells, + compute_physical_cut_vertices, + complete_from_physical, + make_quadrature, + runtime_quadrature, + physical_points, ) diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 2d681e7..481a363 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -19,6 +19,8 @@ #include "../../cpp/src/cut_cell.h" #include "../../cpp/src/cut_mesh.h" #include "../../cpp/src/write_vtk.h" +#include "../../cpp/src/mapping.h" +#include "../../cpp/src/quadrature.h" namespace nb = nanobind; @@ -217,6 +219,21 @@ void declare_float(nb::module_& m, std::string type) nb::rv_policy::reference_internal, "Return parent entity token for each cut-cell vertex.\n" "Tokens encode the origin: edge intersections use edge id, original vertices use 100+vid, special points use 200+sid.") + .def_prop_ro( + "vertex_coords_phys", + [](const cell::CutCell& self) { + const std::size_t gdim = static_cast(self._gdim); + if (gdim == 0 || self._vertex_coords_phys.empty()) + return nb::ndarray(nullptr, {0, 0}, nb::handle()); + const std::size_t n = self._vertex_coords_phys.size() / gdim; + return nb::ndarray( + self._vertex_coords_phys.data(), + {n, gdim}, + nb::handle()); + }, + nb::rv_policy::reference_internal, + "Zero-copy view of cut-cell physical vertex coordinates as shape (num_vertices, gdim). " + "Empty until compute_physical_cut_vertices() or complete_from_physical() is called.") .def("str", [](const cell::CutCell& self) {cell::str(self); return ;}) .def("volume", [](const cell::CutCell& self) {return cell::volume(self);}) .def("write_vtk", [](cell::CutCell& self, std::string fname) {io::write_vtk(fname,self); return ;}); @@ -261,26 +278,26 @@ void declare_float(nb::module_& m, std::string type) [](const mesh::CutMesh& self) { const std::size_t gdim = static_cast(self._gdim); if (gdim == 0) - return nb::ndarray(nullptr, {0, 0}, nb::handle()); + return nb::ndarray(nullptr, {0, 0}, nb::handle()); const std::size_t n = self._vertex_coords.size() / gdim; - return nb::ndarray( - self._vertex_coords.data(), - {n, gdim}, - nb::handle()); + // Return an *owned* copy so pyvista (which retains the array) does not + // keep the CutMesh alive via a numpy base reference at interpreter shutdown. + std::vector copy = self._vertex_coords; + return as_nbarray(std::move(copy), {n, gdim}); }, - nb::rv_policy::reference_internal, - "Zero-copy view of mesh vertex coordinates as shape (num_vertices, gdim).") + nb::rv_policy::move, + "Copy of mesh vertex coordinates as shape (num_vertices, gdim).") .def_prop_ro( "connectivity", [](const mesh::CutMesh& self) { - return nb::ndarray(self._connectivity.data(),{self._connectivity.size()}, nb::handle()); + return nb::ndarray(self._connectivity.data(),{self._connectivity.size()}, nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal, " Return connectivity vector.") .def_prop_ro( "offset", [](const mesh::CutMesh& self) { - return nb::ndarray(self._offset.data(),{self._offset.size()}, nb::handle()); + return nb::ndarray(self._offset.data(),{self._offset.size()}, nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal, " Return offset vector.") @@ -320,11 +337,88 @@ void declare_float(nb::module_& m, std::string type) .def_prop_ro( "parent_map", [](const mesh::CutMesh& self) { - return nb::ndarray(self._parent_map.data(),{self._parent_map.size()}, nb::handle()); + return nb::ndarray(self._parent_map.data(),{self._parent_map.size()}, nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal, " Return parent map of cut mesh."); + // ---- frame-completion helpers ---- + m.def("compute_physical_cut_vertices", + [](cell::CutCell& cut_cell) { + nb::gil_scoped_release release; + cell::compute_physical_cut_vertices(cut_cell); + }, + nb::arg("cut_cell"), + "Fill _vertex_coords_phys via affine push-forward of _vertex_coords (reference-frame cut path)."); + + m.def("complete_from_physical", + [](cell::CutCell& cut_cell) { + nb::gil_scoped_release release; + cell::complete_from_physical(cut_cell); + }, + nb::arg("cut_cell"), + "Copy vertex_coords to _vertex_coords_phys, then pull back to fill _vertex_coords " + "with parent reference coordinates (physical-frame cut path)."); + + // ---- QuadratureRules class ---- + { + std::string qr_name = "QuadratureRules_" + type; + nb::class_>(m, qr_name.c_str(), + "Flat batch quadrature rules for a collection of cut cells.\n" + "Points are in parent reference space; weights incorporate the physical Jacobian determinant.") + .def(nb::init<>()) + .def_prop_ro("tdim", + [](const quadrature::QuadratureRules& self) { return self._tdim; }, + "Topological dimension of the point coordinates.") + .def_prop_ro("points", + [](const quadrature::QuadratureRules& self) { + // Always return a flat 1-D array; caller reshapes with [:, tdim] if needed. + // Use nb::cast(self, ...) as the ndarray owner so NumPy holds a proper + // strong reference to the parent — avoids keep_alive cycles at shutdown. + return nb::ndarray( + self._points.data(), {self._points.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Quadrature points in parent reference space, flat array of length (total_points * tdim). " + "Reshape to (-1, tdim) to get shape (total_points, tdim).") + .def_prop_ro("weights", + [](const quadrature::QuadratureRules& self) { + return nb::ndarray( + self._weights.data(), {self._weights.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Physical integration weights, shape (total_points,).") + .def_prop_ro("offset", + [](const quadrature::QuadratureRules& self) { + return nb::ndarray( + self._offset.data(), {self._offset.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Offsets into points/weights per cut-cell rule, shape (num_rules+1,).") + .def_prop_ro("parent_map", + [](const quadrature::QuadratureRules& self) { + return nb::ndarray( + self._parent_map.data(), {self._parent_map.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal, + "Index of the originating cut-cell in the input list, shape (num_rules,)."); + } + + // ---- make_quadrature ---- + m.def("make_quadrature", + [](const std::vector>& cut_cells, int order) { + nb::gil_scoped_release release; + return quadrature::make_quadrature(cut_cells, order); + }, + nb::arg("cut_cells"), nb::arg("order"), + "Generate flat quadrature rules for a list of enriched CutCells.\n" + "Both vertex_coords (_vertex_coords, reference) and vertex_coords_phys must be populated on each cell.\n" + "Returns a QuadratureRules_ object."); + m.def("create_cut_mesh", [](mesh::CutCells& cut_cells){ nb::gil_scoped_release release; return mesh::create_cut_mesh(cut_cells); @@ -394,6 +488,62 @@ void declare_float(nb::module_& m, std::string type) , nb::arg("ls_vals"), nb::arg("points"), nb::arg("connectivity"), nb::arg("offset"), nb::arg("vtk_type"), nb::arg("cut_type_str"), nb::arg("triangulate") = true , "cut vtk mesh"); + + m.def("runtime_quadrature", + [](const nb::ndarray, nb::c_contig>& ls_vals, + const nb::ndarray, nb::c_contig>& points, + const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offset, + const nb::ndarray, nb::c_contig>& vtk_type, + const std::string& cut_type_str, + bool triangulate, + int order) { + { + nb::gil_scoped_release release; + return quadrature::runtime_quadrature( + std::span(ls_vals.data(), ls_vals.size()), + std::span(points.data(), points.size()), + std::span(connectivity.data(),connectivity.size()), + std::span(offset.data(), offset.size()), + std::span(vtk_type.data(), vtk_type.size()), + cut_type_str, + triangulate, + order); + } + }, + nb::arg("ls_vals"), nb::arg("points"), nb::arg("connectivity"), + nb::arg("offset"), nb::arg("vtk_type"), nb::arg("cut_type_str"), + nb::arg("triangulate") = true, nb::arg("order") = 3, + "Generate flat quadrature rules for all mesh cells in the requested " + "level-set domain.\n" + "Full cells (entirely inside/outside) use a direct reference rule scaled " + "by |det J|.\n" + "Cut cells (intersected) are cut and integrated via append_quadrature.\n" + "Returns a QuadratureRules object with reference-space points and " + "physical weights."); + + m.def("physical_points", + [](const quadrature::QuadratureRules& rules, + const nb::ndarray, nb::c_contig>& points, + const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offset, + const nb::ndarray, nb::c_contig>& vtk_type) { + std::vector pts; + { + nb::gil_scoped_release release; + pts = quadrature::physical_points( + rules, + std::span(points.data(), points.size()), + std::span(connectivity.data(),connectivity.size()), + std::span(offset.data(), offset.size()), + std::span(vtk_type.data(), vtk_type.size())); + } + return as_nbarray(std::move(pts)); + }, + nb::arg("rules"), nb::arg("points"), nb::arg("connectivity"), + nb::arg("offset"), nb::arg("vtk_type"), + "Map reference-space quadrature points to physical space.\n" + "Returns a flat numpy array of shape (total_num_points * 3,)."); } } // namespace diff --git a/python/demo/quadrature/demo_quadrature_2D.py b/python/demo/quadrature/demo_quadrature_2D.py new file mode 100644 index 0000000..be6df2d --- /dev/null +++ b/python/demo/quadrature/demo_quadrature_2D.py @@ -0,0 +1,261 @@ +""" +Quadrature point visualisation — 2D (triangulated mesh, z = 0) +============================================================== +Creates a background triangular mesh in the square [-1, 1]^2 embedded in +the z = 0 plane and cuts it with a circular level-set phi(x) = ||x|| - r. + +Two quadrature integrals are computed and visualised: + + Area integral (phi < 0): ∫_{Ω_h} 1 dA ≈ π r² + · fully-inside triangles → standard reference Gauss rule + · cut triangles → cut-cell sub-triangulation rule + Perimeter integral (phi = 0): ∫_{Γ_h} 1 ds ≈ 2π r + · cut triangles → interface edge rule + +Both sets of quadrature points are displayed. Physical points are obtained +by mapping 2D reference coordinates through the affine triangle map to the +3D z = 0 plane. +""" + +import numpy as np +import pyvista as pv +import cutcells + +RADIUS = 0.7 +N = 22 # grid resolution (NxN before Delaunay) +ORDER = 2 # quadrature order + + +# --------------------------------------------------------------------------- +# Mesh +# --------------------------------------------------------------------------- +def circle_ls(pts: np.ndarray) -> np.ndarray: + return np.sqrt(pts[:, 0] ** 2 + pts[:, 1] ** 2) - RADIUS + + +def build_mesh(N: int) -> pv.UnstructuredGrid: + x = np.linspace(-1.0, 1.0, N) + y = np.linspace(-1.0, 1.0, N) + xx, yy, zz = np.meshgrid(x, y, [0.0]) + pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + return pv.UnstructuredGrid(pv.PolyData(pts).delaunay_2d()) + + +grid = build_mesh(N) +points = np.asarray(grid.points, dtype=np.float64) +ls_values = circle_ls(points) +points_flat = points.ravel() +connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) +offset = np.asarray(grid.offset, dtype=np.int32) +celltypes = np.asarray(grid.celltypes, dtype=np.int32) + +# --------------------------------------------------------------------------- +# Area quadrature (phi < 0 — inside + cut cells) +# --------------------------------------------------------------------------- +rules_area = cutcells.runtime_quadrature( + ls_values, + points_flat, + connectivity, + offset, + celltypes, + "phi<0", + True, + ORDER, +) +phys_area = cutcells.physical_points( + rules_area, points_flat, connectivity, offset, celltypes +).reshape(-1, 3) +w_area = np.asarray(rules_area.weights) +n_area = len(np.asarray(rules_area.parent_map)) + +# --------------------------------------------------------------------------- +# Perimeter quadrature (phi = 0 — interface) +# --------------------------------------------------------------------------- +rules_peri = cutcells.runtime_quadrature( + ls_values, + points_flat, + connectivity, + offset, + celltypes, + "phi=0", + True, + ORDER, +) +phys_peri = cutcells.physical_points( + rules_peri, points_flat, connectivity, offset, celltypes +).reshape(-1, 3) +w_peri = np.asarray(rules_peri.weights) +n_peri = len(np.asarray(rules_peri.parent_map)) + +exact_area = np.pi * RADIUS**2 +exact_peri = 2.0 * np.pi * RADIUS +print(f"Mesh : {grid.n_cells} cells ({N}×{N} Delaunay 2D)") +print( + f"Area : {n_area:4d} rules, {len(w_area):5d} qpts " + f"∫1 dA = {w_area.sum():.6f} (π r² = {exact_area:.6f} " + f"err = {abs(w_area.sum() - exact_area):.4f})" +) +print( + f"Peri : {n_peri:4d} rules, {len(w_peri):5d} qpts " + f"∫1 ds = {w_peri.sum():.6f} (2π r = {exact_peri:.6f} " + f"err = {abs(w_peri.sum() - exact_peri):.4f})" +) + +# --------------------------------------------------------------------------- +# Classify cells and split area q-pts by parent cell type +# --------------------------------------------------------------------------- +inside_ids = cutcells.locate_cells( + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" +) +inside_set = set(inside_ids.tolist()) + +n_cells = grid.n_cells +cell_min_ls = np.zeros(n_cells) +cell_max_ls = np.zeros(n_cells) +for ci in range(n_cells): + vids = connectivity[offset[ci] : offset[ci + 1]] + cell_min_ls[ci] = ls_values[vids].min() + cell_max_ls[ci] = ls_values[vids].max() +cut_ids = np.where((cell_min_ls < 0) & (cell_max_ls > 0))[0] + +# Split area quadrature points into inside-cell pts and cut-cell pts so the +# two populations can be shown in distinct colours (not via a weight colormap +# which confusingly mixes cell-type information with weight magnitude). +area_parent_map = np.asarray(rules_area.parent_map) +area_offset = np.asarray(rules_area.offset) +inside_mask = np.zeros(len(w_area), dtype=bool) +for rule_i, parent in enumerate(area_parent_map): + q0, q1 = int(area_offset[rule_i]), int(area_offset[rule_i + 1]) + if parent in inside_set: + inside_mask[q0:q1] = True + +phys_area_inside = phys_area[inside_mask] # full cells → reference Gauss rule +phys_area_cut = phys_area[~inside_mask] # cut cells → sub-triangulation rule + +# --------------------------------------------------------------------------- +# Build cut sub-meshes for visual verification +# --------------------------------------------------------------------------- +# Volume sub-triangulation of cut region (phi < 0 part of each cut triangle) +cut_vol = cutcells.cut_vtk_mesh( + ls_values, points_flat, connectivity, offset, celltypes, "phi<0" +) +pv_cut_vol = pv.UnstructuredGrid( + np.asarray(cut_vol.cells), + np.asarray(cut_vol.vtk_types), + np.asarray(cut_vol.vertex_coords), +) + +# Interface edges (phi = 0 — one interval per cut triangle) +cut_iface = cutcells.cut_vtk_mesh( + ls_values, points_flat, connectivity, offset, celltypes, "phi=0" +) +pv_cut_iface = pv.UnstructuredGrid( + np.asarray(cut_iface.cells), + np.asarray(cut_iface.vtk_types), + np.asarray(cut_iface.vertex_coords), +) + +# --------------------------------------------------------------------------- +# PyVista visualisation +# --------------------------------------------------------------------------- +POINT_SIZE = 8 # pixels, same for all point clouds → easy visual comparison + +pl = pv.Plotter(window_size=(1200, 900)) +pl.set_background("white") + +# Background wireframe +pl.add_mesh(grid, style="wireframe", color="lightgrey", line_width=0.5, opacity=0.6) + +# Inside cells (blue fill) +if len(inside_ids): + pl.add_mesh( + grid.extract_cells(inside_ids), + color="#80b3ff", + opacity=0.25, + show_edges=True, + edge_color="steelblue", + line_width=0.5, + label="inside cells", + ) + +# Cut sub-triangles of the volume part (phi<0) — dark green outline, transparent fill +pl.add_mesh( + pv_cut_vol, + color="#00aa44", + opacity=0.20, + show_edges=True, + edge_color="#007730", + line_width=1.2, + label="cut sub-tris (phi<0)", +) + +# Interface edges (phi=0) — thick magenta lines + their nodes +pl.add_mesh( + pv_cut_iface, + color="magenta", + line_width=3.0, + label="interface edges (phi=0)", +) +pl.add_points( + pv_cut_iface.points, + color="darkviolet", + point_size=10, + render_points_as_spheres=True, + label=f"interface nodes ({pv_cut_iface.n_points})", +) + +# Cut volume sub-triangle nodes +pl.add_points( + pv_cut_vol.points, + color="#007730", + point_size=7, + render_points_as_spheres=True, + label=f"cut vol nodes ({pv_cut_vol.n_points})", +) + +# Area q-pts in fully-inside cells (green) +if len(phys_area_inside): + pl.add_points( + phys_area_inside, + color="limegreen", + point_size=POINT_SIZE, + render_points_as_spheres=True, + label=f"area q-pts inside ({len(phys_area_inside)})", + ) + +# Area q-pts in cut cells (orange-red) +if len(phys_area_cut): + pl.add_points( + phys_area_cut, + color="orangered", + point_size=POINT_SIZE, + render_points_as_spheres=True, + label=f"area q-pts cut ({len(phys_area_cut)})", + ) + +# Perimeter q-pts (dark blue) +if len(phys_peri): + pl.add_points( + phys_peri, + color="navy", + point_size=POINT_SIZE, + render_points_as_spheres=True, + label=f"perimeter q-pts ({len(phys_peri)})", + ) + +# Circle outline +theta = np.linspace(0.0, 2.0 * np.pi, 512) +circle_pts = np.c_[RADIUS * np.cos(theta), RADIUS * np.sin(theta), np.zeros(512)] +pl.add_mesh( + pv.Spline(circle_pts, 512), color="black", line_width=2.0, label="level set φ = 0" +) + +pl.add_legend(bcolor="white", border=True, size=(0.42, 0.34)) +pl.camera_position = "xy" +pl.add_title( + f"Quadrature — 2D r={RADIUS} order={ORDER} " + f"∫dA≈{w_area.sum():.4f} [π r²={exact_area:.4f}] " + f"∫ds≈{w_peri.sum():.4f} [2πr={exact_peri:.4f}]", + font_size=9, +) +pl.show() diff --git a/python/demo/quadrature/demo_quadrature_3D.py b/python/demo/quadrature/demo_quadrature_3D.py new file mode 100644 index 0000000..6f40805 --- /dev/null +++ b/python/demo/quadrature/demo_quadrature_3D.py @@ -0,0 +1,196 @@ +""" +Quadrature point visualisation — 3D (tetrahedral mesh) +====================================================== +Creates a background tetrahedral mesh inside the box [-1, 1]^3 and cuts it +with a spherical level-set phi(x) = ||x|| - r. + +For every qualifying cell runtime_quadrature produces reference-space +quadrature rules; physical_points maps them to the physical domain. + +The plot shows: + - clipped background mesh (light grey wireframe) + - cut cells (red, semi-transparent) — one half of the sphere, clipped + - quadrature points (glyphs sized and coloured by weight) + - a translucent sphere surface at the interface +""" + +import numpy as np +import pyvista as pv +import cutcells + +RADIUS = 0.65 +N = 9 # grid resolution +ORDER = 3 # quadrature order +CUT_TYPE = "phi<0" + + +# --------------------------------------------------------------------------- +# Build mesh +# --------------------------------------------------------------------------- + + +def sphere_ls(pts: np.ndarray) -> np.ndarray: + """Signed distance to a sphere of radius RADIUS centred at the origin.""" + return np.sqrt((pts**2).sum(axis=1)) - RADIUS + + +def build_box_mesh(N: int) -> pv.UnstructuredGrid: + x = np.linspace(-1.0, 1.0, N) + y = np.linspace(-1.0, 1.0, N) + z = np.linspace(-1.0, 1.0, N) + xx, yy, zz = np.meshgrid(x, y, z) + pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + grid = pv.UnstructuredGrid(pv.PolyData(pts).delaunay_3d()) + return grid + + +grid = build_box_mesh(N) + +points = np.asarray(grid.points, dtype=np.float64) +ls_values = sphere_ls(points) +points_flat = points.ravel() +connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) +offset = np.asarray(grid.offset, dtype=np.int32) +celltypes = np.asarray(grid.celltypes, dtype=np.int32) + +# --------------------------------------------------------------------------- +# Quadrature via runtime_quadrature +# --------------------------------------------------------------------------- + +rules = cutcells.runtime_quadrature( + ls_values, points_flat, connectivity, offset, celltypes, CUT_TYPE, True, ORDER +) + +phys_pts = cutcells.physical_points( + rules, points_flat, connectivity, offset, celltypes +).reshape(-1, 3) + +weights = np.asarray(rules.weights) +parent_map = np.asarray(rules.parent_map) + +exact = (4.0 / 3.0) * np.pi * RADIUS**3 +print(f"Mesh: {grid.n_cells} cells (N = {N})") +print(f"Quadrature rules: {len(parent_map)} rules, {len(weights)} total points") +print(f"∫1 dV ≈ {weights.sum():.6f} (exact (4/3)π r³ = {exact:.6f})") + +# --------------------------------------------------------------------------- +# Classify cells for colouring +# --------------------------------------------------------------------------- + +inside_ids = cutcells.locate_cells( + ls_values, points_flat, connectivity, offset, celltypes, CUT_TYPE +) +# Cut cells: at least one vertex inside, at least one outside +n_cells = grid.n_cells +cell_min_ls = np.zeros(n_cells) +cell_max_ls = np.zeros(n_cells) +for ci in range(n_cells): + vids = connectivity[offset[ci] : offset[ci + 1]] + cell_min_ls[ci] = ls_values[vids].min() + cell_max_ls[ci] = ls_values[vids].max() +cut_parent_ids = np.where((cell_min_ls < 0) & (cell_max_ls > 0))[0].astype(np.int64) + +# --------------------------------------------------------------------------- +# PyVista visualisation +# --------------------------------------------------------------------------- + +# Clipping plane (y = 0: show x > 0 half for interior view) +clip_normal = (0.0, -1.0, 0.0) +clip_origin = (0.0, 0.0, 0.0) + +# Analytic sphere surface +sphere_surf = pv.Sphere(radius=RADIUS, theta_resolution=60, phi_resolution=60) + +# Quadrature points on the selected half +mask = phys_pts[:, 1] >= 0.0 +phys_half = phys_pts[mask] +weights_half = weights[mask] + +qpts_cloud = pv.PolyData(phys_half) +qpts_cloud["weight"] = weights_half + +glyph_geom = pv.Sphere(radius=1.0, theta_resolution=8, phi_resolution=8) +w_max = weights_half.max() if len(weights_half) else 1.0 +qpts_cloud["radius"] = 0.008 + 0.018 * (weights_half / w_max) + +pl = pv.Plotter(window_size=(1300, 1000)) +pl.set_background("white") + +# Clipped background mesh (wireframe) +grid_clipped = grid.clip(normal=clip_normal, origin=clip_origin, invert=True) +pl.add_mesh( + grid_clipped, style="wireframe", color="#cccccc", line_width=0.4, opacity=0.5 +) + +# Inside cells (clipped half) +if len(inside_ids) > 0: + inside_mesh = grid.extract_cells(inside_ids) + inside_clipped = inside_mesh.clip( + normal=clip_normal, origin=clip_origin, invert=True + ) + pl.add_mesh( + inside_clipped, + color="#80b3ff", + opacity=0.25, + show_edges=True, + edge_color="steelblue", + line_width=0.6, + label="inside cells", + ) + +# Cut cells (clipped half) +if len(cut_parent_ids) > 0: + cut_mesh = grid.extract_cells(cut_parent_ids) + cut_clipped = cut_mesh.clip(normal=clip_normal, origin=clip_origin, invert=True) + pl.add_mesh( + cut_clipped, + color="#ffaaaa", + opacity=0.40, + show_edges=True, + edge_color="#cc4444", + line_width=0.8, + label="cut cells", + ) + +# Sphere surface (translucent) +sphere_clipped = sphere_surf.clip(normal=clip_normal, origin=clip_origin, invert=True) +pl.add_mesh( + sphere_clipped, + color="lightyellow", + opacity=0.35, + show_edges=False, + label="level set", +) +# Outline ring at the clip plane +sphere_ring = sphere_surf.clip(normal=clip_normal, origin=clip_origin) +pl.add_mesh(sphere_ring, style="wireframe", color="gold", line_width=1.2, opacity=0.6) + +# Quadrature point glyphs (visible half only) +if len(phys_half) > 0: + glyphs = qpts_cloud.glyph(geom=glyph_geom, scale="radius", orient=False) + pl.add_mesh( + glyphs, + scalars="weight", + cmap="plasma", + show_scalar_bar=True, + scalar_bar_args={ + "title": "weight", + "n_labels": 4, + "position_x": 0.82, + "position_y": 0.25, + }, + label="quadrature pts", + ) + +pl.add_legend(bcolor="white", border=True, size=(0.25, 0.20), loc="upper right") +pl.add_title( + f"Quadrature points — 3D (r = {RADIUS}, order = {ORDER}, " + f"∫1 dV ≈ {weights.sum():.4f} [exact {exact:.4f}])", + font_size=10, +) +pl.camera_position = [ + (2.5, 2.0, 1.8), + (0.0, 0.0, 0.0), + (0.0, 0.0, 1.0), +] +pl.show() diff --git a/python/tests/test_mapping.py b/python/tests/test_mapping.py new file mode 100644 index 0000000..c5d1cc0 --- /dev/null +++ b/python/tests/test_mapping.py @@ -0,0 +1,223 @@ +# Copyright (c) 2024 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +"""Tests for mapping.h / compute_physical_cut_vertices & complete_from_physical. + +Design: + - CutCells normally cuts in PHYSICAL space: + _vertex_coords from cut() = physical coordinates. + To obtain the reference coordinates: call complete_from_physical(). + -> copies _vertex_coords -> _vertex_coords_phys (physical) + -> pulls back to fill _vertex_coords with reference coords + + - complete_from_physical (the standard workflow): + After: _vertex_coords = reference, _vertex_coords_phys = physical. + + - compute_physical_cut_vertices (reference-space cut workflow): + If _vertex_coords were already in reference coords, push them to physical. + After: _vertex_coords = unchanged (reference), _vertex_coords_phys = physical. + +Coverage: + - complete_from_physical: pull-back produces correct reference coordinates. + - compute_physical_cut_vertices: push-forward from reference to physical. + - round-trip consistency. +""" + +import numpy as np +import pytest +import cutcells + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _unit_triangle(): + """Unit right triangle (0,0),(1,0),(0,1) in physical/reference space.""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + return vertex_coords, 2 + + +def _scaled_triangle(sx, sy): + """Triangle (0,0),(sx,0),(0,sy) — physical coords differ from reference.""" + vertex_coords = np.array([0.0, 0.0, sx, 0.0, 0.0, sy], dtype=np.float64) + return vertex_coords, 2 + + +def _standard_tet(): + """Standard tetrahedron: (0,0,0),(1,0,0),(0,1,0),(0,0,1).""" + vertex_coords = np.array( + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], dtype=np.float64 + ) + return vertex_coords, 3 + + +# --------------------------------------------------------------------------- +# 1. complete_from_physical (standard workflow: cut in physical space) +# --------------------------------------------------------------------------- + + +class TestCompleteFromPhysical: + """cut() is performed with physical vertex coordinates. + complete_from_physical() sets _vertex_coords_phys = copy of _vertex_coords + (i.e. the physical cut coords), then pulls back to fill _vertex_coords with + reference coords.""" + + def test_unit_triangle_identity(self): + """For the unit triangle (identity map) ref == phys after call.""" + vertex_coords, gdim = _unit_triangle() + ls = np.array([0.5, -0.5, 0.5], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, gdim, ls, "phi<0", True + ) + assert cut_cell.vertex_coords_phys.size == 0, ( + "vertex_coords_phys must be empty before complete_from_physical" + ) + phys_from_cutter = cut_cell.vertex_coords.copy() + + cutcells.complete_from_physical(cut_cell) + + vc_ref = cut_cell.vertex_coords + vc_phys = cut_cell.vertex_coords_phys + + # physical should be what came out of the cutter + np.testing.assert_allclose( + vc_phys, phys_from_cutter.reshape(-1, gdim), atol=1e-14 + ) + # reference == physical for identity map + np.testing.assert_allclose( + vc_ref, phys_from_cutter.reshape(-1, gdim), atol=1e-14 + ) + + def test_scaled_triangle_pull_back(self): + """Scaled triangle (3×2): pull-back yields ref = phys / scale.""" + sx, sy = 3.0, 2.0 + vertex_coords, gdim = _scaled_triangle(sx, sy) + ls = np.array([0.1, -0.1, 0.2], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, gdim, ls, "phi<0", True + ) + phys_from_cutter = cut_cell.vertex_coords.copy() + + cutcells.complete_from_physical(cut_cell) + + vc_ref = cut_cell.vertex_coords + vc_phys = cut_cell.vertex_coords_phys + + np.testing.assert_allclose( + vc_phys, phys_from_cutter.reshape(-1, gdim), atol=1e-13 + ) + # For affine map v0=0, J=diag(sx,sy): ref = phys / [sx, sy] + expected_ref = phys_from_cutter.reshape(-1, gdim) / np.array([sx, sy]) + np.testing.assert_allclose(vc_ref, expected_ref, atol=1e-13) + + def test_tetrahedron_pull_back(self): + """Standard tet (identity map): ref == phys after complete_from_physical.""" + vertex_coords, gdim = _standard_tet() + ls = np.array([0.1, -0.1, 0.2, 0.2], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.tetrahedron, vertex_coords, gdim, ls, "phi<0", True + ) + phys_from_cutter = cut_cell.vertex_coords.copy() + + cutcells.complete_from_physical(cut_cell) + + np.testing.assert_allclose( + cut_cell.vertex_coords_phys, phys_from_cutter.reshape(-1, gdim), atol=1e-14 + ) + np.testing.assert_allclose( + cut_cell.vertex_coords, phys_from_cutter.reshape(-1, gdim), atol=1e-14 + ) + + def test_phys_preserved_after_first_call(self): + """complete_from_physical preserves the original physical cut coords in + vertex_coords_phys and converts vertex_coords to reference coordinates. + Calling it a second time would further pull back (not idempotent) — verify + that only the first call gives meaningful physical-space vertex_coords_phys.""" + vertex_coords, gdim = _scaled_triangle(2.5, 1.5) + ls = np.array([0.5, -0.5, 0.5], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, gdim, ls, "phi<0", True + ) + phys_original = cut_cell.vertex_coords.copy() # physical from cut() + + cutcells.complete_from_physical(cut_cell) + + # After first call: _vertex_coords_phys == original physical cut coords + np.testing.assert_allclose( + cut_cell.vertex_coords_phys, + phys_original.reshape(-1, gdim), + atol=1e-13, + err_msg="vertex_coords_phys must equal original physical cut coords", + ) + # _vertex_coords must have been pulled back + expected_ref = phys_original.reshape(-1, gdim) / np.array([2.5, 1.5]) + np.testing.assert_allclose(cut_cell.vertex_coords, expected_ref, atol=1e-13) + + +# --------------------------------------------------------------------------- +# 2. compute_physical_cut_vertices (reference-frame cut workflow) +# --------------------------------------------------------------------------- + + +class TestComputePhysicalCutVertices: + """compute_physical_cut_vertices pushes _vertex_coords (treated as reference) + to physical space using the affine map defined by _parent_vertex_coords. + + For the standard physical-space cutting workflow, _vertex_coords are already + physical, so compute_physical_cut_vertices would push them *again* (incorrect + usage). The correct function in that case is complete_from_physical. + + Here we test the correct usage: cutting with reference (unit) vertex coordinates + and then pushing forward to physical.""" + + def test_unit_triangle_identity(self): + """For identity map push-forward gives phys == ref.""" + vertex_coords, gdim = _unit_triangle() + ls = np.array([0.5, -0.5, 0.5], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, gdim, ls, "phi<0", True + ) + assert cut_cell.vertex_coords_phys.size == 0 + + cutcells.compute_physical_cut_vertices(cut_cell) + + vc_ref = cut_cell.vertex_coords.reshape(-1, gdim) + vc_phys = cut_cell.vertex_coords_phys + np.testing.assert_allclose(vc_phys, vc_ref, atol=1e-14) + + def test_tetrahedron_identity(self): + """Standard tet (identity map): phys == ref.""" + vertex_coords, gdim = _standard_tet() + ls = np.array([0.1, -0.1, 0.2, 0.2], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.tetrahedron, vertex_coords, gdim, ls, "phi<0", True + ) + cutcells.compute_physical_cut_vertices(cut_cell) + + vc_ref = cut_cell.vertex_coords.reshape(-1, gdim) + vc_phys = cut_cell.vertex_coords_phys + np.testing.assert_allclose(vc_phys, vc_ref, atol=1e-14) + + def test_idempotent(self): + """Calling compute_physical_cut_vertices twice gives the same physical coords.""" + vertex_coords, gdim = _unit_triangle() + ls = np.array([0.5, -0.5, 0.5], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, gdim, ls, "phi<0", True + ) + cutcells.compute_physical_cut_vertices(cut_cell) + phys1 = cut_cell.vertex_coords_phys.copy() + cutcells.compute_physical_cut_vertices(cut_cell) + phys2 = cut_cell.vertex_coords_phys + np.testing.assert_array_equal(phys1, phys2) diff --git a/python/tests/test_quadrature.py b/python/tests/test_quadrature.py new file mode 100644 index 0000000..72d69c4 --- /dev/null +++ b/python/tests/test_quadrature.py @@ -0,0 +1,309 @@ +# Copyright (c) 2024 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +"""Tests for quadrature.h / make_quadrature. + +Coverage: + - QuadratureRules struct (tdim, points, weights, offset, parent_map). + - make_quadrature: full-cell case integrates to correct area/volume. + - make_quadrature: cut-cell case integrates subdomain area. + - Batch interface: multiple cut cells, offset/parent_map metadata. + - Cell types: triangle, tetrahedron, hexahedron. +""" + +import numpy as np +import pytest +import cutcells + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_and_enrich( + cell_type, vertex_coords, gdim, ls_values, cut_type, triangulate=True +): + """Cut (physical coords), complete frame (complete_from_physical), return enriched CutCell.""" + cut_cell = cutcells.cut( + cell_type, vertex_coords, gdim, ls_values, cut_type, triangulate + ) + cutcells.complete_from_physical(cut_cell) + return cut_cell + + +def _integrate(cut_cells_list, order=3): + """Return QuadratureRules and summed weight for cut_cells_list.""" + qr = cutcells.make_quadrature(cut_cells_list, order) + return qr, np.sum(qr.weights) + + +# --------------------------------------------------------------------------- +# 1. QuadratureRules struct +# --------------------------------------------------------------------------- + + +class TestQuadratureRulesStruct: + def test_default_empty(self): + qr = cutcells._cutcellscpp.QuadratureRules_float64() + assert qr.tdim == 0 + assert qr.points.size == 0 + assert qr.weights.size == 0 + assert qr.offset.size == 0 + assert qr.parent_map.size == 0 + + def test_types(self): + """After make_quadrature the arrays have expected dtypes.""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + ls = np.array([0.1, -0.1, 0.2], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + qr = cutcells.make_quadrature([cut_cell], 2) + assert qr.points.dtype == np.float64 + assert qr.weights.dtype == np.float64 + assert qr.offset.dtype == np.int32 + assert qr.parent_map.dtype == np.int32 + + +# --------------------------------------------------------------------------- +# 2. Full-cell integration (no cut) +# --------------------------------------------------------------------------- + + +class TestFullCellIntegration: + """Barely-intersected cells (epsilon level-set) must integrate to ~full area/volume. + A level-set like [-1,-1,...,+eps] creates a tiny cut but leaves essentially the full + interior intact, so the integral must be close to the full cell measure.""" + + def test_triangle_full_cell_area(self): + """Unit right triangle has area 0.5.""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + # Tiny positive at one vertex: interior ~= full triangle + eps = 1e-4 + ls = np.array([-1.0, -1.0, eps], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=4) + np.testing.assert_allclose(total, 0.5, rtol=1e-3) + + def test_triangle_scaled_area(self): + """Scaled triangle (3×2) has area 3.0.""" + vertex_coords = np.array([0.0, 0.0, 3.0, 0.0, 0.0, 2.0], dtype=np.float64) + eps = 1e-4 + ls = np.array([-1.0, -1.0, eps], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=4) + np.testing.assert_allclose(total, 3.0, rtol=1e-3) + + def test_tetrahedron_full_cell_volume(self): + """Standard tet has volume 1/6.""" + vertex_coords = np.array( + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + dtype=np.float64, + ) + eps = 1e-4 + ls = np.array([-1.0, -1.0, -1.0, eps], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.tetrahedron, vertex_coords, 3, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=3) + np.testing.assert_allclose(total, 1.0 / 6.0, rtol=1e-3) + + def test_hexahedron_full_cell_volume(self): + """Unit cube has volume 1.0. + + VTK hexahedron vertex ordering: + v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) + v4=(0,0,1) v5=(1,0,1) v6=(1,1,1) v7=(0,1,1) + """ + vertex_coords = np.array( + [ + 0.0, + 0.0, + 0.0, # v0 + 1.0, + 0.0, + 0.0, # v1 + 1.0, + 1.0, + 0.0, # v2 + 0.0, + 1.0, + 0.0, # v3 + 0.0, + 0.0, + 1.0, # v4 + 1.0, + 0.0, + 1.0, # v5 + 1.0, + 1.0, + 1.0, # v6 + 0.0, + 1.0, + 1.0, # v7 + ], + dtype=np.float64, + ) + eps = 1e-4 + # v6=(1,1,1) has eps>0; all others negative → phi<0 is almost the full cube + ls = np.array([-1.0] * 6 + [eps] + [-1.0], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.hexahedron, vertex_coords, 3, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=3) + np.testing.assert_allclose(total, 1.0, rtol=1e-3) + + +# --------------------------------------------------------------------------- +# 3. Cut-cell integration +# --------------------------------------------------------------------------- + + +class TestCutCellIntegration: + """The interior cut of a known fraction must integrate to that fraction.""" + + def test_triangle_half_cut(self): + """Level set x - 0.5 = 0 cuts the unit triangle. + The interior phi<0 part has area 0.5 * 0.5 * (1 - 0.5^2) ... compute + analytically: triangle with vertices (0,0),(0.5,0),(0,1),(... tricky). + Use the known vol from test_triangle.py instead: ls=[0.1,-0.1,0.2] -> 1/12.""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0], dtype=np.float64) + ls = np.array([0.1, -0.1, 0.2], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=5) + np.testing.assert_allclose(total, 1.0 / 12.0, rtol=1e-9) + + def test_tetrahedron_cut(self): + """Known tet cut: ls=[0.1,-0.1,0.2,0.2] interior vol = 4/27.""" + vertex_coords = np.array( + [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0], + dtype=np.float64, + ) + ls = np.array([0.1, -0.1, 0.2, 0.2], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.tetrahedron, vertex_coords, 3, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=5) + np.testing.assert_allclose(total, 4.0 / 27.0, rtol=1e-8) + + @pytest.mark.parametrize("order", [1, 2, 3, 4, 5]) + def test_triangle_order_convergence(self, order): + """All orders should give the exact answer for a linear level-set cut + because the sub-cells are simplices (exact for polynomials of any order).""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0], dtype=np.float64) + ls = np.array([0.1, -0.1, 0.2], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + _, total = _integrate([cut_cell], order=order) + np.testing.assert_allclose( + total, 1.0 / 12.0, rtol=1e-9, err_msg=f"order={order} failed" + ) + + +# --------------------------------------------------------------------------- +# 4. Batch interface +# --------------------------------------------------------------------------- + + +class TestBatchIntegration: + """make_quadrature([c1, c2, ...]) must produce correct offset/parent_map.""" + + def _make_triangle_cells(self, n_cells): + """n_cells copies of a half-cut unit triangle.""" + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float64) + ls = np.array([0.5, -0.5, 0.5], dtype=np.float64) + cells = [] + for _ in range(n_cells): + c = _make_and_enrich( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0" + ) + cells.append(c) + return cells + + def test_offset_length(self): + cells = self._make_triangle_cells(4) + qr = cutcells.make_quadrature(cells, 3) + assert len(qr.offset) == len(cells) + 1, "offset length must be num_rules + 1" + assert qr.offset[0] == 0, "offset[0] must be 0" + assert qr.offset[-1] == len(qr.weights), ( + "last offset must equal total number of quadrature points" + ) + + def test_parent_map_length(self): + cells = self._make_triangle_cells(3) + qr = cutcells.make_quadrature(cells, 3) + assert len(qr.parent_map) == len(cells) + + def test_parent_map_values(self): + cells = self._make_triangle_cells(3) + qr = cutcells.make_quadrature(cells, 3) + np.testing.assert_array_equal(qr.parent_map, [0, 1, 2]) + + def test_batch_sum_equals_individual_sum(self): + cells = self._make_triangle_cells(5) + qr_batch = cutcells.make_quadrature(cells, 3) + batch_total = np.sum(qr_batch.weights) + + individual_total = 0.0 + for c in cells: + qr_i = cutcells.make_quadrature([c], 3) + individual_total += np.sum(qr_i.weights) + + np.testing.assert_allclose(batch_total, individual_total, rtol=1e-14) + + def test_points_shape(self): + cells = self._make_triangle_cells(2) + qr = cutcells.make_quadrature(cells, 3) + total_pts = qr.offset[-1] + assert qr.points.size == total_pts * qr.tdim, ( + "flat points array size must be total_pts * tdim" + ) + + def test_tdim_triangle(self): + cells = self._make_triangle_cells(1) + qr = cutcells.make_quadrature(cells, 2) + assert qr.tdim == 2, "tdim for triangle parent should be 2" + + def test_tdim_tetrahedron(self): + vertex_coords = np.array( + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + dtype=np.float64, + ) + ls = np.array([0.1, -0.1, 0.2, 0.2], dtype=np.float64) + cut_cell = _make_and_enrich( + cutcells.CellType.tetrahedron, vertex_coords, 3, ls, "phi<0" + ) + qr = cutcells.make_quadrature([cut_cell], 2) + assert qr.tdim == 3 + + +# --------------------------------------------------------------------------- +# 5. Float32 variant +# --------------------------------------------------------------------------- + + +class TestFloat32: + """Smoke-test the float32 code path.""" + + def test_triangle_full_cell_float32(self): + vertex_coords = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float32) + ls = np.array([0.1, -0.1, 0.2], dtype=np.float32) + cut_cell = cutcells.cut( + cutcells.CellType.triangle, vertex_coords, 2, ls, "phi<0", True + ) + cutcells.complete_from_physical(cut_cell) + qr = cutcells.make_quadrature([cut_cell], 3) + total = np.sum(qr.weights) + np.testing.assert_allclose(total, 1.0 / 12.0, rtol=1e-4) + assert qr.points.dtype == np.float32 + assert qr.weights.dtype == np.float32 diff --git a/python/tests/test_runtime_quadrature.py b/python/tests/test_runtime_quadrature.py new file mode 100644 index 0000000..54d9607 --- /dev/null +++ b/python/tests/test_runtime_quadrature.py @@ -0,0 +1,166 @@ +"""Pytest suite for runtime_quadrature and physical_points.""" + +import numpy as np +import pytest +import cutcells + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _full_cell_rules(pts, conn_indices, vtk_type_id, triangulate, order=4): + """Return QuadratureRules for a fully-inside cell.""" + pts_arr = np.array(pts, dtype=np.float64).flatten() + conn = np.array(conn_indices, dtype=np.int32) + offs = np.array([0, len(conn_indices)], dtype=np.int32) + vtype = np.array([vtk_type_id], dtype=np.int32) + ls = np.full(len(pts), -1.0, dtype=np.float64) # all inside + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtype, "phi<0", triangulate, order + ) + phys = cutcells.physical_points(rules, pts_arr, conn, offs, vtype) + return rules, phys.reshape(-1, 3) + + +# --------------------------------------------------------------------------- +# Test data: (name, pts, conn, vtk_id, expected_vol) +# --------------------------------------------------------------------------- + +INTERVAL_PTS = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]] +TRI_PTS = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] +TET_PTS = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] +QUAD_PTS = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]] +HEX_PTS = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + [1.0, 1.0, 1.0], + [0.0, 1.0, 1.0], +] +PRISM_PTS = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + [0.0, 1.0, 1.0], +] +PYR_PTS = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 1.0, 0.0], + [0.0, 1.0, 0.0], + [0.5, 0.5, 1.0], +] + +CELL_CASES = [ + # (name, pts, conn, vtk_id, expected_volume) + ("interval", INTERVAL_PTS, [0, 1], 3, 2.0), + ("triangle", TRI_PTS, [0, 1, 2], 5, 0.5), + ("tetrahedron", TET_PTS, [0, 1, 2, 3], 10, 1.0 / 6.0), + ("quad", QUAD_PTS, [0, 1, 2, 3], 9, 1.0), + ("hexahedron", HEX_PTS, list(range(8)), 12, 1.0), + ("prism", PRISM_PTS, list(range(6)), 13, 0.5), + ("pyramid", PYR_PTS, list(range(5)), 14, 1.0 / 3.0), +] + + +# --------------------------------------------------------------------------- +# Parametrized volume tests +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("name,pts,conn,vtk_id,expected_vol", CELL_CASES) +@pytest.mark.parametrize("triangulate", [False, True]) +def test_full_cell_volume(name, pts, conn, vtk_id, expected_vol, triangulate): + """Sum of quadrature weights equals the physical volume of the cell.""" + rules, _ = _full_cell_rules(pts, conn, vtk_id, triangulate) + assert abs(rules.weights.sum() - expected_vol) < 1e-9, ( + f"{name} triangulate={triangulate}: " + f"got {rules.weights.sum()}, expected {expected_vol}" + ) + + +@pytest.mark.parametrize("name,pts,conn,vtk_id,expected_vol", CELL_CASES) +@pytest.mark.parametrize("triangulate", [False, True]) +def test_full_cell_physical_points_shape( + name, pts, conn, vtk_id, expected_vol, triangulate +): + """physical_points returns array of shape (nq, 3).""" + rules, phys = _full_cell_rules(pts, conn, vtk_id, triangulate) + assert phys.shape == (len(rules.weights), 3) + + +# --------------------------------------------------------------------------- +# Cut cell tests (tet only — requires tdim == gdim == 3) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "cut_type,frac", + [ + ("phi<0", 1.0 / 6.0), # cut from one corner; vol < full tet + ("phi>0", 1.0 / 6.0), # complement + ], +) +def test_cut_tet_volume_sign(cut_type, frac): + """Cut tet quadrature produces sensible weights (sum < full volume).""" + pts_arr = np.array(TET_PTS, dtype=np.float64).flatten() + ls = np.array([-1.0, 1.0, 1.0, 1.0], dtype=np.float64) # 1 vertex inside + conn = np.array([0, 1, 2, 3], dtype=np.int32) + offs = np.array([0, 4], dtype=np.int32) + vtype = np.array([10], dtype=np.int32) + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtype, cut_type, True, 3 + ) + s = rules.weights.sum() + assert 0 < s < frac + 1e-10 + + +def test_cut_tet_physical_points(): + """physical_points for a cut tet has correct shape.""" + pts_arr = np.array(TET_PTS, dtype=np.float64).flatten() + ls = np.array([-1.0, 1.0, 1.0, 1.0], dtype=np.float64) + conn = np.array([0, 1, 2, 3], dtype=np.int32) + offs = np.array([0, 4], dtype=np.int32) + vtype = np.array([10], dtype=np.int32) + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtype, "phi<0", True, 3 + ) + phys = cutcells.physical_points(rules, pts_arr, conn, offs, vtype) + assert phys.reshape(-1, 3).shape[1] == 3 + + +# --------------------------------------------------------------------------- +# Fully outside → empty result +# --------------------------------------------------------------------------- + + +def test_fully_outside_empty(): + """A cell with all ls > 0 and cut_type='phi<0' produces zero quadrature points.""" + pts_arr = np.array(TET_PTS, dtype=np.float64).flatten() + ls = np.full(4, 1.0, dtype=np.float64) # all outside + conn = np.array([0, 1, 2, 3], dtype=np.int32) + offs = np.array([0, 4], dtype=np.int32) + vtype = np.array([10], dtype=np.int32) + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtype, "phi<0", True, 3 + ) + assert len(rules.weights) == 0 + + +def test_fully_inside_all_ls_negative(): + """A cell with all ls < 0 and cut_type='phi<0' gives the full volume.""" + pts_arr = np.array(TET_PTS, dtype=np.float64).flatten() + ls = np.full(4, -1.0, dtype=np.float64) + conn = np.array([0, 1, 2, 3], dtype=np.int32) + offs = np.array([0, 4], dtype=np.int32) + vtype = np.array([10], dtype=np.int32) + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtype, "phi<0", True, 4 + ) + assert abs(rules.weights.sum() - 1.0 / 6.0) < 1e-9 diff --git a/tablegen/scripts/gen_quadrature_tables.py b/tablegen/scripts/gen_quadrature_tables.py new file mode 100644 index 0000000..ac58080 --- /dev/null +++ b/tablegen/scripts/gen_quadrature_tables.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Offline Basix quadrature table generator for CutCells. + +Usage: + python gen_quadrature_tables.py [MAX_ORDER [OUTPUT_DIR]] + +Defaults: + MAX_ORDER = 10 + OUTPUT_DIR = /cpp/src/generated + +Environment variables (to override Basix lookup paths): + BASIX_PYPATH – path to basix Python sources (e.g. .../basix/python) + BASIX_SO_PATH – path to compiled _basixcpp.so (e.g. .../basix/build/python) +""" + +import sys +import os +import argparse + +# --------------------------------------------------------------------------- +# Basix path bootstrap – use environment or well-known local build. +# --------------------------------------------------------------------------- +_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +_DEFAULT_BASIX_PYTHON = os.path.join(os.path.dirname(_REPO_ROOT), "basix", "python") +_DEFAULT_BASIX_SO = os.path.join( + os.path.dirname(_REPO_ROOT), "basix", "build", "python" +) + +for _p in [ + os.environ.get("BASIX_SO_PATH", _DEFAULT_BASIX_SO), + os.environ.get("BASIX_PYPATH", _DEFAULT_BASIX_PYTHON), +]: + if _p and _p not in sys.path: + sys.path.insert(0, _p) + +import basix # noqa: E402 (must come after path fixup) + +# --------------------------------------------------------------------------- +# Cell-type table: (name, basix.CellType, topological_dimension) +# --------------------------------------------------------------------------- +CELL_TYPES = [ + ("interval", basix.CellType.interval, 1), + ("triangle", basix.CellType.triangle, 2), + ("quadrilateral", basix.CellType.quadrilateral, 2), + ("tetrahedron", basix.CellType.tetrahedron, 3), + ("hexahedron", basix.CellType.hexahedron, 3), + ("prism", basix.CellType.prism, 3), + ("pyramid", basix.CellType.pyramid, 3), +] + +# --------------------------------------------------------------------------- +# Code-generation helpers +# --------------------------------------------------------------------------- + +_HEADER_TMPL = """\ +// AUTO-GENERATED by gen_quadrature_tables.py — do not edit. +// Basix {version} quadrature rules for '{cell_type}', orders 1..{max_order}. +// Regenerate: python tablegen/scripts/gen_quadrature_tables.py {max_order} +// +// SPDX-License-Identifier: MIT +#pragma once + +#include + +namespace cutcells::quadrature::generated +{{ +""" + +_FOOTER = "} // namespace cutcells::quadrature::generated\n" + + +def _fmt_doubles(arr, indent=4): + """Format a flat numpy array as a C++ double initialiser list (no braces).""" + vals = arr.ravel().tolist() + pad = " " * indent + cols = 4 # values per source line + lines = [] + for i in range(0, len(vals), cols): + chunk = vals[i : i + cols] + lines.append(pad + ", ".join(f"{v:.18e}" for v in chunk)) + return ",\n".join(lines) + + +def _write_cell_header(cell_name, basix_ct, tdim, max_order, out_dir): + fname = os.path.join(out_dir, f"quadrature_tables_{cell_name}.h") + with open(fname, "w") as fh: + fh.write( + _HEADER_TMPL.format( + version=basix.__version__, + cell_type=cell_name, + max_order=max_order, + ) + ) + for order in range(1, max_order + 1): + pts, wts = basix.make_quadrature(basix_ct, order) + npts = pts.shape[0] + pts_flat = pts.ravel() + + fh.write(f"// ---- order {order}: {npts} point(s), tdim={tdim} ----\n") + fh.write( + f"inline constexpr int {cell_name}_o{order}_npts = {npts};\n" + ) + fh.write( + f"inline constexpr int {cell_name}_o{order}_tdim = {tdim};\n" + ) + fh.write( + f"inline constexpr double {cell_name}_o{order}_points" + f"[{npts * tdim}] = {{\n" + ) + fh.write(_fmt_doubles(pts_flat)) + fh.write("\n};\n") + fh.write( + f"inline constexpr double {cell_name}_o{order}_weights[{npts}] = {{\n" + ) + fh.write(_fmt_doubles(wts)) + fh.write("\n};\n\n") + + fh.write(_FOOTER) + print(f" {fname}") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "max_order", + nargs="?", + type=int, + default=10, + help="Maximum quadrature order to generate (default: 10)", + ) + parser.add_argument( + "out_dir", + nargs="?", + default=os.path.join(_REPO_ROOT, "cpp", "src", "generated"), + help="Output directory (default: cpp/src/generated)", + ) + args = parser.parse_args() + + out_dir = os.path.abspath(args.out_dir) + os.makedirs(out_dir, exist_ok=True) + + print( + f"Generating Basix {basix.__version__} quadrature tables " + f"(orders 1..{args.max_order}) -> {out_dir}" + ) + for cell_name, basix_ct, tdim in CELL_TYPES: + _write_cell_header(cell_name, basix_ct, tdim, args.max_order, out_dir) + print("Done.") + + +if __name__ == "__main__": + main() From 54d4bd6be436929d7a7befcb4a4100bf5d4e9948 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 14 Mar 2026 16:04:25 +0100 Subject: [PATCH 05/23] add meshview and level set function --- benchmarks/CMakeLists.txt | 65 --- benchmarks/README.md | 47 -- benchmarks/bench_cut_hex.cpp | 96 ---- benchmarks/bench_cut_mesh.cpp | 146 ----- benchmarks/bench_cut_tetrahedron.cpp | 93 ---- benchmarks/bench_cut_triangle.cpp | 91 --- cpp/CMakeLists.txt | 7 +- cpp/src/CMakeLists.txt | 3 + .../generated/quadrature_tables_interval.h | 136 ++--- cpp/src/level_set.h | 69 +++ cpp/src/mesh_view.h | 88 +++ python/cutcells/__init__.py | 7 + python/cutcells/wrapper.cpp | 380 ++++++++++++- python/demo/bench_cut_pipeline.py | 520 ++++++++++++++++++ python/demo/meshview_levelset_demo.py | 325 +++++++++++ tablegen/cutcells_tablegen/emit_c_headers.py | 4 +- tablegen/scripts/gen_quadrature_tables.py | 22 +- 17 files changed, 1456 insertions(+), 643 deletions(-) delete mode 100644 benchmarks/CMakeLists.txt delete mode 100644 benchmarks/README.md delete mode 100644 benchmarks/bench_cut_hex.cpp delete mode 100644 benchmarks/bench_cut_mesh.cpp delete mode 100644 benchmarks/bench_cut_tetrahedron.cpp delete mode 100644 benchmarks/bench_cut_triangle.cpp create mode 100644 cpp/src/level_set.h create mode 100644 cpp/src/mesh_view.h create mode 100644 python/demo/bench_cut_pipeline.py create mode 100644 python/demo/meshview_levelset_demo.py diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt deleted file mode 100644 index b125227..0000000 --- a/benchmarks/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ -cmake_minimum_required(VERSION 3.21) - -if(CMAKE_CONFIGURATION_TYPES) - message(FATAL_ERROR "Benchmarks are supported only for single-config Release builds. Configure with -DCMAKE_BUILD_TYPE=Release.") -endif() - -if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") - message(FATAL_ERROR "Benchmarks compile only in Release mode. Set -DCMAKE_BUILD_TYPE=Release.") -endif() - -include(FetchContent) -find_package(benchmark QUIET) - -if(NOT benchmark_FOUND) - FetchContent_Declare( - benchmark - GIT_REPOSITORY https://github.com/google/benchmark.git - GIT_TAG v1.9.1 - ) - set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(benchmark) -endif() - -function(add_cutcells_benchmark target source_file) - add_executable(${target} ${source_file}) - target_link_libraries(${target} PRIVATE cutcells benchmark::benchmark) - target_compile_features(${target} PRIVATE cxx_std_20) - # On macOS, CMake embeds the Conda lib dir (from the transitive OpenMP - # dependency) in the binary RPATH before the build-tree src path, causing - # the OS to load the *installed* libcutcells instead of the freshly-built one. - # - # Post-build: run a cmake -P script that uses otool to find every - # Conda/miniforge RPATH entry and strips it with install_name_tool. - # The build-tree path (build-bench-release/src) that CMake already embeds - # then becomes the first — and only — place the OS looks for libcutcells. - if(APPLE) - add_custom_command(TARGET ${target} POST_BUILD - COMMAND ${CMAKE_COMMAND} - -D "TARGET_FILE=$" - -P "${CMAKE_CURRENT_LIST_DIR}/fix_benchmark_rpath.cmake" - COMMENT "Stripping conda rpaths from ${target}" - ) - endif() -endfunction() - -add_cutcells_benchmark(bench_cut_triangle bench_cut_triangle.cpp) -add_cutcells_benchmark(bench_cut_tetrahedron bench_cut_tetrahedron.cpp) -add_cutcells_benchmark(bench_cut_hex bench_cut_hex.cpp) -add_cutcells_benchmark(bench_cut_mesh bench_cut_mesh.cpp) - -# run_benchmarks: also set DYLD_LIBRARY_PATH as an extra safety net in case the -# rpath ordering is still overridden by the system linker. -add_custom_target(run_benchmarks - COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" - $ - COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" - $ - COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" - $ - COMMAND ${CMAKE_COMMAND} -E env "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/src" - $ - DEPENDS bench_cut_triangle bench_cut_tetrahedron bench_cut_hex bench_cut_mesh - USES_TERMINAL - COMMENT "Build and run all CutCells benchmarks" -) diff --git a/benchmarks/README.md b/benchmarks/README.md deleted file mode 100644 index 0eb92a6..0000000 --- a/benchmarks/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# CutCells Benchmarks - -This folder contains Google Benchmark executables for: - -- `cut_triangle` -- `cut_tetrahedron` -- `cut_hexahedron` -- `cut_vtk_mesh` -- `create_cut_mesh` - -Each benchmark uses `benchmark::State` and performs `1e6` cut operations per state iteration. - -## Build (Release only) - -Benchmarks are intentionally enabled only in Release mode. - -```bash -cd /Users/sclaus/Workspace/FEniCSx0.10/CutCells/cpp -cmake -S . -B build-bench-release -DCMAKE_BUILD_TYPE=Release -cmake --build build-bench-release --target bench_cut_triangle bench_cut_tetrahedron bench_cut_hex bench_cut_mesh -j -``` - -## Run individual benchmarks - -```bash -./build-bench-release/benchmarks/bench_cut_triangle -./build-bench-release/benchmarks/bench_cut_tetrahedron -./build-bench-release/benchmarks/bench_cut_hex -./build-bench-release/benchmarks/bench_cut_mesh -``` - -## Run all benchmarks with one target - -Use the aggregate target: - -```bash -cmake --build build-bench-release --target run_benchmarks -j -``` - -## Useful Google Benchmark flags - -Examples: - -```bash -./build-bench-release/benchmarks/bench_cut_hex --benchmark_filter=CutHexahedron -./build-bench-release/benchmarks/bench_cut_mesh --benchmark_repetitions=5 --benchmark_report_aggregates_only=true -``` diff --git a/benchmarks/bench_cut_hex.cpp b/benchmarks/bench_cut_hex.cpp deleted file mode 100644 index caed254..0000000 --- a/benchmarks/bench_cut_hex.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "cut_cell.h" -#include "cut_hexahedron.h" - -namespace -{ -constexpr int kRepeats = 1'000'000; - -std::array make_random_hexahedron(std::mt19937_64& rng) -{ - std::uniform_real_distribution shift(-10.0, 10.0); - std::uniform_real_distribution len(0.1, 2.0); - - const double x0 = shift(rng); - const double y0 = shift(rng); - const double z0 = shift(rng); - const double lx = len(rng); - const double ly = len(rng); - const double lz = len(rng); - - return { - x0, y0, z0, - x0 + lx, y0, z0, - x0 + lx, y0 + ly, z0, - x0, y0 + ly, z0, - x0, y0, z0 + lz, - x0 + lx, y0, z0 + lz, - x0 + lx, y0 + ly, z0 + lz, - x0, y0 + ly, z0 + lz, - }; -} - -std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) -{ - std::uniform_real_distribution w(-1.0, 1.0); - std::uniform_real_distribution d(-1.0, 1.0); - - std::array ls{}; - for (;;) - { - const double nx = w(rng); - const double ny = w(rng); - const double nz = w(rng); - const double shift = d(rng); - - bool has_pos = false; - bool has_neg = false; - bool near_zero = false; - - for (int i = 0; i < 8; ++i) - { - const double x = coords[i * 3 + 0]; - const double y = coords[i * 3 + 1]; - const double z = coords[i * 3 + 2]; - ls[i] = nx * x + ny * y + nz * z + shift; - - has_pos = has_pos || (ls[i] > 0.0); - has_neg = has_neg || (ls[i] < 0.0); - near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); - } - - if (has_pos && has_neg && !near_zero) - return ls; - } -} -} // namespace - -static void BM_CutHexahedron(benchmark::State& state) -{ - std::mt19937_64 rng(20260307); - - const auto coords = make_random_hexahedron(rng); - const auto ls = make_intersecting_ls(coords, rng); - - for (auto _ : state) - { - cutcells::cell::CutCell out; - for (int i = 0; i < kRepeats; ++i) - { - cutcells::cell::hexahedron::cut(coords, 3, ls, "phi=0", out, false); - benchmark::DoNotOptimize(out); - } - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); -} - -BENCHMARK(BM_CutHexahedron); -BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_mesh.cpp b/benchmarks/bench_cut_mesh.cpp deleted file mode 100644 index 6cc8337..0000000 --- a/benchmarks/bench_cut_mesh.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include "cell_types.h" -#include "cut_hexahedron.h" -#include "cut_mesh.h" - -namespace -{ -constexpr int kRepeatsCutVtkMesh = 10'000; -constexpr int kRepeatsCreateCutMesh = 10'000; - -struct RandomHexCellData -{ - std::array points; - std::array connectivity; - std::array offset; - std::array vtk_type; - std::array ls; -}; - -RandomHexCellData make_random_hex_cell_data(std::mt19937_64& rng) -{ - std::uniform_real_distribution shift(-10.0, 10.0); - std::uniform_real_distribution len(0.1, 2.0); - std::uniform_real_distribution w(-1.0, 1.0); - std::uniform_real_distribution d(-1.0, 1.0); - - RandomHexCellData data{ - .points = {}, - .connectivity = {0, 1, 2, 3, 4, 5, 6, 7}, - .offset = {0, 8}, - .vtk_type = {static_cast(cutcells::cell::vtk_types::VTK_HEXAHEDRON)}, - .ls = {}, - }; - - const double x0 = shift(rng); - const double y0 = shift(rng); - const double z0 = shift(rng); - const double lx = len(rng); - const double ly = len(rng); - const double lz = len(rng); - - data.points = { - x0, y0, z0, - x0 + lx, y0, z0, - x0 + lx, y0 + ly, z0, - x0, y0 + ly, z0, - x0, y0, z0 + lz, - x0 + lx, y0, z0 + lz, - x0 + lx, y0 + ly, z0 + lz, - x0, y0 + ly, z0 + lz, - }; - - for (;;) - { - const double nx = w(rng); - const double ny = w(rng); - const double nz = w(rng); - const double shift_plane = d(rng); - - bool has_pos = false; - bool has_neg = false; - bool near_zero = false; - - for (int i = 0; i < 8; ++i) - { - const double x = data.points[i * 3 + 0]; - const double y = data.points[i * 3 + 1]; - const double z = data.points[i * 3 + 2]; - data.ls[i] = nx * x + ny * y + nz * z + shift_plane; - - has_pos = has_pos || (data.ls[i] > 0.0); - has_neg = has_neg || (data.ls[i] < 0.0); - near_zero = near_zero || (std::abs(data.ls[i]) < std::numeric_limits::epsilon()); - } - - if (has_pos && has_neg && !near_zero) - return data; - } -} - -cutcells::mesh::CutCells make_random_cut_cells(std::mt19937_64& rng) -{ - const auto data = make_random_hex_cell_data(rng); - - cutcells::cell::CutCell cut_cell; - cutcells::cell::hexahedron::cut(data.points, 3, data.ls, "phi=0", cut_cell, false); - - cut_cell._parent_cell_type = cutcells::cell::type::hexahedron; - cut_cell._parent_vertex_ids = std::vector(data.connectivity.begin(), data.connectivity.end()); - - cutcells::mesh::CutCells cells; - cells._cut_cells = {std::move(cut_cell)}; - cells._parent_map = {0}; - cells._types = {cutcells::cell::type::hexahedron}; - return cells; -} -} // namespace - -static void BM_CutVtkMesh(benchmark::State& state) -{ - std::mt19937_64 rng(20260307); - const auto data = make_random_hex_cell_data(rng); - - for (auto _ : state) - { - cutcells::mesh::CutMesh out; - for (int i = 0; i < kRepeatsCutVtkMesh; ++i) - { - out = cutcells::mesh::cut_vtk_mesh(data.ls, data.points, data.connectivity, data.offset, - data.vtk_type, "phi=0", false); - benchmark::DoNotOptimize(out); - } - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeatsCutVtkMesh); -} - -static void BM_CreateCutMesh(benchmark::State& state) -{ - std::mt19937_64 rng(20260307); - - for (auto _ : state) - { - for (int i = 0; i < kRepeatsCreateCutMesh; ++i) - { - auto cells = make_random_cut_cells(rng); - auto out = cutcells::mesh::create_cut_mesh(cells); - benchmark::DoNotOptimize(out); - } - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeatsCreateCutMesh); -} - -BENCHMARK(BM_CutVtkMesh); -BENCHMARK(BM_CreateCutMesh); -BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_tetrahedron.cpp b/benchmarks/bench_cut_tetrahedron.cpp deleted file mode 100644 index 092c3b6..0000000 --- a/benchmarks/bench_cut_tetrahedron.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "cut_cell.h" -#include "cut_tetrahedron.h" - -namespace -{ -constexpr int kRepeats = 1'000'000; - -std::array make_random_tetrahedron(std::mt19937_64& rng) -{ - std::uniform_real_distribution shift(-10.0, 10.0); - std::uniform_real_distribution len(0.1, 2.0); - - const double x0 = shift(rng); - const double y0 = shift(rng); - const double z0 = shift(rng); - - const double lx = len(rng); - const double ly = len(rng); - const double lz = len(rng); - - return { - x0, y0, z0, - x0 + lx, y0, z0, - x0, y0 + ly, z0, - x0, y0, z0 + lz, - }; -} - -std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) -{ - std::uniform_real_distribution w(-1.0, 1.0); - std::uniform_real_distribution d(-1.0, 1.0); - - std::array ls{}; - for (;;) - { - const double nx = w(rng); - const double ny = w(rng); - const double nz = w(rng); - const double shift = d(rng); - - bool has_pos = false; - bool has_neg = false; - bool near_zero = false; - - for (int i = 0; i < 4; ++i) - { - const double x = coords[i * 3 + 0]; - const double y = coords[i * 3 + 1]; - const double z = coords[i * 3 + 2]; - ls[i] = nx * x + ny * y + nz * z + shift; - - has_pos = has_pos || (ls[i] > 0.0); - has_neg = has_neg || (ls[i] < 0.0); - near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); - } - - if (has_pos && has_neg && !near_zero) - return ls; - } -} -} // namespace - -static void BM_CutTetrahedron(benchmark::State& state) -{ - std::mt19937_64 rng(20260307); - - const auto coords = make_random_tetrahedron(rng); - const auto ls = make_intersecting_ls(coords, rng); - - for (auto _ : state) - { - cutcells::cell::CutCell out; - for (int i = 0; i < kRepeats; ++i) - { - cutcells::cell::tetrahedron::cut(coords, 3, ls, "phi=0", out, false); - benchmark::DoNotOptimize(out); - } - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); -} - -BENCHMARK(BM_CutTetrahedron); -BENCHMARK_MAIN(); diff --git a/benchmarks/bench_cut_triangle.cpp b/benchmarks/bench_cut_triangle.cpp deleted file mode 100644 index db0bafd..0000000 --- a/benchmarks/bench_cut_triangle.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "cut_cell.h" -#include "cut_triangle.h" - -namespace -{ -constexpr int kRepeats = 1'000'000; - -std::array make_random_triangle(std::mt19937_64& rng) -{ - std::uniform_real_distribution shift(-10.0, 10.0); - std::uniform_real_distribution len(0.1, 2.0); - - const double x0 = shift(rng); - const double y0 = shift(rng); - const double z0 = shift(rng); - - const double lx = len(rng); - const double ly = len(rng); - - return { - x0, y0, z0, - x0 + lx, y0, z0, - x0, y0 + ly, z0, - }; -} - -std::array make_intersecting_ls(const std::array& coords, std::mt19937_64& rng) -{ - std::uniform_real_distribution w(-1.0, 1.0); - std::uniform_real_distribution d(-1.0, 1.0); - - std::array ls{}; - for (;;) - { - const double nx = w(rng); - const double ny = w(rng); - const double nz = w(rng); - const double shift = d(rng); - - bool has_pos = false; - bool has_neg = false; - bool near_zero = false; - - for (int i = 0; i < 3; ++i) - { - const double x = coords[i * 3 + 0]; - const double y = coords[i * 3 + 1]; - const double z = coords[i * 3 + 2]; - ls[i] = nx * x + ny * y + nz * z + shift; - - has_pos = has_pos || (ls[i] > 0.0); - has_neg = has_neg || (ls[i] < 0.0); - near_zero = near_zero || (std::abs(ls[i]) < std::numeric_limits::epsilon()); - } - - if (has_pos && has_neg && !near_zero) - return ls; - } -} -} // namespace - -static void BM_CutTriangle(benchmark::State& state) -{ - std::mt19937_64 rng(20260307); - - const auto coords = make_random_triangle(rng); - const auto ls = make_intersecting_ls(coords, rng); - - for (auto _ : state) - { - cutcells::cell::CutCell out; - for (int i = 0; i < kRepeats; ++i) - { - cutcells::cell::triangle::cut(coords, 3, ls, "phi=0", out, false); - benchmark::DoNotOptimize(out); - } - benchmark::ClobberMemory(); - } - - state.SetItemsProcessed(static_cast(state.iterations()) * kRepeats); -} - -BENCHMARK(BM_CutTriangle); -BENCHMARK_MAIN(); diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 77aec53..d5088ff 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -18,7 +18,6 @@ include(CheckIPOSupported) # Source files add_subdirectory(src) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../benchmarks ${CMAKE_CURRENT_BINARY_DIR}/benchmarks) find_package(OpenMP) if(OpenMP_CXX_FOUND) @@ -39,6 +38,12 @@ target_include_directories(cutcells PUBLIC install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells/${DIR} COMPONENT Development) +# Install generated headers produced at build time (tables, etc.) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/generated + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells/src + COMPONENT Development + FILES_MATCHING PATTERN "*.h") + # Install the cutcells library install(TARGETS cutcells EXPORT CutCellsTargets diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index aed58e4..18bd855 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/cell_types.h ${CMAKE_CURRENT_SOURCE_DIR}/cell_flags.h ${CMAKE_CURRENT_SOURCE_DIR}/cell_subdivision.h + ${CMAKE_CURRENT_SOURCE_DIR}/cell_topology.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_triangle.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_interval.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_quadrilateral.h @@ -29,6 +30,8 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.h ${CMAKE_CURRENT_SOURCE_DIR}/mapping.h + ${CMAKE_CURRENT_SOURCE_DIR}/level_set.h + ${CMAKE_CURRENT_SOURCE_DIR}/mesh_view.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature_tables.h ${CMAKE_CURRENT_SOURCE_DIR}/span_math.h diff --git a/cpp/src/generated/quadrature_tables_interval.h b/cpp/src/generated/quadrature_tables_interval.h index c3662a2..3042455 100644 --- a/cpp/src/generated/quadrature_tables_interval.h +++ b/cpp/src/generated/quadrature_tables_interval.h @@ -9,110 +9,114 @@ namespace cutcells::quadrature::generated { -// ---- order 1: 1 point(s), tdim=1 ---- -inline constexpr int interval_o1_npts = 1; +// ---- order 1: 2 point(s), tdim=1 ---- +inline constexpr int interval_o1_npts = 2; inline constexpr int interval_o1_tdim = 1; -inline constexpr double interval_o1_points[1] = { - 5.000000000000000000e-01 +inline constexpr double interval_o1_points[2] = { + 0.000000000000000000e+00, 1.000000000000000000e+00 }; -inline constexpr double interval_o1_weights[1] = { - 1.000000000000000000e+00 +inline constexpr double interval_o1_weights[2] = { + 4.999999999999998890e-01, 4.999999999999998890e-01 }; -// ---- order 2: 2 point(s), tdim=1 ---- -inline constexpr int interval_o2_npts = 2; +// ---- order 2: 3 point(s), tdim=1 ---- +inline constexpr int interval_o2_npts = 3; inline constexpr int interval_o2_tdim = 1; -inline constexpr double interval_o2_points[2] = { - 2.113248654051871345e-01, 7.886751345948128655e-01 +inline constexpr double interval_o2_points[3] = { + 1.110223024625156540e-16, 9.999999999999998890e-01, 5.000000000000000000e-01 }; -inline constexpr double interval_o2_weights[2] = { - 5.000000000000000000e-01, 5.000000000000000000e-01 +inline constexpr double interval_o2_weights[3] = { + 1.666666666666667129e-01, 1.666666666666666574e-01, 6.666666666666662966e-01 }; -// ---- order 3: 2 point(s), tdim=1 ---- -inline constexpr int interval_o3_npts = 2; +// ---- order 3: 3 point(s), tdim=1 ---- +inline constexpr int interval_o3_npts = 3; inline constexpr int interval_o3_tdim = 1; -inline constexpr double interval_o3_points[2] = { - 2.113248654051871345e-01, 7.886751345948128655e-01 +inline constexpr double interval_o3_points[3] = { + 1.110223024625156540e-16, 9.999999999999998890e-01, 5.000000000000000000e-01 }; -inline constexpr double interval_o3_weights[2] = { - 5.000000000000000000e-01, 5.000000000000000000e-01 +inline constexpr double interval_o3_weights[3] = { + 1.666666666666667129e-01, 1.666666666666666574e-01, 6.666666666666662966e-01 }; -// ---- order 4: 3 point(s), tdim=1 ---- -inline constexpr int interval_o4_npts = 3; +// ---- order 4: 4 point(s), tdim=1 ---- +inline constexpr int interval_o4_npts = 4; inline constexpr int interval_o4_tdim = 1; -inline constexpr double interval_o4_points[3] = { - 1.127016653792582979e-01, 5.000000000000000000e-01, 8.872983346207417021e-01 +inline constexpr double interval_o4_points[4] = { + -1.110223024625156540e-16, 1.000000000000000000e+00, 2.763932022500210639e-01, 7.236067977499789361e-01 }; -inline constexpr double interval_o4_weights[3] = { - 2.777777777777776791e-01, 4.444444444444444198e-01, 2.777777777777776791e-01 +inline constexpr double interval_o4_weights[4] = { + 8.333333333333323156e-02, 8.333333333333335646e-02, 4.166666666666665186e-01, 4.166666666666663521e-01 }; -// ---- order 5: 3 point(s), tdim=1 ---- -inline constexpr int interval_o5_npts = 3; +// ---- order 5: 4 point(s), tdim=1 ---- +inline constexpr int interval_o5_npts = 4; inline constexpr int interval_o5_tdim = 1; -inline constexpr double interval_o5_points[3] = { - 1.127016653792582979e-01, 5.000000000000000000e-01, 8.872983346207417021e-01 +inline constexpr double interval_o5_points[4] = { + -1.110223024625156540e-16, 1.000000000000000000e+00, 2.763932022500210639e-01, 7.236067977499789361e-01 }; -inline constexpr double interval_o5_weights[3] = { - 2.777777777777776791e-01, 4.444444444444444198e-01, 2.777777777777776791e-01 +inline constexpr double interval_o5_weights[4] = { + 8.333333333333323156e-02, 8.333333333333335646e-02, 4.166666666666665186e-01, 4.166666666666663521e-01 }; -// ---- order 6: 4 point(s), tdim=1 ---- -inline constexpr int interval_o6_npts = 4; +// ---- order 6: 5 point(s), tdim=1 ---- +inline constexpr int interval_o6_npts = 5; inline constexpr int interval_o6_tdim = 1; -inline constexpr double interval_o6_points[4] = { - 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01 +inline constexpr double interval_o6_points[5] = { + 5.551115123125782702e-17, 1.000000000000000000e+00, 1.726731646460114566e-01, 5.000000000000000000e-01, + 8.273268353539885434e-01 }; -inline constexpr double interval_o6_weights[4] = { - 1.739274225687268416e-01, 3.260725774312731029e-01, 3.260725774312729919e-01, 1.739274225687268416e-01 +inline constexpr double interval_o6_weights[5] = { + 5.000000000000003747e-02, 5.000000000000004441e-02, 2.722222222222222543e-01, 3.555555555555554581e-01, + 2.722222222222220323e-01 }; -// ---- order 7: 4 point(s), tdim=1 ---- -inline constexpr int interval_o7_npts = 4; +// ---- order 7: 5 point(s), tdim=1 ---- +inline constexpr int interval_o7_npts = 5; inline constexpr int interval_o7_tdim = 1; -inline constexpr double interval_o7_points[4] = { - 6.943184420297371373e-02, 3.300094782075718713e-01, 6.699905217924281287e-01, 9.305681557970262308e-01 +inline constexpr double interval_o7_points[5] = { + 5.551115123125782702e-17, 1.000000000000000000e+00, 1.726731646460114566e-01, 5.000000000000000000e-01, + 8.273268353539885434e-01 }; -inline constexpr double interval_o7_weights[4] = { - 1.739274225687268416e-01, 3.260725774312731029e-01, 3.260725774312729919e-01, 1.739274225687268416e-01 +inline constexpr double interval_o7_weights[5] = { + 5.000000000000003747e-02, 5.000000000000004441e-02, 2.722222222222222543e-01, 3.555555555555554581e-01, + 2.722222222222220323e-01 }; -// ---- order 8: 5 point(s), tdim=1 ---- -inline constexpr int interval_o8_npts = 5; +// ---- order 8: 6 point(s), tdim=1 ---- +inline constexpr int interval_o8_npts = 6; inline constexpr int interval_o8_tdim = 1; -inline constexpr double interval_o8_points[5] = { - 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, - 9.530899229693319263e-01 +inline constexpr double interval_o8_points[6] = { + 2.775557561562891351e-16, 1.000000000000000000e+00, 1.174723380352676871e-01, 3.573842417596773680e-01, + 6.426157582403224655e-01, 8.825276619647323129e-01 }; -inline constexpr double interval_o8_weights[5] = { - 1.184634425280945563e-01, 2.393143352496833187e-01, 2.844444444444443887e-01, 2.393143352496833187e-01, - 1.184634425280945563e-01 +inline constexpr double interval_o8_weights[6] = { + 3.333333333333327042e-02, 3.333333333333340920e-02, 1.892374781489237490e-01, 2.774291885177430084e-01, + 2.774291885177428418e-01, 1.892374781489237490e-01 }; -// ---- order 9: 5 point(s), tdim=1 ---- -inline constexpr int interval_o9_npts = 5; +// ---- order 9: 6 point(s), tdim=1 ---- +inline constexpr int interval_o9_npts = 6; inline constexpr int interval_o9_tdim = 1; -inline constexpr double interval_o9_points[5] = { - 4.691007703066801815e-02, 2.307653449471584461e-01, 5.000000000000000000e-01, 7.692346550528414983e-01, - 9.530899229693319263e-01 +inline constexpr double interval_o9_points[6] = { + 2.775557561562891351e-16, 1.000000000000000000e+00, 1.174723380352676871e-01, 3.573842417596773680e-01, + 6.426157582403224655e-01, 8.825276619647323129e-01 }; -inline constexpr double interval_o9_weights[5] = { - 1.184634425280945563e-01, 2.393143352496833187e-01, 2.844444444444443887e-01, 2.393143352496833187e-01, - 1.184634425280945563e-01 +inline constexpr double interval_o9_weights[6] = { + 3.333333333333327042e-02, 3.333333333333340920e-02, 1.892374781489237490e-01, 2.774291885177430084e-01, + 2.774291885177428418e-01, 1.892374781489237490e-01 }; -// ---- order 10: 6 point(s), tdim=1 ---- -inline constexpr int interval_o10_npts = 6; +// ---- order 10: 7 point(s), tdim=1 ---- +inline constexpr int interval_o10_npts = 7; inline constexpr int interval_o10_tdim = 1; -inline constexpr double interval_o10_points[6] = { - 3.376524289842397497e-02, 1.693953067668677037e-01, 3.806904069584015060e-01, 6.193095930415984940e-01, - 8.306046932331323518e-01, 9.662347571015760250e-01 +inline constexpr double interval_o10_points[7] = { + -1.110223024625156540e-16, 1.000000000000000000e+00, 8.488805186071646247e-02, 2.655756032646428011e-01, + 5.000000000000001110e-01, 7.344243967353571989e-01, 9.151119481392833155e-01 }; -inline constexpr double interval_o10_weights[6] = { - 8.566224618958498405e-02, 1.803807865240693031e-01, 2.339569672863456296e-01, 2.339569672863454908e-01, - 1.803807865240693031e-01, 8.566224618958498405e-02 +inline constexpr double interval_o10_weights[7] = { + 2.380952380952375963e-02, 2.380952380952357922e-02, 1.384130236807827041e-01, 2.158726906049313332e-01, + 2.438095238095235040e-01, 2.158726906049308336e-01, 1.384130236807826209e-01 }; } // namespace cutcells::quadrature::generated diff --git a/cpp/src/level_set.h b/cpp/src/level_set.h new file mode 100644 index 0000000..92602b9 --- /dev/null +++ b/cpp/src/level_set.h @@ -0,0 +1,69 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + +namespace cutcells +{ + +template +struct LevelSetFunction +{ + // cell_id == -1 means "unknown / not provided" + std::function value_fn; + std::function grad_fn; + + // Optional nodal values + std::span nodal_values; + + // Optional keep-alive anchor for Python / external memory + std::shared_ptr owner; + + int gdim = 0; + + bool has_value() const + { + return static_cast(value_fn); + } + + bool has_gradient() const + { + return static_cast(grad_fn); + } + + bool has_nodal_values() const + { + return !nodal_values.empty(); + } + + T value(const T* x, I cell_id = static_cast(-1)) const + { + if (!value_fn) + throw std::runtime_error("LevelSetFunction::value not available"); + return value_fn(x, cell_id); + } + + void grad(const T* x, I cell_id, T* g) const + { + if (!grad_fn) + throw std::runtime_error("LevelSetFunction::grad not available"); + grad_fn(x, cell_id, g); + } + + T value_at_node(I node_id) const + { + if (nodal_values.empty()) + throw std::runtime_error("LevelSetFunction::value_at_node not available"); + return nodal_values[static_cast(node_id)]; + } +}; + +} // namespace cutcells \ No newline at end of file diff --git a/cpp/src/mesh_view.h b/cpp/src/mesh_view.h new file mode 100644 index 0000000..51b9229 --- /dev/null +++ b/cpp/src/mesh_view.h @@ -0,0 +1,88 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + +namespace cutcells +{ + +template +struct MeshView +{ + int gdim = 0; + int tdim = 0; + + // Flattened coordinates: [x0_0, x0_1, ..., x1_0, x1_1, ...] + std::span coordinates; + + // CSR-like cell storage + std::span connectivity; + std::span offsets; + + // Cell types, one per cell + std::span cell_types; + + // Optional keep-alive anchor for Python / external memory + std::shared_ptr owner; + + I num_nodes() const + { + if (gdim <= 0) + return 0; + return static_cast(coordinates.size() / static_cast(gdim)); + } + + I num_cells() const + { + if (offsets.empty()) + return 0; + return static_cast(offsets.size() - 1); + } + + bool has_cell_types() const + { + return !cell_types.empty(); + } + + const T* node(I node_id) const + { + return coordinates.data() + static_cast(node_id) * static_cast(gdim); + } + + I cell_num_nodes(I cell_id) const + { + return offsets[static_cast(cell_id) + 1] - offsets[static_cast(cell_id)]; + } + + std::span cell_nodes(I cell_id) const + { + const std::size_t begin = static_cast(offsets[static_cast(cell_id)]); + const std::size_t end = static_cast(offsets[static_cast(cell_id) + 1]); + return connectivity.subspan(begin, end - begin); + } + + I cell_node(I cell_id, I local_id) const + { + const std::size_t pos = static_cast(offsets[static_cast(cell_id)]) + + static_cast(local_id); + return connectivity[pos]; + } + + I cell_type(I cell_id) const + { + if (cell_types.empty()) + throw std::runtime_error("MeshView::cell_type not available"); + return cell_types[static_cast(cell_id)]; + } +}; + +} // namespace cutcells \ No newline at end of file diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 19bb392..fd7f2a4 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -14,11 +14,18 @@ CutMesh_float64, QuadratureRules_float32, QuadratureRules_float64, + MeshView, + MeshView_float32, + MeshView_float64, + LevelSetFunction, + LevelSetFunction_float32, + LevelSetFunction_float64, cut, higher_order_cut, create_cut_mesh, locate_cells, cut_vtk_mesh, + cut_mesh_view, csr_to_vtk_cells, compute_physical_cut_vertices, complete_from_physical, diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 481a363..1bba63f 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -11,9 +11,12 @@ #include #include #include +#include #include #include #include +#include +#include #include "../../cpp/src/cell_types.h" #include "../../cpp/src/cut_cell.h" @@ -21,6 +24,8 @@ #include "../../cpp/src/write_vtk.h" #include "../../cpp/src/mapping.h" #include "../../cpp/src/quadrature.h" +#include "../../cpp/src/mesh_view.h" +#include "../../cpp/src/level_set.h" namespace nb = nanobind; @@ -70,38 +75,362 @@ auto as_nbarrayp(std::pair>&& x) return as_nbarray(std::move(x.first), x.second.size(), x.second.data()); } -inline std::vector csr_to_vtk_cells_impl(std::span connectivity, - std::span offsets) -{ - if (offsets.empty()) - return {}; +template +using ndarray1 = nb::ndarray, nb::c_contig>; - const int conn_size = static_cast(connectivity.size()); - const bool has_terminal_offset = (offsets.back() == conn_size); - const int num_cells = has_terminal_offset ? static_cast(offsets.size()) - 1 - : static_cast(offsets.size()); +template +using ndarray2 = nb::ndarray, nb::c_contig>; - if (num_cells < 0) - throw std::runtime_error("Invalid CSR offsets: negative cell count"); +std::shared_ptr make_owner_from_objects(nb::object a = nb::object(), + nb::object b = nb::object(), + nb::object c = nb::object(), + nb::object d = nb::object()) +{ + struct Owner + { + nb::object a, b, c, d; + }; + auto owner = std::make_shared(); + owner->a = std::move(a); + owner->b = std::move(b); + owner->c = std::move(c); + owner->d = std::move(d); + return owner; +} - std::vector cells; - cells.reserve(connectivity.size() + static_cast(num_cells)); +// Convert CSR connectivity+offset arrays into VTK packed cells layout +// [n0, v0_0, v0_1, ..., n1, v1_0, ...]. +static std::vector csr_to_vtk_cells_impl(std::span connectivity, + std::span offsets) +{ + std::vector out; + if (offsets.size() == 0) + return out; + const std::size_t ncells = (offsets.size() > 0) ? (offsets.size() - 1) : 0; + out.reserve(connectivity.size() + ncells); + for (std::size_t i = 0; i < ncells; ++i) + { + int start = offsets[i]; + int end = offsets[i + 1]; + int n = end - start; + out.push_back(n); + for (int j = start; j < end; ++j) + out.push_back(connectivity[static_cast(j)]); + } + return out; +} - for (int i = 0; i < num_cells; ++i) +template +cutcells::MeshView make_mesh_view_from_numpy( + const ndarray2& coordinates, + const ndarray1& connectivity, + const ndarray1& offsets, + const std::optional>& cell_types, + int tdim) +{ + cutcells::MeshView mesh; + mesh.gdim = static_cast(coordinates.shape(1)); + mesh.tdim = tdim; + + mesh.coordinates = std::span(coordinates.data(), + static_cast(coordinates.size())); + mesh.connectivity = std::span(connectivity.data(), + static_cast(connectivity.size())); + mesh.offsets = std::span(offsets.data(), + static_cast(offsets.size())); + + nb::object coords_obj = nb::cast(coordinates); + nb::object conn_obj = nb::cast(connectivity); + nb::object offs_obj = nb::cast(offsets); + nb::object types_obj; + + if (cell_types.has_value()) { - const int begin = offsets[i]; - const int end = (i + 1 < static_cast(offsets.size())) ? offsets[i + 1] : conn_size; + mesh.cell_types = std::span(cell_types->data(), + static_cast(cell_types->size())); + types_obj = nb::cast(*cell_types); + } - if (begin < 0 || end < begin || end > conn_size) - throw std::runtime_error("Invalid CSR offsets/connectivity bounds"); + mesh.owner = make_owner_from_objects(coords_obj, conn_obj, offs_obj, types_obj); + return mesh; +} - const int nverts = end - begin; - cells.push_back(static_cast(nverts)); - for (int j = begin; j < end; ++j) - cells.push_back(static_cast(connectivity[j])); - } +template +void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) +{ + using MeshViewT = cutcells::MeshView; + using LevelSetT = cutcells::LevelSetFunction; + + const std::string mesh_name = "MeshView_" + suffix; + nb::class_(m, mesh_name.c_str(), "Lightweight mesh view") + .def( + "__init__", + [](MeshViewT* self, + const ndarray2& coordinates, + const ndarray1& connectivity, + const ndarray1& offsets, + nb::object cell_types_obj, + int tdim) + { + std::optional> cell_types; + if (!cell_types_obj.is_none()) + cell_types = nb::cast>(cell_types_obj); + + new (self) MeshViewT( + make_mesh_view_from_numpy(coordinates, connectivity, offsets, cell_types, tdim)); + }, + nb::arg("coordinates"), + nb::arg("connectivity"), + nb::arg("offsets"), + nb::arg("cell_types") = nb::none(), + nb::arg("tdim")) + .def_prop_ro("gdim", [](const MeshViewT& self) { return self.gdim; }) + .def_prop_ro("tdim", [](const MeshViewT& self) { return self.tdim; }) + .def("num_nodes", &MeshViewT::num_nodes) + .def("num_cells", &MeshViewT::num_cells) + .def("has_cell_types", &MeshViewT::has_cell_types) + .def("cell_num_nodes", &MeshViewT::cell_num_nodes) + .def("cell_node", &MeshViewT::cell_node) + .def( + "node", + [](const MeshViewT& self, int node_id) + { + const T* x = self.node(node_id); + return nb::ndarray( + x, {static_cast(self.gdim)}, nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "coordinates", + [](const MeshViewT& self) + { + return nb::ndarray( + self.coordinates.data(), + {static_cast(self.num_nodes()), + static_cast(self.gdim)}, + nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "connectivity", + [](const MeshViewT& self) + { + return nb::ndarray( + self.connectivity.data(), + {self.connectivity.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "offsets", + [](const MeshViewT& self) + { + return nb::ndarray( + self.offsets.data(), + {self.offsets.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_types", + [](const MeshViewT& self) + { + if (self.cell_types.empty()) + return nb::ndarray(nullptr, {0}, nb::handle()); + return nb::ndarray( + self.cell_types.data(), + {self.cell_types.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal); + + const std::string ls_name = "LevelSetFunction_" + suffix; + nb::class_(m, ls_name.c_str(), "Level-set function") + .def( + "__init__", + [](LevelSetT* self, + nb::object value_obj, + nb::object grad_obj, + nb::object nodal_values_obj, + int gdim) + { + using ValueFn = std::function; + using GradFn = std::function; + + ValueFn value_fn; + GradFn grad_fn; + std::span nodal_values; + nb::object nodal_owner; - return cells; + if (!value_obj.is_none()) + { + nb::callable value_callable = nb::cast(value_obj); + + value_fn = [value_callable](const T* x, int cell_id) -> T + { + nb::gil_scoped_acquire gil; + nb::ndarray x_arr(x, {static_cast(3)}, nb::handle()); + try + { + return nb::cast(value_callable(x_arr, cell_id)); + } + catch (const nb::python_error&) + { + return nb::cast(value_callable(x_arr)); + } + }; + } + + if (!grad_obj.is_none()) + { + nb::callable grad_callable = nb::cast(grad_obj); + + grad_fn = [grad_callable](const T* x, int cell_id, T* g) + { + nb::gil_scoped_acquire gil; + nb::ndarray x_arr(x, {static_cast(3)}, nb::handle()); + + nb::object result; + try + { + result = grad_callable(x_arr, cell_id); + } + catch (const nb::python_error&) + { + result = grad_callable(x_arr); + } + + auto grad_arr = nb::cast>(result); + for (std::size_t i = 0; i < grad_arr.size(); ++i) + g[i] = grad_arr(i); + }; + } + + if (!nodal_values_obj.is_none()) + { + auto nodal_array = nb::cast>(nodal_values_obj); + nodal_values = std::span(nodal_array.data(), + static_cast(nodal_array.size())); + nodal_owner = nodal_values_obj; + } + + if (!value_fn && nodal_values.empty()) + throw std::runtime_error("LevelSetFunction requires at least one of value or nodal_values."); + + if (grad_fn && !value_fn) + throw std::runtime_error("LevelSetFunction: grad requires value."); + + new (self) LevelSetT{}; + self->value_fn = std::move(value_fn); + self->grad_fn = std::move(grad_fn); + self->nodal_values = nodal_values; + self->gdim = gdim; + self->owner = make_owner_from_objects(value_obj, grad_obj, nodal_owner); + }, + nb::arg("value") = nb::none(), + nb::arg("grad") = nb::none(), + nb::arg("nodal_values") = nb::none(), + nb::arg("gdim") = 0) + .def_prop_ro("gdim", [](const LevelSetT& self) { return self.gdim; }) + .def("has_value", &LevelSetT::has_value) + .def("has_gradient", &LevelSetT::has_gradient) + .def("has_nodal_values", &LevelSetT::has_nodal_values) + .def( + "value", + [](const LevelSetT& self, const ndarray1& x, int cell_id) + { + return self.value(x.data(), cell_id); + }, + nb::arg("x"), + nb::arg("cell_id") = -1) + .def( + "grad", + [](const LevelSetT& self, const ndarray1& x, int cell_id) + { + std::vector g(static_cast(self.gdim), T(0)); + self.grad(x.data(), cell_id, g.data()); + return nb::ndarray( + g.data(), + {g.size()}, + nb::capsule(new std::vector(std::move(g)), + [](void* p) noexcept { delete static_cast*>(p); })); + }, + nb::arg("x"), + nb::arg("cell_id") = -1) + .def("value_at_node", &LevelSetT::value_at_node) + .def_prop_ro( + "nodal_values", + [](const LevelSetT& self) + { + if (self.nodal_values.empty()) + return nb::ndarray(nullptr, {0}, nb::handle()); + return nb::ndarray( + self.nodal_values.data(), + {self.nodal_values.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal); + + // ---- cut_mesh_view ---- + // Cuts a MeshView using a LevelSetFunction, returning a CutMesh. + // Nodal level-set values are taken from ls.nodal_values if available, + // otherwise ls.value() is evaluated at every mesh node. + m.def( + "cut_mesh_view", + [](const MeshViewT& mesh, const LevelSetT& ls, + const std::string& cut_type_str, bool triangulate) + { + if (!mesh.has_cell_types()) + throw std::runtime_error( + "cut_mesh_view: MeshView must have cell_types (VTK type IDs)"); + + // Build nodal level-set values while GIL is held + // (ls.value() may call back into Python) + const std::size_t n = static_cast(mesh.num_nodes()); + std::vector ls_vals(n); + if (ls.has_nodal_values()) + { + if (ls.nodal_values.size() != n) + throw std::runtime_error( + "cut_mesh_view: nodal_values size does not match MeshView num_nodes"); + std::copy(ls.nodal_values.begin(), ls.nodal_values.end(), ls_vals.begin()); + } + else if (ls.has_value()) + { + for (std::size_t i = 0; i < n; ++i) + ls_vals[i] = ls.value(mesh.node(static_cast(i)), -1); + } + else + { + throw std::runtime_error( + "cut_mesh_view: LevelSetFunction has neither value nor nodal_values"); + } + + // Cut mesh with GIL released + nb::gil_scoped_release release; + return mesh::cut_vtk_mesh( + std::span(ls_vals.data(), ls_vals.size()), + mesh.coordinates, + mesh.connectivity, + mesh.offsets, + mesh.cell_types, + cut_type_str, + triangulate); + }, + nb::arg("mesh"), + nb::arg("level_set"), + nb::arg("cut_type"), + nb::arg("triangulate") = true, + "Cut a MeshView with a LevelSetFunction.\n" + "Returns a CutMesh containing cells classified by cut_type (\"phi<0\", \"phi=0\", \"phi>0\").\n" + "Level-set values are taken from nodal_values if set, otherwise evaluated via value()."); + + // Simple aliases for Python + if constexpr (std::is_same_v) + { + m.attr("MeshView") = m.attr(mesh_name.c_str()); + m.attr("LevelSetFunction") = m.attr(ls_name.c_str()); + } } template @@ -566,6 +895,9 @@ NB_MODULE(_cutcellscpp, m) declare_float(m, "float32"); declare_float(m, "float64"); + declare_meshview_and_levelset(m, "float32"); + declare_meshview_and_levelset(m, "float64"); + m.def("csr_to_vtk_cells", [](const nb::ndarray, nb::c_contig>& connectivity, const nb::ndarray, nb::c_contig>& offsets) diff --git a/python/demo/bench_cut_pipeline.py b/python/demo/bench_cut_pipeline.py new file mode 100644 index 0000000..72b4c97 --- /dev/null +++ b/python/demo/bench_cut_pipeline.py @@ -0,0 +1,520 @@ +""" +bench_cut_pipeline.py +===================== + +Pure-Python micro-benchmark for the CutCells cutting pipeline. + +Measures wall time for: + 1. cut_vtk_mesh – for triangle, quad, tetrahedron, hex meshes + 2. runtime_quadrature – phi<0 and phi=0 domains + 3. physical_points – mapping reference → physical space + +Run with: + conda run -n fenicsxv10 python bench_cut_pipeline.py [--n N] [--repeat R] [--order O] + +Columns: + kernel – what is being timed + mesh / domain – short description + n_cells – number of background cells + n_pts – number of quadrature points (quadrature kernels only) + total_ms – total wall time for repetitions [ms] + per_call_us – time per single call [µs] + Mcells/s – million cells per second (cut kernels) + Mpts/s – million quad pts per second (quadrature mapping) +""" + +import argparse +import time +from typing import Callable + +import numpy as np + +try: + import pyvista as pv + + HAS_PV = True +except ImportError: + HAS_PV = False + +import cutcells + +# --------------------------------------------------------------------------- +# Timing helper +# --------------------------------------------------------------------------- + + +def timeit(fn: Callable, repeat: int = 5) -> float: + """Return minimum wall time [seconds] over calls.""" + times = [] + for _ in range(repeat): + t0 = time.perf_counter() + fn() + times.append(time.perf_counter() - t0) + return min(times) + + +# --------------------------------------------------------------------------- +# Mesh creation helpers +# --------------------------------------------------------------------------- + + +def make_triangle_mesh_vtk(n: int): + """Return (points_flat, conn, offset, celltypes, ls_vals) for an n×n Delaunay + triangle mesh on [-1,1]^2, level-set = circle of radius 0.7.""" + if not HAS_PV: + raise RuntimeError("pyvista required for triangle/quad mesh generation") + x = np.linspace(-1.0, 1.0, n) + y = np.linspace(-1.0, 1.0, n) + xx, yy, zz = np.meshgrid(x, y, [0.0]) + pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + grid = pv.UnstructuredGrid(pv.PolyData(pts).delaunay_2d()) + pts3 = np.asarray(grid.points, dtype=np.float64) + conn = np.asarray(grid.cell_connectivity, dtype=np.int32) + off = np.asarray(grid.offset, dtype=np.int32) + ctypes = np.asarray(grid.celltypes, dtype=np.int32) + ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2) - 0.7 + return pts3.ravel(), conn, off, ctypes, ls, grid.n_cells + + +def make_quad_mesh_vtk(n: int): + """Structured quad mesh on [-1,1]^2 (vectorised).""" + x = np.linspace(-1.0, 1.0, n) + y = np.linspace(-1.0, 1.0, n) + xx, yy = np.meshgrid(x, y) # both (n, n) + zz = np.zeros_like(xx) + pts3 = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1) # (n*n, 3) + + # Node index: node_id[i,j] = i*n + j + i = np.arange(n - 1) + j = np.arange(n - 1) + ii, jj = np.meshgrid(i, j, indexing="ij") # (n-1, n-1) + ii = ii.ravel() + jj = jj.ravel() + n0 = ii * n + jj + n1 = ii * n + (jj + 1) + n2 = (ii + 1) * n + (jj + 1) + n3 = (ii + 1) * n + jj + conn = np.stack([n0, n1, n2, n3], axis=1).astype(np.int32).ravel() + n_cells = (n - 1) * (n - 1) + off = np.arange(0, (n_cells + 1) * 4, 4, dtype=np.int32) + ct = np.full(n_cells, 9, dtype=np.int32) # VTK_QUAD = 9 + ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2) - 0.7 + return pts3.ravel(), conn, off, ct, ls, n_cells + + +def make_tet_mesh_vtk(n: int): + """Random tet mesh built via pyvista Delaunay 3D.""" + if not HAS_PV: + raise RuntimeError("pyvista required") + x = np.linspace(-1.0, 1.0, n) + y = np.linspace(-1.0, 1.0, n) + z = np.linspace(-1.0, 1.0, n) + xx, yy, zz = np.meshgrid(x, y, z) + pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + grid = pv.UnstructuredGrid(pv.PolyData(pts).delaunay_3d()) + pts3 = np.asarray(grid.points, dtype=np.float64) + conn = np.asarray(grid.cell_connectivity, dtype=np.int32) + off = np.asarray(grid.offset, dtype=np.int32) + ctypes = np.asarray(grid.celltypes, dtype=np.int32) + ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2 + pts3[:, 2] ** 2) - 0.7 + return pts3.ravel(), conn, off, ctypes, ls, grid.n_cells + + +def make_hex_mesh_vtk(n: int): + """Structured hex mesh on [-1,1]^3 (vectorised).""" + x = np.linspace(-1.0, 1.0, n) + y = np.linspace(-1.0, 1.0, n) + z = np.linspace(-1.0, 1.0, n) + # node_id[i,j,k] = i*n*n + j*n + k (i=z-axis, j=y-axis, k=x-axis) + zz, yy, xx = np.meshgrid(z, y, x, indexing="ij") # each (n,n,n) + pts3 = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1) # (n^3, 3) + + i = np.arange(n - 1) + j = np.arange(n - 1) + k = np.arange(n - 1) + ii, jj, kk = np.meshgrid(i, j, k, indexing="ij") # each (n-1)^3 + ii = ii.ravel() + jj = jj.ravel() + kk = kk.ravel() + + def nid(a, b, c): + return (a * n + b) * n + c + + conn = ( + np.stack( + [ + nid(ii, jj, kk), + nid(ii, jj, kk + 1), + nid(ii, jj + 1, kk + 1), + nid(ii, jj + 1, kk), + nid(ii + 1, jj, kk), + nid(ii + 1, jj, kk + 1), + nid(ii + 1, jj + 1, kk + 1), + nid(ii + 1, jj + 1, kk), + ], + axis=1, + ) + .astype(np.int32) + .ravel() + ) + n_cells = (n - 1) ** 3 + off = np.arange(0, (n_cells + 1) * 8, 8, dtype=np.int32) + ct = np.full(n_cells, 12, dtype=np.int32) # VTK_HEXAHEDRON = 12 + ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2 + pts3[:, 2] ** 2) - 0.7 + return pts3.ravel(), conn, off, ct, ls, n_cells + + +# --------------------------------------------------------------------------- +# Warm-up (avoids measuring JIT / module-load costs) +# --------------------------------------------------------------------------- + + +def warmup(): + pts = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], dtype=np.float64) + ls = np.array([-0.1, 0.1, 0.1], dtype=np.float64) + conn = np.array([0, 1, 2], dtype=np.int32) + off = np.array([0, 3], dtype=np.int32) + ct = np.array([5], dtype=np.int32) # VTK_TRIANGLE + for _ in range(10): + cutcells.cut_vtk_mesh(ls, pts, conn, off, ct, "phi<0", True) + + +# --------------------------------------------------------------------------- +# Benchmark runners +# --------------------------------------------------------------------------- + + +def bench_cut_vtk_mesh(pts_flat, conn, off, ct, ls, n_cells, label, repeat): + dt = timeit( + lambda: cutcells.cut_vtk_mesh(ls, pts_flat, conn, off, ct, "phi<0", True), + repeat=repeat, + ) + per_call_us = dt * 1e6 + mcells_s = n_cells / dt / 1e6 + return label, n_cells, None, per_call_us, mcells_s, None + + +def bench_runtime_quadrature( + pts_flat, conn, off, ct, ls, n_cells, domain, label, order, repeat +): + def _run(): + cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) + + # collect once to count points + q = cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) + n_pts = len(q.weights) + dt = timeit(_run, repeat=repeat) + per_call_us = dt * 1e6 + mcells_s = n_cells / dt / 1e6 + return label, n_cells, n_pts, per_call_us, mcells_s, None + + +def bench_physical_points( + pts_flat, conn, off, ct, ls, n_cells, domain, label, order, repeat +): + q = cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) + n_pts = len(q.weights) + + def _run(): + cutcells.physical_points(q, pts_flat, conn, off, ct) + + dt = timeit(_run, repeat=repeat) + per_call_us = dt * 1e6 + mpts_s = n_pts / dt / 1e6 if n_pts > 0 else 0.0 + return label, n_cells, n_pts, per_call_us, None, mpts_s + + +# --------------------------------------------------------------------------- +# Reporting +# --------------------------------------------------------------------------- + +HDR = f"{'kernel':<36} {'n_cells':>8} {'n_pts':>8} {'us/call':>10} {'Mcells/s':>10} {'Mpts/s':>8}" +SEP = "-" * len(HDR) + + +def print_row(kernel, n_cells, n_pts, per_call_us, mcells_s, mpts_s): + npts_s = f"{n_pts:>8}" if n_pts is not None else f"{'':>8}" + mc_s = f"{mcells_s:>10.2f}" if mcells_s is not None else f"{'':>10}" + mp_s = f"{mpts_s:>8.2f}" if mpts_s is not None else f"{'':>8}" + print(f"{kernel:<36} {n_cells:>8} {npts_s} {per_call_us:>10.1f} {mc_s} {mp_s}") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description="CutCells pipeline benchmark") + parser.add_argument( + "--n2d", + type=int, + default=40, + help="Grid points per axis for 2-D meshes (default: 40)", + ) + parser.add_argument( + "--n3d", + type=int, + default=12, + help="Grid points per axis for 3-D meshes (default: 12)", + ) + parser.add_argument( + "--repeat", + type=int, + default=7, + help="Repetitions per kernel (min is reported, default: 7)", + ) + parser.add_argument( + "--order", type=int, default=3, help="Quadrature order (default: 3)" + ) + args = parser.parse_args() + + print( + f"\nCutCells pipeline benchmark (repeat={args.repeat}, quad order={args.order})" + ) + print( + f" 2-D mesh resolution: {args.n2d} pts/axis (~{2 * (args.n2d - 1) ** 2} tri cells, {(args.n2d - 1) ** 2} quad cells)" + ) + print( + f" 3-D mesh resolution: {args.n3d} pts/axis (~{(args.n3d - 1) ** 3} hex cells)" + ) + + print("\nBuilding meshes …") + t0 = time.perf_counter() + tri_data = make_triangle_mesh_vtk(args.n2d) + quad_data = make_quad_mesh_vtk(args.n2d) + hex_data = make_hex_mesh_vtk(args.n3d) + tet_data = make_tet_mesh_vtk(args.n3d) if HAS_PV else None + print(f" mesh generation: {(time.perf_counter() - t0) * 1e3:.1f} ms") + if tet_data is not None: + print( + f" tri {tri_data[5]:>6} cells | quad {quad_data[5]:>6} cells | " + f"tet {tet_data[5]:>6} cells | hex {hex_data[5]:>6} cells" + ) + else: + print( + f" tri {tri_data[5]:>6} cells | quad {quad_data[5]:>6} cells | " + f"hex {hex_data[5]:>6} cells" + ) + + print("\nWarming up …") + warmup() + + print() + print(HDR) + print(SEP) + + # ------------------------------------------------------------------ + # 2-D Triangle mesh + # ------------------------------------------------------------------ + pts, conn, off, ct, ls, nc = tri_data + for row in [ + bench_cut_vtk_mesh( + pts, conn, off, ct, ls, nc, "cut_vtk_mesh tri phi<0", args.repeat + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "runtime_quad tri phi<0", + args.order, + args.repeat, + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi=0", + "runtime_quad tri phi=0", + args.order, + args.repeat, + ), + bench_physical_points( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "physical_pts tri phi<0", + args.order, + args.repeat, + ), + bench_physical_points( + pts, + conn, + off, + ct, + ls, + nc, + "phi=0", + "physical_pts tri phi=0", + args.order, + args.repeat, + ), + ]: + print_row(*row) + + print() + + # ------------------------------------------------------------------ + # 2-D Quad mesh + # ------------------------------------------------------------------ + pts, conn, off, ct, ls, nc = quad_data + for row in [ + bench_cut_vtk_mesh( + pts, conn, off, ct, ls, nc, "cut_vtk_mesh quad phi<0", args.repeat + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "runtime_quad quad phi<0", + args.order, + args.repeat, + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi=0", + "runtime_quad quad phi=0", + args.order, + args.repeat, + ), + bench_physical_points( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "physical_pts quad phi<0", + args.order, + args.repeat, + ), + ]: + print_row(*row) + + print() + + # ------------------------------------------------------------------ + # 3-D Tetrahedron mesh + # ------------------------------------------------------------------ + if HAS_PV and tet_data is not None: + pts, conn, off, ct, ls, nc = tet_data + for row in [ + bench_cut_vtk_mesh( + pts, conn, off, ct, ls, nc, "cut_vtk_mesh tet phi<0", args.repeat + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "runtime_quad tet phi<0", + args.order, + args.repeat, + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi=0", + "runtime_quad tet phi=0", + args.order, + args.repeat, + ), + bench_physical_points( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "physical_pts tet phi<0", + args.order, + args.repeat, + ), + ]: + print_row(*row) + print() + + # ------------------------------------------------------------------ + # 3-D Hex mesh + # ------------------------------------------------------------------ + pts, conn, off, ct, ls, nc = hex_data + for row in [ + bench_cut_vtk_mesh( + pts, conn, off, ct, ls, nc, "cut_vtk_mesh hex phi<0", args.repeat + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "runtime_quad hex phi<0", + args.order, + args.repeat, + ), + bench_runtime_quadrature( + pts, + conn, + off, + ct, + ls, + nc, + "phi=0", + "runtime_quad hex phi=0", + args.order, + args.repeat, + ), + bench_physical_points( + pts, + conn, + off, + ct, + ls, + nc, + "phi<0", + "physical_pts hex phi<0", + args.order, + args.repeat, + ), + ]: + print_row(*row) + + print(SEP) + print("Times are minimum over all repetitions (best-of-N).") + print("Mcells/s = background cells processed per second.") + print("Mpts/s = quadrature points mapped per second.") + + +if __name__ == "__main__": + main() diff --git a/python/demo/meshview_levelset_demo.py b/python/demo/meshview_levelset_demo.py new file mode 100644 index 0000000..398a1f2 --- /dev/null +++ b/python/demo/meshview_levelset_demo.py @@ -0,0 +1,325 @@ +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: MeshView + LevelSetFunction Python wrappers +================================================== + +Demonstrates: + * Building a ``cutcells.MeshView`` from a pyvista ``UnstructuredGrid``. + * Building a ``cutcells.LevelSetFunction`` from a plain Python callable. + * Calling ``cutcells.cut_mesh_view`` to obtain cut meshes for + – the interface (phi = 0) + – the inside (phi < 0) + * Visualising the results with pyvista. + +The background mesh is a 2-D Delaunay triangulation of a square [-1, 1]^2. +The level-set is a circle of radius 0.7 centred at the origin: + phi(x) = sqrt(x0^2 + x1^2) - 0.7 +""" + +import argparse + +import numpy as np + +try: + import pyvista as pv +except ImportError as exc: + raise SystemExit( + "pyvista is required for this demo. " + "Install with: python -m pip install pyvista\n" + f"Import error: {exc}" + ) + +import cutcells + + +# --------------------------------------------------------------------------- +# Level-set definition +# --------------------------------------------------------------------------- + +RADIUS = 0.7 +CENTER = np.array([0.0, 0.0, 0.0]) + + +def circle_phi(x: np.ndarray) -> float: + """Signed-distance to a circle of radius RADIUS centred at CENTER. + + ``x`` is a 1-D NumPy array of length 3 (pyvista always stores 3-D coords). + Returns a float: negative inside the circle, positive outside. + """ + return float(np.sqrt(np.sum((x - CENTER) ** 2)) - RADIUS) + + +# --------------------------------------------------------------------------- +# Helper: build MeshView from a pyvista UnstructuredGrid +# --------------------------------------------------------------------------- + + +def mesh_view_from_pyvista(grid: pv.UnstructuredGrid, tdim: int) -> cutcells.MeshView: + """Wrap a pyvista UnstructuredGrid as a cutcells MeshView. + + Parameters + ---------- + grid : pv.UnstructuredGrid + Source grid. Points must already by stored as (N, 3) float64. + tdim : int + Topological dimension of the cells (e.g. 2 for triangles/quads). + + Returns + ------- + cutcells.MeshView (MeshView_float64 alias) + Zero-copy view backed by the pyvista arrays; the pyvista grid must + remain alive as long as the MeshView is used. + """ + coordinates = np.asarray(grid.points, dtype=np.float64) # (N, 3) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) # flat CSR + offsets = np.asarray(grid.offset, dtype=np.int32) # (ncells+1,) + cell_types_np = np.asarray(grid.celltypes, dtype=np.int32) # VTK type IDs + + return cutcells.MeshView( + coordinates=coordinates, + connectivity=connectivity, + offsets=offsets, + cell_types=cell_types_np, + tdim=tdim, + ) + + +# --------------------------------------------------------------------------- +# Mesh generation +# --------------------------------------------------------------------------- + + +def create_triangle_mesh(x0: float, y0: float, x1: float, y1: float, N: int): + """Create a 2-D Delaunay triangle mesh on [x0,x1]×[y0,y1]. + + Returns a pyvista UnstructuredGrid with triangular cells. + """ + x = np.linspace(x0, x1, num=N) + y = np.linspace(y0, y1, num=N) + xx, yy, zz = np.meshgrid(x, y, [0.0]) + points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + poly = pv.PolyData(points).delaunay_2d() + return pv.UnstructuredGrid(poly) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Demo: cut_mesh_view with MeshView and LevelSetFunction" + ) + parser.add_argument( + "--n", + type=int, + default=30, + help="Grid points per axis (default: 30)", + ) + parser.add_argument( + "--no-plot", + action="store_true", + help="Skip interactive visualisation (useful for CI / off-screen runs)", + ) + args = parser.parse_args() + + N = int(args.n) + + # ------------------------------------------------------------------ + # 1. Build the background pyvista grid and wrap it in a MeshView + # ------------------------------------------------------------------ + print(f"Creating {N}×{N} Delaunay triangle mesh …") + grid = create_triangle_mesh(-1.0, -1.0, 1.0, 1.0, N) + print(f" nodes: {grid.n_points}, cells: {grid.n_cells}") + + mesh_view = mesh_view_from_pyvista(grid, tdim=2) + print( + f" MeshView gdim={mesh_view.gdim}, tdim={mesh_view.tdim}, " + f"num_nodes={mesh_view.num_nodes()}, num_cells={mesh_view.num_cells()}" + ) + + # ------------------------------------------------------------------ + # 2. Build a LevelSetFunction from a Python callable + # ------------------------------------------------------------------ + # The callable receives a length-3 NumPy array (x) and may optionally + # accept a second argument cell_id (ignored here). + level_set = cutcells.LevelSetFunction( + value=circle_phi, + gdim=mesh_view.gdim, # 3 (pyvista always stores 3-D coords) + ) + print( + f"\nLevelSetFunction has_value={level_set.has_value()}, " + f"has_nodal_values={level_set.has_nodal_values()}" + ) + + # ------------------------------------------------------------------ + # 3. Cut the mesh – interface (phi = 0) + # ------------------------------------------------------------------ + print("\nCutting mesh for phi=0 (interface) …") + cut_interface = cutcells.cut_mesh_view( + mesh_view, level_set, "phi=0", triangulate=True + ) + print( + f" Interface cut mesh: {len(cut_interface.vtk_types)} cells, " + f"{len(cut_interface.vertex_coords)} vertices" + ) + + # ------------------------------------------------------------------ + # 4. Cut the mesh – interior (phi < 0) + # ------------------------------------------------------------------ + print("Cutting mesh for phi<0 (inside) …") + cut_inside = cutcells.cut_mesh_view(mesh_view, level_set, "phi<0", triangulate=True) + print( + f" Inside cut mesh: {len(cut_inside.vtk_types)} cells, " + f"{len(cut_inside.vertex_coords)} vertices" + ) + + # ------------------------------------------------------------------ + # 5. (Optional) also build nodal values and use LevelSetFunction + # with nodal_values instead of value callable – same result + # ------------------------------------------------------------------ + print("\nBuilding nodal level-set values manually …") + ls_nodal = np.array([circle_phi(p) for p in grid.points], dtype=np.float64) + level_set_nodal = cutcells.LevelSetFunction( + nodal_values=ls_nodal, + gdim=mesh_view.gdim, + ) + print( + f" LevelSetFunction (nodal) has_nodal_values={level_set_nodal.has_nodal_values()}" + ) + + cut_inside_nodal = cutcells.cut_mesh_view( + mesh_view, level_set_nodal, "phi<0", triangulate=True + ) + assert len(cut_inside_nodal.vtk_types) == len(cut_inside.vtk_types), ( + "Nodal and callable level-set should give identical cut meshes" + ) + print(" Nodal and callable results agree ✓") + + # ------------------------------------------------------------------ + # 6. Runtime Quadrature and Physical Mapping + # ------------------------------------------------------------------ + print("\nComputing runtime quadrature for phi<0 …") + order = 2 + # Use the flat arrays from the grid + points_flat = np.asarray(grid.points, dtype=np.float64).ravel() + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offsets = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) + + # Calculate quadrature rules in reference space for phi<0 + q_rules_inside = cutcells.runtime_quadrature( + ls_nodal, + points_flat, + connectivity, + offsets, + celltypes, + "phi<0", + triangulate=True, + order=order, + ) + print(f" Inside: Generated {len(q_rules_inside.weights)} total quadrature points.") + + # Calculate quadrature rules in reference space for phi=0 + print("Computing runtime quadrature for phi=0 …") + q_rules_interface = cutcells.runtime_quadrature( + ls_nodal, + points_flat, + connectivity, + offsets, + celltypes, + "phi=0", + triangulate=True, + order=order, + ) + print( + f" Interface: Generated {len(q_rules_interface.weights)} total quadrature points." + ) + + # Map to physical space + print(" Mapping quadrature points to physical space …") + q_points_inside_phys = cutcells.physical_points( + q_rules_inside, points_flat, connectivity, offsets, celltypes + ).reshape(-1, 3) + + q_points_interface_phys = cutcells.physical_points( + q_rules_interface, points_flat, connectivity, offsets, celltypes + ).reshape(-1, 3) + + # ------------------------------------------------------------------ + # 7. Visualise + # ------------------------------------------------------------------ + if args.no_plot: + print("\n--no-plot specified, skipping visualisation.") + return + + # -- wrap cut results as pyvista grids -- + pv_interface = pv.UnstructuredGrid( + cut_interface.cells, + cut_interface.vtk_types, + np.asarray(cut_interface.vertex_coords, dtype=np.float64), + ) + pv_inside = pv.UnstructuredGrid( + cut_inside.cells, + cut_inside.vtk_types, + np.asarray(cut_inside.vertex_coords, dtype=np.float64), + ) + + # -- inside background cells (entirely inside the circle) -- + points_flat = np.asarray(grid.points, dtype=np.float64).ravel() + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offsets = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) + inside_ids = cutcells.locate_cells( + ls_nodal, points_flat, connectivity, offsets, celltypes, "phi<0" + ) + pv_inside_bg = grid.extract_cells(inside_ids) + + # -- combined inside view: background inside cells + cut cells -- + pv_combined = pv_inside_bg.merge(pv_inside) + + # -- wrap quadrature points as pyvista points -- + pv_q_points_inside = pv.PolyData(q_points_inside_phys) + pv_q_points_interface = pv.PolyData(q_points_interface_phys) + + # Plot + pl = pv.Plotter(shape=(1, 2), title="CutCells – MeshView + LevelSetFunction demo") + + pl.subplot(0, 0) + pl.add_title("Interface (phi = 0) + Quad Points") + pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) + pl.add_mesh(pv_interface, show_edges=True, color="red") + pl.add_mesh( + pv_q_points_interface, + color="yellow", + point_size=10, + render_points_as_spheres=True, + label=f"Interface Points ({len(q_rules_interface.weights)})", + ) + pl.add_legend() + pl.view_xy() + + pl.subplot(0, 1) + pl.add_title("Inside domain (phi < 0) + Quad Points") + pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) + pl.add_mesh(pv_combined, show_edges=True, color="steelblue", opacity=0.8) + pl.add_mesh( + pv_q_points_inside, + color="orange", + point_size=10, + render_points_as_spheres=True, + label=f"Inside Points ({len(q_rules_inside.weights)})", + ) + pl.add_legend() + pl.view_xy() + + pl.show() + + +if __name__ == "__main__": + main() diff --git a/tablegen/cutcells_tablegen/emit_c_headers.py b/tablegen/cutcells_tablegen/emit_c_headers.py index b379b4a..c794652 100644 --- a/tablegen/cutcells_tablegen/emit_c_headers.py +++ b/tablegen/cutcells_tablegen/emit_c_headers.py @@ -249,7 +249,9 @@ def _emit_tet_like_header( end = arrays["sub_element_offset"][i + 1] num_subcells_per_case.append(end - start) - lines.append(f"// Number of subcells produced for each case ({table_kind} volume)") + lines.append( + f"// Number of subcells produced for each case ({table_kind} volume)" + ) lines.append( f"constexpr int num_subcells_{table_kind}[{n_cases}] = " + _format_int_list(num_subcells_per_case) diff --git a/tablegen/scripts/gen_quadrature_tables.py b/tablegen/scripts/gen_quadrature_tables.py index ac58080..6858cd4 100644 --- a/tablegen/scripts/gen_quadrature_tables.py +++ b/tablegen/scripts/gen_quadrature_tables.py @@ -16,24 +16,13 @@ import sys import os import argparse +import basix # --------------------------------------------------------------------------- # Basix path bootstrap – use environment or well-known local build. # --------------------------------------------------------------------------- _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -_DEFAULT_BASIX_PYTHON = os.path.join(os.path.dirname(_REPO_ROOT), "basix", "python") -_DEFAULT_BASIX_SO = os.path.join( - os.path.dirname(_REPO_ROOT), "basix", "build", "python" -) -for _p in [ - os.environ.get("BASIX_SO_PATH", _DEFAULT_BASIX_SO), - os.environ.get("BASIX_PYPATH", _DEFAULT_BASIX_PYTHON), -]: - if _p and _p not in sys.path: - sys.path.insert(0, _p) - -import basix # noqa: E402 (must come after path fixup) # --------------------------------------------------------------------------- # Cell-type table: (name, basix.CellType, topological_dimension) @@ -92,7 +81,14 @@ def _write_cell_header(cell_name, basix_ct, tdim, max_order, out_dir): ) ) for order in range(1, max_order + 1): - pts, wts = basix.make_quadrature(basix_ct, order) + # Use Gauss-Lobatto-Legendre (GLL) for 1D interval tables so + # endpoints are included; keep defaults for other cell types. + if basix_ct == basix.CellType.interval: + pts, wts = basix.make_quadrature( + basix_ct, order, rule=basix.QuadratureType.gll + ) + else: + pts, wts = basix.make_quadrature(basix_ct, order) npts = pts.shape[0] pts_flat = pts.ravel() From dd3d68e43f24b42e73447e1c9ca74c93f6e7d386 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Wed, 8 Apr 2026 22:18:32 +0200 Subject: [PATCH 06/23] Add higher-order level set infrastructure and mesh utilities C++ changes: - Add LevelSetType enum (Analytical/Polynomial) to level_set.h - Add LevelSetMeshData struct for per-cell DOF storage with CSR layout - Add level_set.cpp with implementation of LevelSetMeshData accessors - Extend write_vtk.h/cpp with higher-order mesh output support - Update CMakeLists.txt for new source files and dependencies Python changes: - Add mesh_utils.py with structured mesh generation: * rectangle_triangle_mesh, rectangle_quad_mesh (2D) * box_tetrahedron_mesh, box_hex_mesh (3D) * mesh_from_pyvista for PyVista interoperability - Update wrapper.cpp with nanobind bindings for LevelSetFunction extensions - Update __init__.py to export mesh utility functions - Add test_mesh_utils.py for mesh generation tests - Add test_level_set_interpolate.py, test_level_set_mesh_data.py, test_level_set_vtu.py for higher-order level set tests - Add demo_level_set_ho_vtk.py, demo_level_set_ho_curved_vtk.py, demo_meshview.py for higher-order demonstrations Removed: - Delete obsolete demo files (bench_cut_pipeline.py, meshview_levelset_demo.py) This commit establishes the foundation for the higher-order cut-cell pipeline as described in the implementation plan. --- cpp/CMakeLists.txt | 44 +- cpp/src/CMakeLists.txt | 9 +- cpp/src/level_set.cpp | 1097 +++++++++++++++++++ cpp/src/level_set.h | 126 ++- cpp/src/write_vtk.cpp | 521 ++++++++- cpp/src/write_vtk.h | 7 +- python/CMakeLists.txt | 223 ++-- python/cutcells/__init__.py | 15 + python/cutcells/mesh_utils.py | 162 +++ python/cutcells/wrapper.cpp | 233 ++++ python/demo/demo_level_set_ho_curved_vtk.py | 213 ++++ python/demo/demo_level_set_ho_vtk.py | 97 ++ python/demo/demo_meshview.py | 290 +++++ python/tests/test_level_set_interpolate.py | 123 +++ python/tests/test_level_set_mesh_data.py | 93 ++ python/tests/test_level_set_vtu.py | 210 ++++ python/tests/test_mesh_utils.py | 17 + 17 files changed, 3370 insertions(+), 110 deletions(-) create mode 100644 cpp/src/level_set.cpp create mode 100644 python/cutcells/mesh_utils.py create mode 100644 python/demo/demo_level_set_ho_curved_vtk.py create mode 100644 python/demo/demo_level_set_ho_vtk.py create mode 100644 python/demo/demo_meshview.py create mode 100644 python/tests/test_level_set_interpolate.py create mode 100644 python/tests/test_level_set_mesh_data.py create mode 100644 python/tests/test_level_set_vtu.py create mode 100644 python/tests/test_mesh_utils.py diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d5088ff..b43c30c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1,26 +1,20 @@ cmake_minimum_required(VERSION 3.21) -# ------------------------------------------------------------------------------ -# Set project name and version number project(CutCells VERSION "0.2.0" LANGUAGES CXX C) -# Use C++20 set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Options option(BUILD_SHARED_LIBS "Build CutCells with shared libraries." ON) -#add_feature_info(BUILD_SHARED_LIBS BUILD_SHARED_LIBS "Build cutfemx with shared libraries.") -include(FeatureSummary) +include(GNUInstallDirs) include(CheckIPOSupported) -# Source files add_subdirectory(src) -find_package(OpenMP) -if(OpenMP_CXX_FOUND) +find_package(OpenMP QUIET COMPONENTS CXX) +if(OpenMP_CXX_FOUND AND TARGET OpenMP::OpenMP_CXX) target_link_libraries(cutcells PUBLIC OpenMP::OpenMP_CXX) endif() @@ -29,41 +23,41 @@ if(CUTCELLS_IPO_SUPPORTED) set_property(TARGET cutcells PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) endif() -set_target_properties(cutcells PROPERTIES PRIVATE_HEADER "${HEADERS}") target_include_directories(cutcells PUBLIC - $ - "$") + $ + $) -install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells/${DIR} -COMPONENT Development) +install(FILES ${HEADERS} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells + COMPONENT Development) # Install generated headers produced at build time (tables, etc.) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/generated - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells/src + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cutcells COMPONENT Development FILES_MATCHING PATTERN "*.h") # Install the cutcells library install(TARGETS cutcells EXPORT CutCellsTargets - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - PRIVATE_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/src RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT RuntimeExecutables LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT RuntimeLibraries ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT Development) -# Configure CMake helpers include(CMakePackageConfigHelpers) -write_basic_package_version_file(CutCellsConfigVersion.cmake VERSION ${PACKAGE_VERSION} +write_basic_package_version_file(CutCellsConfigVersion.cmake + VERSION ${PROJECT_VERSION} COMPATIBILITY AnyNewerVersion) -configure_package_config_file(CutCellsConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfig.cmake +configure_package_config_file(CutCellsConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cutcells) -# Install CMake files -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfigVersion.cmake +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/CutCellsConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cutcells COMPONENT Development) -install(EXPORT CutCellsTargets FILE CutCellsTargets.cmake NAMESPACE CUTCELLS:: +install(EXPORT CutCellsTargets + FILE CutCellsTargets.cmake + NAMESPACE CUTCELLS:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cutcells) - - diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 18bd855..a12885a 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -1,12 +1,8 @@ -# ------------------------------------------------------------------------------ include(GNUInstallDirs) -# ------------------------------------------------------------------------------ -# Declare the library (target) add_library(cutcells) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in version.h) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) # Make headers in this directory discoverable for generated table headers # (e.g. generated/*_tables.h includes "cell_types.h"). @@ -51,13 +47,10 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/level_set.cpp ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.cpp ${CMAKE_CURRENT_SOURCE_DIR}/write_vtk.cpp - ) target_compile_options(cutcells PRIVATE $<$:-O3>) - - - diff --git a/cpp/src/level_set.cpp b/cpp/src/level_set.cpp new file mode 100644 index 0000000..f48ad17 --- /dev/null +++ b/cpp/src/level_set.cpp @@ -0,0 +1,1097 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "level_set.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cell_types.h" + +namespace cutcells +{ +namespace +{ + +template +struct EdgeKey +{ + I v0 = -1; + I v1 = -1; + + bool operator==(const EdgeKey&) const = default; +}; + +template +struct EdgeKeyHash +{ + std::size_t operator()(const EdgeKey& key) const noexcept + { + std::size_t seed = 0; + seed ^= std::hash{}(key.v0) + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + seed ^= std::hash{}(key.v1) + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + return seed; + } +}; + +template +struct FaceKey +{ + int nverts = 0; + std::array verts = {-1, -1, -1, -1}; + + bool operator==(const FaceKey&) const = default; +}; + +template +struct FaceKeyHash +{ + std::size_t operator()(const FaceKey& key) const noexcept + { + std::size_t seed = std::hash{}(key.nverts); + for (int i = 0; i < key.nverts; ++i) + seed ^= std::hash{}(key.verts[static_cast(i)]) + + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + return seed; + } +}; + +struct Param2Key +{ + int a = 0; + int b = 0; + + bool operator==(const Param2Key&) const = default; +}; + +struct Param2KeyHash +{ + std::size_t operator()(const Param2Key& key) const noexcept + { + std::size_t seed = 0; + seed ^= std::hash{}(key.a) + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + seed ^= std::hash{}(key.b) + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + return seed; + } +}; + +template +struct FaceData +{ + cell::type face_type = cell::type::point; + int nverts = 0; + std::array verts = {-1, -1, -1, -1}; + std::unordered_map dof_map; +}; + +template +I append_dof(LevelSetMeshData& out, std::span x, + int8_t parent_dim, int32_t parent_id, std::span params) +{ + const I dof = out.num_dofs(); + out.dof_coordinates.insert(out.dof_coordinates.end(), x.begin(), x.end()); + out.dof_parent_dim.push_back(parent_dim); + out.dof_parent_id.push_back(parent_id); + out.dof_parent_param.insert(out.dof_parent_param.end(), params.begin(), params.end()); + out.dof_parent_param_offset.push_back( + static_cast(out.dof_parent_param.size())); + return dof; +} + +template +void initialize_vertex_dofs(const MeshView& mesh, LevelSetMeshData& out) +{ + out.dof_coordinates.assign(mesh.coordinates.begin(), mesh.coordinates.end()); + const I ndofs = mesh.num_nodes(); + out.dof_parent_dim.assign(static_cast(ndofs), 0); + out.dof_parent_id.resize(static_cast(ndofs)); + for (I i = 0; i < ndofs; ++i) + out.dof_parent_id[static_cast(i)] = static_cast(i); + out.dof_parent_param_offset.assign(static_cast(ndofs) + 1, 0); +} + +template +cell::type infer_cell_type(const MeshView& mesh, I cell_id) +{ + if (mesh.has_cell_types()) + { + return cell::map_vtk_type_to_cell_type( + static_cast(mesh.cell_type(cell_id))); + } + + const I n = mesh.cell_num_nodes(cell_id); + switch (mesh.tdim) + { + case 1: + if (n == 2) + return cell::type::interval; + break; + case 2: + if (n == 3) + return cell::type::triangle; + if (n == 4) + return cell::type::quadrilateral; + break; + case 3: + if (n == 4) + return cell::type::tetrahedron; + if (n == 5) + return cell::type::pyramid; + if (n == 6) + return cell::type::prism; + if (n == 8) + return cell::type::hexahedron; + break; + } + + throw std::runtime_error("create_level_set_mesh_data: unsupported cell type inference from MeshView"); +} + +inline std::vector vtk_to_basix_vertex_permutation(cell::type ctype, bool has_vtk_order) +{ + if (!has_vtk_order) + return {}; + + switch (ctype) + { + case cell::type::interval: + return {0, 1}; + case cell::type::triangle: + return {0, 1, 2}; + case cell::type::quadrilateral: + return {0, 1, 3, 2}; + case cell::type::tetrahedron: + return {0, 1, 2, 3}; + case cell::type::hexahedron: + return {0, 1, 3, 2, 4, 5, 7, 6}; + case cell::type::prism: + return {0, 1, 2, 3, 4, 5}; + case cell::type::pyramid: + return {0, 1, 2, 3, 4}; + default: + throw std::runtime_error("create_level_set_mesh_data: unsupported cell type"); + } +} + +template +std::vector cell_vertices_in_basix_order(const MeshView& mesh, I cell_id, + cell::type ctype) +{ + const std::span nodes = mesh.cell_nodes(cell_id); + const int nverts = cell::get_num_vertices(ctype); + if (static_cast(nodes.size()) != nverts) + { + throw std::runtime_error( + "create_level_set_mesh_data: MeshView cell connectivity must contain only corner vertices"); + } + + const std::vector perm = vtk_to_basix_vertex_permutation(ctype, mesh.has_cell_types()); + std::vector verts; + verts.reserve(static_cast(nverts)); + if (perm.empty()) + { + verts.insert(verts.end(), nodes.begin(), nodes.end()); + } + else + { + for (int i = 0; i < nverts; ++i) + verts.push_back(nodes[static_cast(perm[static_cast(i)])]); + } + return verts; +} + +template +std::vector gather_vertex_coordinates(const MeshView& mesh, + const std::vector& vertex_ids) +{ + std::vector verts; + verts.reserve(static_cast(vertex_ids.size() * mesh.gdim)); + for (I v : vertex_ids) + { + const T* x = mesh.node(v); + verts.insert(verts.end(), x, x + mesh.gdim); + } + return verts; +} + +template +struct FaceDef +{ + cell::type face_type = cell::type::point; + int nverts = 0; + std::array verts = {-1, -1, -1, -1}; +}; + +inline std::span> basix_edges(cell::type ctype) +{ + static constexpr std::array, 1> interval = {{{0, 1}}}; + static constexpr std::array, 3> triangle = {{{1, 2}, {0, 2}, {0, 1}}}; + static constexpr std::array, 4> quadrilateral = {{{0, 1}, {0, 2}, {1, 3}, {2, 3}}}; + static constexpr std::array, 6> tetrahedron = {{{2, 3}, {1, 3}, {1, 2}, {0, 3}, {0, 2}, {0, 1}}}; + static constexpr std::array, 12> hexahedron = {{ + {0, 1}, {2, 3}, {0, 2}, {1, 3}, + {4, 5}, {6, 7}, {4, 6}, {5, 7}, + {0, 4}, {1, 5}, {2, 6}, {3, 7} + }}; + static constexpr std::array, 9> prism = {{ + {0, 1}, {0, 2}, {1, 2}, + {0, 3}, {1, 4}, {2, 5}, + {3, 4}, {3, 5}, {4, 5} + }}; + static constexpr std::array, 8> pyramid = {{ + {0, 1}, {0, 3}, {1, 2}, {2, 3}, + {0, 4}, {1, 4}, {2, 4}, {3, 4} + }}; + + switch (ctype) + { + case cell::type::interval: return std::span(interval); + case cell::type::triangle: return std::span(triangle); + case cell::type::quadrilateral: return std::span(quadrilateral); + case cell::type::tetrahedron: return std::span(tetrahedron); + case cell::type::hexahedron: return std::span(hexahedron); + case cell::type::prism: return std::span(prism); + case cell::type::pyramid: return std::span(pyramid); + default: return {}; + } +} + +inline std::span> basix_faces(cell::type ctype) +{ + static constexpr std::array, 4> tetrahedron = {{ + {cell::type::triangle, 3, {1, 2, 3, -1}}, + {cell::type::triangle, 3, {0, 2, 3, -1}}, + {cell::type::triangle, 3, {0, 1, 3, -1}}, + {cell::type::triangle, 3, {0, 1, 2, -1}}, + }}; + static constexpr std::array, 6> hexahedron = {{ + {cell::type::quadrilateral, 4, {0, 1, 2, 3}}, + {cell::type::quadrilateral, 4, {4, 5, 6, 7}}, + {cell::type::quadrilateral, 4, {0, 1, 4, 5}}, + {cell::type::quadrilateral, 4, {0, 2, 4, 6}}, + {cell::type::quadrilateral, 4, {1, 3, 5, 7}}, + {cell::type::quadrilateral, 4, {2, 3, 6, 7}}, + }}; + static constexpr std::array, 5> prism = {{ + {cell::type::triangle, 3, {0, 1, 2, -1}}, + {cell::type::triangle, 3, {3, 4, 5, -1}}, + {cell::type::quadrilateral, 4, {0, 1, 3, 4}}, + {cell::type::quadrilateral, 4, {0, 2, 3, 5}}, + {cell::type::quadrilateral, 4, {1, 2, 4, 5}}, + }}; + static constexpr std::array, 5> pyramid = {{ + {cell::type::quadrilateral, 4, {0, 1, 2, 3}}, + {cell::type::triangle, 3, {0, 1, 4, -1}}, + {cell::type::triangle, 3, {1, 2, 4, -1}}, + {cell::type::triangle, 3, {2, 3, 4, -1}}, + {cell::type::triangle, 3, {0, 3, 4, -1}}, + }}; + + switch (ctype) + { + case cell::type::tetrahedron: return std::span(tetrahedron); + case cell::type::hexahedron: return std::span(hexahedron); + case cell::type::prism: return std::span(prism); + case cell::type::pyramid: return std::span(pyramid); + default: return {}; + } +} + +template +std::array canonical_triangle(const std::array& v) +{ + std::array, 6> perms = {{ + {{v[0], v[1], v[2]}}, + {{v[0], v[2], v[1]}}, + {{v[1], v[0], v[2]}}, + {{v[1], v[2], v[0]}}, + {{v[2], v[0], v[1]}}, + {{v[2], v[1], v[0]}} + }}; + return *std::min_element(perms.begin(), perms.end()); +} + +template +std::array canonical_quad(const std::array& v) +{ + std::array, 8> perms = {{ + {{v[0], v[1], v[2], v[3]}}, + {{v[1], v[3], v[0], v[2]}}, + {{v[3], v[2], v[1], v[0]}}, + {{v[2], v[0], v[3], v[1]}}, + {{v[0], v[2], v[1], v[3]}}, + {{v[2], v[3], v[0], v[1]}}, + {{v[3], v[1], v[2], v[0]}}, + {{v[1], v[0], v[3], v[2]}} + }}; + return *std::min_element(perms.begin(), perms.end()); +} + +template +FaceKey canonical_face_key(cell::type face_type, const std::vector& local_vertices) +{ + FaceKey key; + key.nverts = static_cast(local_vertices.size()); + if (face_type == cell::type::triangle) + { + const std::array loc = { + local_vertices[0], local_vertices[1], local_vertices[2]}; + const std::array can = canonical_triangle(loc); + std::copy(can.begin(), can.end(), key.verts.begin()); + } + else if (face_type == cell::type::quadrilateral) + { + const std::array loc = { + local_vertices[0], local_vertices[1], local_vertices[2], local_vertices[3]}; + const std::array can = canonical_quad(loc); + std::copy(can.begin(), can.end(), key.verts.begin()); + } + else + { + throw std::runtime_error("create_level_set_mesh_data: unsupported face type"); + } + return key; +} + +template +std::vector local_to_canonical_positions(const std::vector& local_vertices, + const std::array& canonical_vertices, + int nverts) +{ + std::vector pos(static_cast(nverts), -1); + for (int p = 0; p < nverts; ++p) + { + for (int q = 0; q < nverts; ++q) + { + if (local_vertices[static_cast(p)] == canonical_vertices[static_cast(q)]) + { + pos[static_cast(p)] = q; + break; + } + } + if (pos[static_cast(p)] < 0) + throw std::runtime_error("create_level_set_mesh_data: invalid face permutation"); + } + return pos; +} + +template +void affine_edge_point(std::span x0, std::span x1, T t, std::vector& out) +{ + out.resize(x0.size()); + for (std::size_t d = 0; d < x0.size(); ++d) + out[d] = (T(1) - t) * x0[d] + t * x1[d]; +} + +template +void bary_triangle_point(std::span x0, std::span x1, std::span x2, + T u, T v, std::vector& out) +{ + const T w0 = T(1) - u - v; + out.resize(x0.size()); + for (std::size_t d = 0; d < x0.size(); ++d) + out[d] = w0 * x0[d] + u * x1[d] + v * x2[d]; +} + +template +void bilinear_quad_point(std::span x0, std::span x1, + std::span x2, std::span x3, + T u, T v, std::vector& out) +{ + const T w0 = (T(1) - u) * (T(1) - v); + const T w1 = u * (T(1) - v); + const T w2 = (T(1) - u) * v; + const T w3 = u * v; + out.resize(x0.size()); + for (std::size_t d = 0; d < x0.size(); ++d) + out[d] = w0 * x0[d] + w1 * x1[d] + w2 * x2[d] + w3 * x3[d]; +} + +template +void bary_tetra_point(std::span x0, std::span x1, + std::span x2, std::span x3, + T u, T v, T w, std::vector& out) +{ + const T w0 = T(1) - u - v - w; + out.resize(x0.size()); + for (std::size_t d = 0; d < x0.size(); ++d) + out[d] = w0 * x0[d] + u * x1[d] + v * x2[d] + w * x3[d]; +} + +template +void multilinear_hex_point(const std::vector& verts, int gdim, + T u, T v, T w, std::vector& out) +{ + out.resize(static_cast(gdim)); + for (int d = 0; d < gdim; ++d) + { + out[static_cast(d)] = + (T(1) - u) * (T(1) - v) * (T(1) - w) * verts[static_cast(d)] + + u * (T(1) - v) * (T(1) - w) * verts[static_cast(gdim + d)] + + (T(1) - u) * v * (T(1) - w) * verts[static_cast(2 * gdim + d)] + + u * v * (T(1) - w) * verts[static_cast(3 * gdim + d)] + + (T(1) - u) * (T(1) - v) * w * verts[static_cast(4 * gdim + d)] + + u * (T(1) - v) * w * verts[static_cast(5 * gdim + d)] + + (T(1) - u) * v * w * verts[static_cast(6 * gdim + d)] + + u * v * w * verts[static_cast(7 * gdim + d)]; + } +} + +template +void multilinear_prism_point(const std::vector& verts, int gdim, + T u, T v, T w, std::vector& out) +{ + const T l0 = T(1) - u - v; + const T l1 = u; + const T l2 = v; + out.resize(static_cast(gdim)); + for (int d = 0; d < gdim; ++d) + { + out[static_cast(d)] = + l0 * (T(1) - w) * verts[static_cast(d)] + + l1 * (T(1) - w) * verts[static_cast(gdim + d)] + + l2 * (T(1) - w) * verts[static_cast(2 * gdim + d)] + + l0 * w * verts[static_cast(3 * gdim + d)] + + l1 * w * verts[static_cast(4 * gdim + d)] + + l2 * w * verts[static_cast(5 * gdim + d)]; + } +} + +template +EdgeKey make_edge_key(I a, I b) +{ + return (a <= b) ? EdgeKey{a, b} : EdgeKey{b, a}; +} + +template +bool edge_matches_canonical(I a, I b, const EdgeKey& key) +{ + return a == key.v0 && b == key.v1; +} + +inline std::array transform_quad_indices(int i, int j, int p, + const std::vector& local_to_can) +{ + static constexpr std::array, 8> perms = {{ + {{0, 1, 2, 3}}, + {{1, 3, 0, 2}}, + {{3, 2, 1, 0}}, + {{2, 0, 3, 1}}, + {{0, 2, 1, 3}}, + {{2, 3, 0, 1}}, + {{3, 1, 2, 0}}, + {{1, 0, 3, 2}}, + }}; + + for (const auto& perm : perms) + { + bool match = true; + for (int k = 0; k < 4; ++k) + { + if (local_to_can[static_cast(k)] != perm[static_cast(k)]) + { + match = false; + break; + } + } + if (!match) + continue; + + switch (&perm - &perms[0]) + { + case 0: return {i, j, 0, 0}; + case 1: return {j, p - i, 0, 0}; + case 2: return {p - i, p - j, 0, 0}; + case 3: return {p - j, i, 0, 0}; + case 4: return {j, i, 0, 0}; + case 5: return {p - i, j, 0, 0}; + case 6: return {p - j, p - i, 0, 0}; + case 7: return {i, p - j, 0, 0}; + } + } + + throw std::runtime_error("create_level_set_mesh_data: unsupported quad face permutation"); +} + +template +I get_or_create_edge_block(const MeshView& mesh, + LevelSetMeshData& out, + std::unordered_map, I, EdgeKeyHash>& edge_ids, + std::vector& edge_first_dof, + I a, I b, int degree) +{ + const EdgeKey key = make_edge_key(a, b); + auto it = edge_ids.find(key); + if (it != edge_ids.end()) + return it->second; + + const I edge_id = static_cast(edge_ids.size()); + edge_ids.emplace(key, edge_id); + edge_first_dof.push_back(static_cast(-1)); + if (degree == 1) + return edge_id; + + edge_first_dof[static_cast(edge_id)] = out.num_dofs(); + std::vector x; + std::array param; + for (int k = 1; k < degree; ++k) + { + const T t = static_cast(k) / static_cast(degree); + affine_edge_point( + std::span(mesh.node(key.v0), static_cast(mesh.gdim)), + std::span(mesh.node(key.v1), static_cast(mesh.gdim)), + t, x); + param[0] = t; + append_dof(out, std::span(x.data(), x.size()), 1, + static_cast(edge_id), + std::span(param.data(), param.size())); + } + return edge_id; +} + +template +I get_or_create_face_block(const MeshView& mesh, + LevelSetMeshData& out, + std::unordered_map, I, FaceKeyHash>& face_ids, + std::vector>& faces, + cell::type face_type, + const std::vector& local_vertices, + int degree) +{ + const FaceKey key = canonical_face_key(face_type, local_vertices); + auto it = face_ids.find(key); + if (it != face_ids.end()) + return it->second; + + const I face_id = static_cast(faces.size()); + face_ids.emplace(key, face_id); + + FaceData face; + face.face_type = face_type; + face.nverts = key.nverts; + face.verts = key.verts; + + if (degree > 2 || face_type == cell::type::quadrilateral) + { + std::vector x; + if (face_type == cell::type::triangle) + { + std::array param; + const T* x0 = mesh.node(face.verts[0]); + const T* x1 = mesh.node(face.verts[1]); + const T* x2 = mesh.node(face.verts[2]); + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + bary_triangle_point( + std::span(x0, static_cast(mesh.gdim)), + std::span(x1, static_cast(mesh.gdim)), + std::span(x2, static_cast(mesh.gdim)), + u, v, x); + param[0] = u; + param[1] = v; + const I dof = append_dof(out, std::span(x.data(), x.size()), 2, + static_cast(face_id), + std::span(param.data(), param.size())); + face.dof_map.emplace(Param2Key{i, j}, dof); + } + } + } + else + { + std::array param; + const T* x0 = mesh.node(face.verts[0]); + const T* x1 = mesh.node(face.verts[1]); + const T* x2 = mesh.node(face.verts[2]); + const T* x3 = mesh.node(face.verts[3]); + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + bilinear_quad_point( + std::span(x0, static_cast(mesh.gdim)), + std::span(x1, static_cast(mesh.gdim)), + std::span(x2, static_cast(mesh.gdim)), + std::span(x3, static_cast(mesh.gdim)), + u, v, x); + param[0] = u; + param[1] = v; + const I dof = append_dof(out, std::span(x.data(), x.size()), 2, + static_cast(face_id), + std::span(param.data(), param.size())); + face.dof_map.emplace(Param2Key{i, j}, dof); + } + } + } + } + + faces.push_back(std::move(face)); + return face_id; +} + +template +void append_interval_cell_dofs(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + out.cell_dofs.push_back(verts[0]); + out.cell_dofs.push_back(verts[1]); + if (degree == 1) + return; + + std::vector x; + std::array param; + for (int k = 1; k < degree; ++k) + { + const T t = static_cast(k) / static_cast(degree); + affine_edge_point( + std::span(mesh.node(verts[0]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[1]), static_cast(mesh.gdim)), + t, x); + param[0] = t; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 1, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } +} + +template +void append_triangle_cell_interior(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + if (degree <= 2) + return; + + std::vector x; + std::array param; + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + bary_triangle_point( + std::span(mesh.node(verts[0]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[1]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[2]), static_cast(mesh.gdim)), + u, v, x); + param[0] = u; + param[1] = v; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 2, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } + } +} + +template +void append_quadrilateral_cell_interior(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + if (degree <= 1) + return; + + std::vector x; + std::array param; + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + bilinear_quad_point( + std::span(mesh.node(verts[0]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[1]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[2]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[3]), static_cast(mesh.gdim)), + u, v, x); + param[0] = u; + param[1] = v; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 2, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } + } +} + +template +void append_tetrahedron_cell_interior(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + if (degree <= 3) + return; + + std::vector x; + std::array param; + for (int k = 1; k <= degree - 3; ++k) + { + for (int j = 1; j <= degree - k - 2; ++j) + { + for (int i = 1; i <= degree - j - k - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + const T w = static_cast(k) / static_cast(degree); + bary_tetra_point( + std::span(mesh.node(verts[0]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[1]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[2]), static_cast(mesh.gdim)), + std::span(mesh.node(verts[3]), static_cast(mesh.gdim)), + u, v, w, x); + param[0] = u; + param[1] = v; + param[2] = w; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 3, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } + } + } +} + +template +void append_hexahedron_cell_interior(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + if (degree <= 1) + return; + + const std::vector xverts = gather_vertex_coordinates(mesh, verts); + std::vector x; + std::array param; + for (int k = 1; k < degree; ++k) + { + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + const T w = static_cast(k) / static_cast(degree); + multilinear_hex_point(xverts, mesh.gdim, u, v, w, x); + param[0] = u; + param[1] = v; + param[2] = w; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 3, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } + } + } +} + +template +void append_prism_cell_interior(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, const std::vector& verts, int degree) +{ + if (degree <= 2) + return; + + const std::vector xverts = gather_vertex_coordinates(mesh, verts); + std::vector x; + std::array param; + for (int k = 1; k < degree; ++k) + { + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + const T w = static_cast(k) / static_cast(degree); + multilinear_prism_point(xverts, mesh.gdim, u, v, w, x); + param[0] = u; + param[1] = v; + param[2] = w; + out.cell_dofs.push_back(append_dof( + out, std::span(x.data(), x.size()), 3, + static_cast(cell_id), + std::span(param.data(), param.size()))); + } + } + } +} + +template +void append_cell_dofs(const MeshView& mesh, LevelSetMeshData& out, + I cell_id, cell::type ctype, const std::vector& verts, + int degree, + std::unordered_map, I, EdgeKeyHash>& edge_ids, + std::vector& edge_first_dof, + std::unordered_map, I, FaceKeyHash>& face_ids, + std::vector>& faces) +{ + for (I v : verts) + out.cell_dofs.push_back(v); + + if (ctype == cell::type::interval) + { + append_interval_cell_dofs(mesh, out, cell_id, verts, degree); + return; + } + + const auto edges = basix_edges(ctype); + for (std::size_t e = 0; e < edges.size(); ++e) + { + const I a = verts[static_cast(edges[e][0])]; + const I b = verts[static_cast(edges[e][1])]; + const EdgeKey key = make_edge_key(a, b); + const bool same_orientation = edge_matches_canonical(a, b, key); + const I edge_id = get_or_create_edge_block( + mesh, out, edge_ids, edge_first_dof, a, b, degree); + if (degree == 1) + continue; + + const I first = edge_first_dof[static_cast(edge_id)]; + for (int k = 1; k < degree; ++k) + { + const int canonical_k = same_orientation ? k : (degree - k); + out.cell_dofs.push_back(first + static_cast(canonical_k - 1)); + } + } + + if (mesh.tdim == 3) + { + const auto faces_local = basix_faces(ctype); + for (std::size_t f = 0; f < faces_local.size(); ++f) + { + const auto& fdef = faces_local[f]; + std::vector fverts; + fverts.reserve(static_cast(fdef.nverts)); + for (int j = 0; j < fdef.nverts; ++j) + fverts.push_back(verts[static_cast(fdef.verts[static_cast(j)])]); + + const I face_id = get_or_create_face_block( + mesh, out, face_ids, faces, fdef.face_type, fverts, degree); + const FaceData& face = faces[static_cast(face_id)]; + const std::vector local_to_can = + local_to_canonical_positions(fverts, face.verts, fdef.nverts); + + if (fdef.face_type == cell::type::triangle) + { + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + int local_bary[3] = {degree - i - j, i, j}; + int can_bary[3] = {0, 0, 0}; + for (int p = 0; p < 3; ++p) + can_bary[local_to_can[static_cast(p)]] = local_bary[p]; + + const auto it = face.dof_map.find(Param2Key{can_bary[1], can_bary[2]}); + if (it == face.dof_map.end()) + throw std::runtime_error("create_level_set_mesh_data: missing triangle face dof"); + out.cell_dofs.push_back(it->second); + } + } + } + else + { + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const auto can = transform_quad_indices(i, j, degree, local_to_can); + const auto it = face.dof_map.find(Param2Key{can[0], can[1]}); + if (it == face.dof_map.end()) + throw std::runtime_error("create_level_set_mesh_data: missing quadrilateral face dof"); + out.cell_dofs.push_back(it->second); + } + } + } + } + } + + switch (ctype) + { + case cell::type::triangle: + append_triangle_cell_interior(mesh, out, cell_id, verts, degree); + break; + case cell::type::quadrilateral: + append_quadrilateral_cell_interior(mesh, out, cell_id, verts, degree); + break; + case cell::type::tetrahedron: + append_tetrahedron_cell_interior(mesh, out, cell_id, verts, degree); + break; + case cell::type::hexahedron: + append_hexahedron_cell_interior(mesh, out, cell_id, verts, degree); + break; + case cell::type::prism: + append_prism_cell_interior(mesh, out, cell_id, verts, degree); + break; + case cell::type::pyramid: + if (degree > 1) + { + throw std::runtime_error( + "create_level_set_mesh_data: pyramid degree > 1 is not implemented yet"); + } + break; + default: + break; + } +} + +template +void validate_mesh_data_args(int gdim, int tdim, int degree, + std::span dof_coordinates, + std::span cell_dofs, + std::span cell_offsets, + std::span cell_types) +{ + if (gdim <= 0) + throw std::runtime_error("create_level_set_mesh_data: gdim must be positive"); + if (tdim <= 0) + throw std::runtime_error("create_level_set_mesh_data: tdim must be positive"); + if (degree < 1) + throw std::runtime_error("create_level_set_mesh_data: degree must be >= 1"); + if (dof_coordinates.size() % static_cast(gdim) != 0) + throw std::runtime_error("create_level_set_mesh_data: dof_coordinates size must be divisible by gdim"); + if (cell_offsets.empty()) + throw std::runtime_error("create_level_set_mesh_data: cell_offsets must not be empty"); + if (cell_offsets.front() != 0) + throw std::runtime_error("create_level_set_mesh_data: cell_offsets must start at 0"); + if (static_cast(cell_offsets.back()) != cell_dofs.size()) + throw std::runtime_error("create_level_set_mesh_data: cell_offsets.back() must equal cell_dofs.size()"); + if (!cell_types.empty() && cell_types.size() + 1 != cell_offsets.size()) + throw std::runtime_error("create_level_set_mesh_data: cell_types size must equal number of cells"); +} + +} // namespace + +template +LevelSetMeshData create_level_set_mesh_data( + const MeshView& mesh, int degree, T merge_tol) +{ + (void)merge_tol; + if (degree < 1) + throw std::runtime_error("create_level_set_mesh_data: degree must be >= 1"); + if (mesh.gdim <= 0 || mesh.tdim <= 0) + throw std::runtime_error("create_level_set_mesh_data: MeshView dimensions must be positive"); + + LevelSetMeshData out; + out.gdim = mesh.gdim; + out.tdim = mesh.tdim; + out.degree = degree; + out.cell_offsets.reserve(static_cast(mesh.num_cells()) + 1); + out.cell_types.reserve(static_cast(mesh.num_cells())); + out.cell_offsets.push_back(0); + + initialize_vertex_dofs(mesh, out); + + std::unordered_map, I, EdgeKeyHash> edge_ids; + std::vector edge_first_dof; + std::unordered_map, I, FaceKeyHash> face_ids; + std::vector> faces; + + for (I cell_id = 0; cell_id < mesh.num_cells(); ++cell_id) + { + const cell::type ctype = infer_cell_type(mesh, cell_id); + const std::vector verts = cell_vertices_in_basix_order(mesh, cell_id, ctype); + append_cell_dofs(mesh, out, cell_id, ctype, verts, degree, + edge_ids, edge_first_dof, face_ids, faces); + + if (mesh.has_cell_types()) + out.cell_types.push_back(mesh.cell_type(cell_id)); + else + out.cell_types.push_back(static_cast(cell::map_cell_type_to_vtk(ctype))); + out.cell_offsets.push_back(static_cast(out.cell_dofs.size())); + } + + return out; +} + +template +LevelSetMeshData create_level_set_mesh_data( + int gdim, + int tdim, + int degree, + std::span dof_coordinates, + std::span cell_dofs, + std::span cell_offsets, + std::span cell_types) +{ + validate_mesh_data_args(gdim, tdim, degree, + dof_coordinates, cell_dofs, cell_offsets, cell_types); + + LevelSetMeshData out; + out.gdim = gdim; + out.tdim = tdim; + out.degree = degree; + out.dof_coordinates.assign(dof_coordinates.begin(), dof_coordinates.end()); + out.cell_dofs.assign(cell_dofs.begin(), cell_dofs.end()); + out.cell_offsets.assign(cell_offsets.begin(), cell_offsets.end()); + out.cell_types.assign(cell_types.begin(), cell_types.end()); + return out; +} + +template +LevelSetFunction create_level_set_function( + std::shared_ptr> mesh_data, + std::span dof_values) +{ + if (!mesh_data) + throw std::runtime_error("create_level_set_function: mesh_data must not be null"); + + const std::size_t expected = static_cast(mesh_data->num_dofs()); + if (dof_values.size() != expected) + { + throw std::runtime_error( + "create_level_set_function: dof_values size mismatch (got " + + std::to_string(dof_values.size()) + ", expected " + + std::to_string(expected) + ")"); + } + + auto owned_values = std::make_shared>(dof_values.begin(), dof_values.end()); + + LevelSetFunction ls; + ls.type = LevelSetType::Polynomial; + ls.gdim = mesh_data->gdim; + ls.mesh_data = std::move(mesh_data); + ls.dof_values = std::span(owned_values->data(), owned_values->size()); + ls.owner = std::static_pointer_cast(owned_values); + return ls; +} + +template LevelSetMeshData create_level_set_mesh_data( + const MeshView& mesh, int degree, float merge_tol); +template LevelSetMeshData create_level_set_mesh_data( + const MeshView& mesh, int degree, double merge_tol); + +template LevelSetMeshData create_level_set_mesh_data( + int gdim, int tdim, int degree, + std::span dof_coordinates, + std::span cell_dofs, + std::span cell_offsets, + std::span cell_types); +template LevelSetMeshData create_level_set_mesh_data( + int gdim, int tdim, int degree, + std::span dof_coordinates, + std::span cell_dofs, + std::span cell_offsets, + std::span cell_types); + +template LevelSetFunction create_level_set_function( + std::shared_ptr> mesh_data, + std::span dof_values); +template LevelSetFunction create_level_set_function( + std::shared_ptr> mesh_data, + std::span dof_values); + +} // namespace cutcells diff --git a/cpp/src/level_set.h b/cpp/src/level_set.h index 92602b9..2626fa6 100644 --- a/cpp/src/level_set.h +++ b/cpp/src/level_set.h @@ -10,23 +10,114 @@ #include #include #include +#include +#include + +#include "mesh_view.h" namespace cutcells { +enum class LevelSetType +{ + Analytical, + Polynomial +}; + +template +struct LevelSetMeshData +{ + int gdim = 0; + int tdim = 0; + int degree = 1; + + // Flattened coordinates of the discrete level-set dofs: + // [x0_0, x0_1, ..., x1_0, x1_1, ...] + std::vector dof_coordinates; + + // CSR-like cell -> global dof map. The local ordering on each cell follows + // Basix ordering for the corresponding cell type and degree. + std::vector cell_dofs; + std::vector cell_offsets; + + // cell types, one per cell when available. + std::vector cell_types; + + // Provenance aligned with AdaptCell: + // 0 = parent vertex, 1 = parent edge, 2 = parent face, 3 = parent cell interior. + std::vector dof_parent_dim; + std::vector dof_parent_id; + std::vector dof_parent_param; + std::vector dof_parent_param_offset; + + I num_dofs() const + { + if (gdim <= 0) + return 0; + return static_cast(dof_coordinates.size() / static_cast(gdim)); + } + + I num_cells() const + { + if (cell_offsets.empty()) + return 0; + return static_cast(cell_offsets.size() - 1); + } + + I cell_num_dofs(I cell_id) const + { + return cell_offsets[static_cast(cell_id) + 1] + - cell_offsets[static_cast(cell_id)]; + } + + std::span cell_dofs_span(I cell_id) const + { + const std::size_t begin = static_cast( + cell_offsets[static_cast(cell_id)]); + const std::size_t end = static_cast( + cell_offsets[static_cast(cell_id) + 1]); + return std::span(cell_dofs.data() + begin, end - begin); + } + + const T* dof_coordinate(I dof_id) const + { + return dof_coordinates.data() + + static_cast(dof_id) * static_cast(gdim); + } + + std::span dof_parent_param_span(I dof_id) const + { + const std::size_t begin = static_cast( + dof_parent_param_offset[static_cast(dof_id)]); + const std::size_t end = static_cast( + dof_parent_param_offset[static_cast(dof_id) + 1]); + return std::span(dof_parent_param.data() + begin, end - begin); + } +}; + template struct LevelSetFunction { + + std::string name = "phi"; + // cell_id == -1 means "unknown / not provided" + // The value and gradient of the level set function in physical coordinates (x) + // together with background cell id. std::function value_fn; std::function grad_fn; - // Optional nodal values + // Legacy low-order nodal storage on mesh vertices. std::span nodal_values; - // Optional keep-alive anchor for Python / external memory + // Optional higher-order mesh/dof layout and corresponding global dof values. + std::shared_ptr> mesh_data; + std::span dof_values; + + // Optional keep-alive anchor for Python / external memory. std::shared_ptr owner; + LevelSetType type = LevelSetType::Analytical; int gdim = 0; bool has_value() const @@ -44,6 +135,16 @@ struct LevelSetFunction return !nodal_values.empty(); } + bool has_mesh_data() const + { + return static_cast(mesh_data); + } + + bool has_dof_values() const + { + return !dof_values.empty(); + } + T value(const T* x, I cell_id = static_cast(-1)) const { if (!value_fn) @@ -66,4 +167,23 @@ struct LevelSetFunction } }; -} // namespace cutcells \ No newline at end of file +template +LevelSetMeshData create_level_set_mesh_data( + const MeshView& mesh, int degree, T merge_tol = T(-1)); + +template +LevelSetMeshData create_level_set_mesh_data( + int gdim, + int tdim, + int degree, + std::span dof_coordinates, + std::span cell_dofs, + std::span cell_offsets, + std::span cell_types = {}); + +template +LevelSetFunction create_level_set_function( + std::shared_ptr> mesh_data, + std::span dof_values); + +} // namespace cutcells diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index 75a368e..aaa88d7 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -7,10 +7,421 @@ #include #include #include +#include +#include +#include +#include +#include #include "write_vtk.h" #include "cell_types.h" +namespace +{ + +constexpr int VTK_LAGRANGE_TRIANGLE = 69; +constexpr int VTK_LAGRANGE_TETRAHEDRON = 71; + +constexpr const char* vtk_byte_order() +{ + if constexpr (std::endian::native == std::endian::little) + return "LittleEndian"; + else + return "BigEndian"; +} + +enum class HOCellFamily +{ + triangle, + tetrahedron +}; + +int vec_pop(std::vector& v, int i) +{ + const auto pos = (i < 0) ? v.end() + i : v.begin() + i; + const int value = *pos; + v.erase(pos); + return value; +} + +int triangle_degree_from_num_nodes(const int num_nodes) +{ + const int n = static_cast((std::sqrt(1 + 8 * num_nodes) - 1) / 2); + if (2 * num_nodes != n * (n + 1)) + throw std::runtime_error("write_level_set_vtu: invalid triangle node count"); + return n - 1; +} + +int tetrahedron_degree_from_num_nodes(const int num_nodes) +{ + int n = 0; + while (n * (n + 1) * (n + 2) < 6 * num_nodes) + ++n; + if (n * (n + 1) * (n + 2) != 6 * num_nodes) + throw std::runtime_error("write_level_set_vtu: invalid tetrahedron node count"); + return n - 1; +} + +std::vector vtk_triangle_remainders(std::vector remainders) +{ + std::vector map; + map.reserve(remainders.size()); + + while (!remainders.empty()) + { + if (remainders.size() == 1) + { + map.push_back(vec_pop(remainders, 0)); + break; + } + + const int degree = triangle_degree_from_num_nodes( + static_cast(remainders.size())); + + map.push_back(vec_pop(remainders, 0)); + map.push_back(vec_pop(remainders, degree - 1)); + map.push_back(vec_pop(remainders, -1)); + + for (int i = 0; i < degree - 1; ++i) + map.push_back(vec_pop(remainders, 0)); + + for (int i = 1, k = degree * (degree - 1) / 2; i < degree; + k -= degree - i, ++i) + { + map.push_back(vec_pop(remainders, -k)); + } + + for (int i = 1, k = 1; i < degree; k += i, ++i) + map.push_back(vec_pop(remainders, -k)); + } + + return map; +} + +std::vector basix_to_vtk_triangle(const int num_nodes) +{ + std::vector map; + map.reserve(num_nodes); + map.insert(map.end(), {0, 1, 2}); + + const int degree = triangle_degree_from_num_nodes(num_nodes); + for (int k = 1; k < degree; ++k) + map.push_back(3 + 2 * (degree - 1) + k - 1); + for (int k = 1; k < degree; ++k) + map.push_back(3 + k - 1); + for (int k = 1; k < degree; ++k) + map.push_back(2 * degree - (k - 1)); + + if (degree < 3) + return map; + + std::vector rem(num_nodes - static_cast(map.size())); + std::iota(rem.begin(), rem.end(), 3 * degree); + std::vector rem_map = vtk_triangle_remainders(std::move(rem)); + map.insert(map.end(), rem_map.begin(), rem_map.end()); + return map; +} + +std::vector vtk_tetrahedron_remainders(std::vector remainders) +{ + std::vector map; + map.reserve(remainders.size()); + + while (!remainders.empty()) + { + if (remainders.size() == 1) + { + map.push_back(vec_pop(remainders, 0)); + break; + } + + const int deg = tetrahedron_degree_from_num_nodes( + static_cast(remainders.size())) + + 1; + map.push_back(vec_pop(remainders, 0)); + map.push_back(vec_pop(remainders, deg - 2)); + map.push_back(vec_pop(remainders, deg * (deg + 1) / 2 - 3)); + map.push_back(vec_pop(remainders, -1)); + + if (deg > 2) + { + for (int i = 0; i < deg - 2; ++i) + map.push_back(vec_pop(remainders, 0)); + + { + int d = deg - 2; + for (int i = 0; i < deg - 2; ++i) + { + map.push_back(vec_pop(remainders, d)); + d += (deg - 3 - i); + } + } + + { + int d = (deg - 2) * (deg - 1) / 2 - 1; + for (int i = 0; i < deg - 2; ++i) + { + map.push_back(vec_pop(remainders, d)); + d -= (2 + i); + } + } + + { + int d = (deg - 3) * (deg - 2) / 2; + for (int i = 0; i < deg - 2; ++i) + { + map.push_back(vec_pop(remainders, d)); + d += ((deg - i) * (deg - i - 1) / 2 - 1); + } + } + + { + int d = (deg - 3) * (deg - 2) / 2 + deg - 3; + for (int i = 0; i < deg - 2; ++i) + { + map.push_back(vec_pop(remainders, d)); + d += ((deg - 2 - i) * (deg - 1 - i) / 2 + deg - 4 - i); + } + } + + { + int d = (deg - 3) * (deg - 2) / 2 + deg - 3 + + (deg - 2) * (deg - 1) / 2 - 1; + for (int i = 0; i < deg - 2; ++i) + { + map.push_back(vec_pop(remainders, d)); + d += ((deg - 3 - i) * (deg - 2 - i) / 2 + deg - i - 5); + } + } + } + + if (deg > 3) + { + { + std::vector dofs; + int d = (deg - 3) * (deg - 2) / 2; + for (int i = 0; i < deg - 3; ++i) + { + for (int ii = 0; ii < deg - 3 - i; ++ii) + dofs.push_back(vec_pop(remainders, d)); + d += ((deg - 2 - i) * (deg - 1 - i) / 2 - 1); + } + + std::vector tri_map = vtk_triangle_remainders(std::move(dofs)); + map.insert(map.end(), tri_map.begin(), tri_map.end()); + } + + { + std::vector dofs; + int start = deg * deg - 4 * deg + 2; + int sub_i_start = deg - 3; + for (int i = 0; i < deg - 3; ++i) + { + int d = start; + int sub_i = sub_i_start; + for (int ii = 0; ii < deg - 3 - i; ++ii) + { + dofs.push_back(vec_pop(remainders, d)); + d += sub_i * (sub_i + 1) / 2 - 2 - i; + sub_i -= 1; + } + + start -= (2 + i); + } + + std::vector tri_map = vtk_triangle_remainders(std::move(dofs)); + map.insert(map.end(), tri_map.begin(), tri_map.end()); + } + + { + std::vector dofs; + int start = (deg - 3) * (deg - 2) / 2; + int sub_i_start = deg - 3; + for (int i = 0; i < deg - 3; ++i) + { + int d = start; + int sub_i = sub_i_start; + for (int ii = 0; ii < deg - 3 - i; ++ii) + { + dofs.push_back(vec_pop(remainders, d)); + d += sub_i * (sub_i + 1) / 2 - 1 - 2 * i; + sub_i -= 1; + } + + start += (deg - 4 - i); + } + + std::vector tri_map = vtk_triangle_remainders(std::move(dofs)); + map.insert(map.end(), tri_map.begin(), tri_map.end()); + } + + { + std::vector dofs; + int add_start = deg - 4; + for (int i = 0; i < deg - 3; ++i) + { + int d = 0; + int add = add_start; + for (int ii = 0; ii < deg - 3 - i; ++ii) + { + dofs.push_back(vec_pop(remainders, d)); + d += add; + add -= 1; + } + + add_start -= 1; + } + + std::vector tri_map = vtk_triangle_remainders(std::move(dofs)); + map.insert(map.end(), tri_map.begin(), tri_map.end()); + } + } + } + + return map; +} + +std::vector basix_to_vtk_tetrahedron(const int num_nodes) +{ + const int degree = tetrahedron_degree_from_num_nodes(num_nodes); + std::vector map; + map.reserve(num_nodes); + map.insert(map.end(), {0, 1, 2, 3}); + + if (degree < 2) + return map; + + int base = 4; + const int edge_dofs = degree - 1; + for (int edge : {5, 2, 4, 3, 1, 0}) + { + if (edge == 4) + { + for (int i = 0; i < edge_dofs; ++i) + map.push_back(base + edge_dofs * (edge + 1) - 1 - i); + } + else + { + for (int i = 0; i < edge_dofs; ++i) + map.push_back(base + edge_dofs * edge + i); + } + } + + if (degree < 3) + return map; + + base += 6 * edge_dofs; + const int n_face_dofs = (degree - 1) * (degree - 2) / 2; + for (int face : {2, 0, 1, 3}) + { + std::vector face_dofs; + face_dofs.reserve(n_face_dofs); + if (face == 2) + { + for (int i = 0; i < n_face_dofs; ++i) + face_dofs.push_back(base + n_face_dofs * face + i); + } + else if (face == 0) + { + for (int i = degree - 3; i >= 0; --i) + { + int d = i; + for (int ii = 0; ii <= i; ++ii) + { + face_dofs.push_back(base + n_face_dofs * face + d); + d += degree - 3 - ii; + } + } + } + else + { + for (int i = 0; i < degree - 2; ++i) + { + int d = i; + for (int ii = 0; ii < degree - 2 - i; ++ii) + { + face_dofs.push_back(base + n_face_dofs * face + d); + d += degree - 2 - ii; + } + } + } + + std::vector face_map = vtk_triangle_remainders(std::move(face_dofs)); + map.insert(map.end(), face_map.begin(), face_map.end()); + } + + if (degree < 4) + return map; + + base += 4 * n_face_dofs; + std::vector remainders((degree - 1) * (degree - 2) * (degree - 3) / 6); + std::iota(remainders.begin(), remainders.end(), base); + std::vector rem_map = vtk_tetrahedron_remainders(std::move(remainders)); + map.insert(map.end(), rem_map.begin(), rem_map.end()); + return map; +} + +HOCellFamily infer_cell_family(const cutcells::LevelSetMeshData& mesh_data, + const int cell_id) +{ + if (!mesh_data.cell_types.empty()) + { + const int vtk_type = mesh_data.cell_types[static_cast(cell_id)]; + if (vtk_type == static_cast(cutcells::cell::vtk_types::VTK_TRIANGLE) + || vtk_type == VTK_LAGRANGE_TRIANGLE) + { + return HOCellFamily::triangle; + } + if (vtk_type == static_cast(cutcells::cell::vtk_types::VTK_TETRA) + || vtk_type == VTK_LAGRANGE_TETRAHEDRON) + { + return HOCellFamily::tetrahedron; + } + + throw std::runtime_error( + "write_level_set_vtu: unsupported VTK cell type " + std::to_string(vtk_type) + + " (supported: triangle, tetrahedron)"); + } + + if (mesh_data.tdim == 2) + return HOCellFamily::triangle; + if (mesh_data.tdim == 3) + return HOCellFamily::tetrahedron; + + throw std::runtime_error( + "write_level_set_vtu: unsupported tdim without cell_types (supported tdim: 2, 3)"); +} + +int expected_local_dofs(const HOCellFamily family, const int degree) +{ + if (family == HOCellFamily::triangle) + return (degree + 1) * (degree + 2) / 2; + return (degree + 1) * (degree + 2) * (degree + 3) / 6; +} + +std::vector basix_to_vtk_lagrange_permutation(const HOCellFamily family, + const int local_dofs, + const int degree) +{ + if (degree < 2 || degree > 4) + { + throw std::runtime_error( + "write_level_set_vtu: unsupported polynomial degree " + std::to_string(degree) + + " (supported: 2, 3, 4)"); + } + + if (local_dofs != expected_local_dofs(family, degree)) + { + throw std::runtime_error( + "write_level_set_vtu: local dof count does not match cell family/degree"); + } + + if (family == HOCellFamily::triangle) + return basix_to_vtk_triangle(local_dofs); + return basix_to_vtk_tetrahedron(local_dofs); +} + +} // namespace namespace cutcells::io { @@ -29,7 +440,8 @@ namespace cutcells::io int num_cells = static_cast(offsets.size()) - 1; ofs << "\n" - << "\n" + << "\n" << "\t\n" << "\t\t\n"; @@ -63,7 +475,7 @@ namespace cutcells::io for(std::size_t i=1;i\n"; - ofs << "\t\t\t "; + ofs << "\t\t\t "; for(auto& type: element_types) { ofs << static_cast(cell::map_cell_type_to_vtk(type)) << " "; @@ -88,4 +500,109 @@ namespace cutcells::io cut_cell._gdim); } + void write_level_set_vtu(std::string filename, + const cutcells::LevelSetFunction& ls, + std::string field_name) + { + if (!ls.has_mesh_data()) + throw std::runtime_error("write_level_set_vtu: level set has no mesh_data"); + if (!ls.has_dof_values()) + throw std::runtime_error("write_level_set_vtu: level set has no dof_values"); + + const auto& mesh_data = *ls.mesh_data; + if (mesh_data.gdim < 2 || mesh_data.gdim > 3) + { + throw std::runtime_error( + "write_level_set_vtu: unsupported geometric dimension " + + std::to_string(mesh_data.gdim) + " (supported: 2 or 3)"); + } + + if (ls.dof_values.size() != static_cast(mesh_data.num_dofs())) + { + throw std::runtime_error( + "write_level_set_vtu: dof_values size does not match mesh_data.num_dofs()"); + } + + const int num_cells = mesh_data.num_cells(); + std::vector connectivity; + connectivity.reserve(mesh_data.cell_dofs.size()); + std::vector offsets; + offsets.reserve(static_cast(num_cells)); + std::vector types; + types.reserve(static_cast(num_cells)); + + for (int cell_id = 0; cell_id < num_cells; ++cell_id) + { + const std::span cell_dofs = mesh_data.cell_dofs_span(cell_id); + const HOCellFamily family = infer_cell_family(mesh_data, cell_id); + const std::vector perm = basix_to_vtk_lagrange_permutation( + family, + static_cast(cell_dofs.size()), + mesh_data.degree); + + for (const int p : perm) + connectivity.push_back(cell_dofs[static_cast(p)]); + + offsets.push_back(static_cast(connectivity.size())); + types.push_back( + (family == HOCellFamily::triangle) + ? VTK_LAGRANGE_TRIANGLE + : VTK_LAGRANGE_TETRAHEDRON); + } + + std::ofstream ofs(filename.c_str(), std::ios::out); + if (!ofs) + throw std::runtime_error("write_level_set_vtu: unable to open file " + filename); + + const int num_points = mesh_data.num_dofs(); + ofs << "\n" + << "\n" + << "\t\n" + << "\t\t\n"; + + ofs << "\t\t\t\n" + << "\t\t\t "; + for (int i = 0; i < num_points; ++i) + { + const std::size_t base = static_cast(i) * static_cast(mesh_data.gdim); + const double x = mesh_data.dof_coordinates[base]; + const double y = mesh_data.dof_coordinates[base + 1]; + const double z = (mesh_data.gdim == 3) ? mesh_data.dof_coordinates[base + 2] : 0.0; + ofs << x << " " << y << " " << z << " "; + } + ofs << "\n" + << "\t\t\t\n"; + + ofs << "\t\t\t\n" + << "\t\t\t "; + for (const double value : ls.dof_values) + ofs << value << " "; + ofs << "\n" + << "\t\t\t\n"; + + ofs << "\t\t\t\n"; + ofs << "\t\t\t "; + for (const int dof : connectivity) + ofs << dof << " "; + ofs << "\n"; + + ofs << "\t\t\t "; + for (const int off : offsets) + ofs << off << " "; + ofs << "\n"; + + ofs << "\t\t\t "; + for (const int type : types) + ofs << type << " "; + ofs << "\n"; + ofs << "\t\t\t\n"; + + ofs << "\t\t\n" + << "\t\n" + << "\n"; + } + } diff --git a/cpp/src/write_vtk.h b/cpp/src/write_vtk.h index 071728c..3dd48cf 100644 --- a/cpp/src/write_vtk.h +++ b/cpp/src/write_vtk.h @@ -7,6 +7,7 @@ #include #include "cut_cell.h" +#include "level_set.h" namespace cutcells::io { @@ -17,4 +18,8 @@ namespace cutcells::io const int gdim); void write_vtk(std::string filename, cell::CutCell& cut_cell); -} \ No newline at end of file + + void write_level_set_vtu(std::string filename, + const cutcells::LevelSetFunction& ls, + std::string field_name = "phi"); +} diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index d149c96..6de86a8 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -1,106 +1,187 @@ cmake_minimum_required(VERSION 3.19) +project(cutcells_nanobind VERSION "0.2.0" LANGUAGES CXX) + # nanobind uses aligned deallocators only present on macOS > 10.14 -if(APPLE) +if(APPLE AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") endif() -project(cutcells_nanobind VERSION "0.2.0" LANGUAGES CXX) - -find_package(OpenMP) - -# Set C++ standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -if (TARGET cutcells) - add_library(CUTCELLS::cutcells ALIAS cutcells) -else() - # Find CutCells (C++) - # Prefer in-repo CutCells configs over any globally/conda-installed libcutcells. - # This avoids ABI mismatches between wrapper headers (from workspace) and an older - # runtime library discovered on the default prefix path. - if (NOT DEFINED CutCells_DIR) - set(_local_cutcells_candidates - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release/prefix/lib/cmake/cutcells" - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build/prefix/lib/cmake/cutcells" - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir/prefix/lib/cmake/cutcells" - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release" - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build" - "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir") - - foreach(_candidate IN LISTS _local_cutcells_candidates) - if (EXISTS "${_candidate}") - if (IS_DIRECTORY "${_candidate}") - set(_candidate_dir "${_candidate}") - else() - get_filename_component(_candidate_dir "${_candidate}" DIRECTORY) - endif() - - if (EXISTS "${_candidate_dir}/CutCellsConfig.cmake" AND EXISTS "${_candidate_dir}/CutCellsTargets.cmake") - set(CutCells_DIR "${_candidate_dir}") - break() - endif() +set(_conda_prefix "$ENV{CONDA_PREFIX}") +if(_conda_prefix) + # Prefer the active conda environment over the base installation. + if(NOT DEFINED Python_ROOT_DIR) + set(Python_ROOT_DIR "${_conda_prefix}") + endif() + if(NOT DEFINED Python_FIND_VIRTUALENV) + set(Python_FIND_VIRTUALENV ONLY) + endif() + if(NOT DEFINED Python_FIND_STRATEGY) + set(Python_FIND_STRATEGY LOCATION) + endif() + if(NOT DEFINED Python_EXECUTABLE) + if(WIN32) + set(_conda_python "${_conda_prefix}/python.exe") + else() + set(_conda_python "${_conda_prefix}/bin/python") + endif() + if(EXISTS "${_conda_python}") + set(Python_EXECUTABLE "${_conda_python}") + endif() + endif() + + list(PREPEND CMAKE_PREFIX_PATH + "${_conda_prefix}" + "${_conda_prefix}/lib/cmake" + "${_conda_prefix}/Library" + "${_conda_prefix}/Library/lib/cmake") +endif() + +# Imported CutCells targets may depend on OpenMP::OpenMP_CXX. +# Keep this optional and quiet so builds without OpenMP still proceed. +find_package(OpenMP QUIET COMPONENTS CXX) + +option(CUTCELLS_PYTHON_USE_LOCAL_CPP + "Build the bundled CutCells C++ library from source with the same compiler as the Python extension." + ON) + +function(_set_default_cutcells_dir) + if(DEFINED CutCells_DIR) + return() + endif() + + set(_candidates + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build/prefix/lib/cmake/cutcells" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-noomp/prefix/lib/cmake/cutcells" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-bench-release/prefix/lib/cmake/cutcells" + "${CMAKE_CURRENT_LIST_DIR}/../cpp/build-dir/prefix/lib/cmake/cutcells") + + set(_best_dir "") + set(_best_ts -1) + foreach(_dir IN LISTS _candidates) + if(EXISTS "${_dir}/CutCellsConfig.cmake") + set(_lib "${_dir}/../../libcutcells.dylib") + if(EXISTS "${_lib}") + file(TIMESTAMP "${_lib}" _ts "%s" UTC) + else() + file(TIMESTAMP "${_dir}/CutCellsConfig.cmake" _ts "%s" UTC) endif() - endforeach() + if(_ts GREATER _best_ts) + set(_best_ts "${_ts}") + set(_best_dir "${_dir}") + endif() + endif() + endforeach() + if(_best_dir) + set(CutCells_DIR "${_best_dir}" PARENT_SCOPE) + endif() +endfunction() + +if(CUTCELLS_PYTHON_USE_LOCAL_CPP + AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/../cpp/CMakeLists.txt" + AND NOT DEFINED CutCells_DIR + AND NOT TARGET CUTCELLS::cutcells + AND NOT TARGET cutcells) + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../cpp" + "${CMAKE_CURRENT_BINARY_DIR}/cutcells_cpp") +endif() + +if(TARGET cutcells) + add_library(CUTCELLS::cutcells ALIAS cutcells) + if(APPLE) + set_target_properties(cutcells PROPERTIES + INSTALL_NAME_DIR "@loader_path/../lib") + endif() +elseif(NOT TARGET CUTCELLS::cutcells) + _set_default_cutcells_dir() + find_package(CutCells REQUIRED CONFIG) +endif() + +if(CutCells_FOUND) + message(STATUS "Found CutCells in: ${CutCells_DIR}") +endif() + +set(_cutcells_lib "") +if(TARGET cutcells) + set(_cutcells_lib "$") +elseif(TARGET CUTCELLS::cutcells) + get_target_property(_cutcells_lib_rel CUTCELLS::cutcells IMPORTED_LOCATION_RELEASE) + get_target_property(_cutcells_lib_dbg CUTCELLS::cutcells IMPORTED_LOCATION_DEBUG) + get_target_property(_cutcells_lib_any CUTCELLS::cutcells IMPORTED_LOCATION) + if(_cutcells_lib_rel) + set(_cutcells_lib "${_cutcells_lib_rel}") + elseif(_cutcells_lib_dbg) + set(_cutcells_lib "${_cutcells_lib_dbg}") + elseif(_cutcells_lib_any) + set(_cutcells_lib "${_cutcells_lib_any}") endif() - find_package(CutCells REQUIRED) endif() -if(CUTCELLS_FOUND) - message(STATUS "Found CutCells at ${CUTCELLS_DIR}") +if(_cutcells_lib AND NOT TARGET cutcells) + message(STATUS "Using CutCells library: ${_cutcells_lib}") + find_program(NM_EXECUTABLE nm) + if(NM_EXECUTABLE) + execute_process( + COMMAND "${NM_EXECUTABLE}" -gU "${_cutcells_lib}" + OUTPUT_VARIABLE _nm_out + ERROR_QUIET + ) + string(FIND "${_nm_out}" "create_level_set_function" _has_create_level_set_function) + string(FIND "${_nm_out}" "write_level_set_vtu" _has_write_level_set_vtu) + if(_has_create_level_set_function EQUAL -1 OR _has_write_level_set_vtu EQUAL -1) + message(FATAL_ERROR + "The selected libcutcells is stale and missing required symbols " + "(create_level_set_function / write_level_set_vtu).\n" + "Selected library: ${_cutcells_lib}\n" + "Rebuild and reinstall the C++ core, then rerun the Python build.") + endif() + endif() endif() find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) +message(STATUS "Using Python executable: ${Python_EXECUTABLE}") +if(_conda_prefix) + message(STATUS "Using active conda prefix: ${_conda_prefix}") +endif() # Detect the installed nanobind package and import it into CMake execute_process( COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())" - OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR) + OUTPUT_VARIABLE NB_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") find_package(nanobind CONFIG REQUIRED) -# Create the binding library nanobind_add_module(_cutcellscpp cutcells/wrapper.cpp) target_link_libraries(_cutcellscpp PRIVATE CUTCELLS::cutcells) -# Add strict compiler flags include(CheckCXXCompilerFlag) -CHECK_CXX_COMPILER_FLAG("-Wall -Werror -Wextra -Wno-comment -pedantic" HAVE_PEDANTIC) - +check_cxx_compiler_flag("-Wall -Wextra -Wno-comment -pedantic" HAVE_PEDANTIC) if(HAVE_PEDANTIC) - target_compile_options(_cutcellscpp PRIVATE -Wall;-Wextra;-Wno-comment) + target_compile_options(_cutcellscpp PRIVATE -Wall -Wextra -Wno-comment) endif() -get_target_property(_location CUTCELLS::cutcells LOCATION) -get_filename_component(_cutcells_dir ${_location} DIRECTORY) - -# Ensure we load the CutCells library we just found/linked against. -# In conda environments it's common to also have a (possibly older) libcutcells.dylib -# available on the default RPATH; if that appears before our in-repo prefix, dyld -# will pick the wrong one at runtime. -set_target_properties(_cutcellscpp PROPERTIES - INSTALL_RPATH "${_cutcells_dir}" - BUILD_RPATH "${_cutcells_dir}" - BUILD_WITH_INSTALL_RPATH TRUE - INSTALL_RPATH_USE_LINK_PATH FALSE) - -if(APPLE AND DEFINED ENV{CONDA_PREFIX}) - # scikit-build/nanobind often injects ${CONDA_PREFIX}/lib as an LC_RPATH ahead of ours. - # If that environment contains another libcutcells.dylib, dyld will load the wrong one. - # Reorder RPATHs post-link so the in-repo CutCells lib directory is searched first. - set(_conda_lib "$ENV{CONDA_PREFIX}/lib") - # If CutCells itself is installed into the conda env lib dir, then _cutcells_dir == _conda_lib - # and reordering is unnecessary (and would attempt to add the same RPATH twice). - if (NOT "${_cutcells_dir}" STREQUAL "${_conda_lib}") - add_custom_command(TARGET _cutcellscpp POST_BUILD - COMMAND install_name_tool -delete_rpath "${_conda_lib}" $ || true - COMMAND install_name_tool -delete_rpath "${_cutcells_dir}" $ || true - COMMAND install_name_tool -add_rpath "${_cutcells_dir}" $ - COMMAND install_name_tool -add_rpath "${_conda_lib}" $ - VERBATIM) +set(_build_rpath "$") +if(TARGET cutcells) + if(APPLE) + set(_install_rpath "@loader_path/../lib") + else() + set(_install_rpath "$ORIGIN/../lib") endif() +else() + set(_install_rpath "$") endif() +set_target_properties(_cutcellscpp PROPERTIES + BUILD_RPATH "${_build_rpath}" + INSTALL_RPATH "${_install_rpath}" + BUILD_WITH_INSTALL_RPATH FALSE + INSTALL_RPATH_USE_LINK_PATH FALSE +) + install(TARGETS _cutcellscpp LIBRARY DESTINATION cutcells) diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index fd7f2a4..e801ef7 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -17,9 +17,15 @@ MeshView, MeshView_float32, MeshView_float64, + LevelSetMeshData, + LevelSetMeshData_float32, + LevelSetMeshData_float64, LevelSetFunction, LevelSetFunction_float32, LevelSetFunction_float64, + create_level_set_mesh_data, + create_level_set_function, + interpolate_level_set, cut, higher_order_cut, create_cut_mesh, @@ -32,4 +38,13 @@ make_quadrature, runtime_quadrature, physical_points, + write_level_set_vtu, +) + +from .mesh_utils import ( + mesh_from_pyvista, + rectangle_triangle_mesh, + rectangle_quad_mesh, + box_tetrahedron_mesh, + box_hex_mesh, ) diff --git a/python/cutcells/mesh_utils.py b/python/cutcells/mesh_utils.py new file mode 100644 index 0000000..c7b508f --- /dev/null +++ b/python/cutcells/mesh_utils.py @@ -0,0 +1,162 @@ +"""Structured mesh generation and pyvista ↔ MeshView conversion utilities.""" + +import numpy as np + +try: + import pyvista as pv +except ImportError: + pv = None + + +# --------------------------------------------------------------------------- +# VTK cell-type → topological dimension lookup +# --------------------------------------------------------------------------- + +_VTK_TDIM = { + 3: 1, # VTK_LINE + 5: 2, # VTK_TRIANGLE + 9: 2, # VTK_QUAD + 10: 3, # VTK_TETRA + 12: 3, # VTK_HEXAHEDRON + 13: 3, # VTK_WEDGE (prism) + 14: 3, # VTK_PYRAMID +} + + +# --------------------------------------------------------------------------- +# pyvista ↔ MeshView +# --------------------------------------------------------------------------- + + +def mesh_from_pyvista(grid, *, tdim=None): + """Create a cutcells.MeshView from a pyvista UnstructuredGrid. + + Parameters + ---------- + grid : pyvista.UnstructuredGrid + Source mesh. + tdim : int, optional + Topological dimension. If None, inferred from VTK cell types. + + Returns + ------- + cutcells.MeshView + """ + from . import MeshView + + if pv is None: + raise ImportError("pyvista is required for mesh_from_pyvista") + + coordinates = np.asarray(grid.points, dtype=np.float64) + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offsets = np.asarray(grid.offset, dtype=np.int32) + cell_types_np = np.asarray(grid.celltypes, dtype=np.int32) + + if tdim is None: + first_type = int(cell_types_np[0]) + tdim = _VTK_TDIM.get(first_type) + if tdim is None: + raise ValueError( + f"Cannot infer tdim from VTK cell type {first_type}. " + "Pass tdim explicitly." + ) + + return MeshView( + coordinates=coordinates, + connectivity=connectivity, + offsets=offsets, + cell_types=cell_types_np, + tdim=tdim, + ) + + +# --------------------------------------------------------------------------- +# 2D structured meshes +# --------------------------------------------------------------------------- + + +def rectangle_triangle_mesh(x0, y0, x1, y1, nx, ny): + """Structured triangular mesh on [x0,x1] × [y0,y1]. + + Creates a regular grid of nx × ny points and triangulates it with + Delaunay. Returns a pyvista UnstructuredGrid. + """ + if pv is None: + raise ImportError("pyvista is required") + x = np.linspace(x0, x1, num=nx) + y = np.linspace(y0, y1, num=ny) + xx, yy, zz = np.meshgrid(x, y, [0.0]) + points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + poly = pv.PolyData(points).delaunay_2d() + return pv.UnstructuredGrid(poly) + + +def rectangle_quad_mesh(x0, y0, x1, y1, nx, ny): + """Structured quadrilateral mesh on [x0,x1] × [y0,y1]. + + Returns a pyvista UnstructuredGrid with (nx-1)*(ny-1) quads. + """ + if pv is None: + raise ImportError("pyvista is required") + x = np.linspace(x0, x1, num=nx) + y = np.linspace(y0, y1, num=ny) + xx, yy = np.meshgrid(x, y) + points = np.c_[xx.ravel(), yy.ravel(), np.zeros(nx * ny)] + + cells = [] + celltypes = [] + for j in range(ny - 1): + for i in range(nx - 1): + v0 = j * nx + i + v1 = v0 + 1 + v2 = v1 + nx + v3 = v0 + nx + cells.extend([4, v0, v1, v2, v3]) + celltypes.append(9) # VTK_QUAD + + return pv.UnstructuredGrid( + np.array(cells, dtype=np.int64), + np.array(celltypes, dtype=np.uint8), + points, + ) + + +# --------------------------------------------------------------------------- +# 3D structured meshes +# --------------------------------------------------------------------------- + + +def box_tetrahedron_mesh(x0, y0, z0, x1, y1, z1, nx, ny, nz): + """Structured tetrahedral mesh on [x0,x1] × [y0,y1] × [z0,z1]. + + Creates a regular grid and uses Delaunay tetrahedralization. + Returns a pyvista UnstructuredGrid. + """ + if pv is None: + raise ImportError("pyvista is required") + x = np.linspace(x0, x1, num=nx) + y = np.linspace(y0, y1, num=ny) + z = np.linspace(z0, z1, num=nz) + xx, yy, zz = np.meshgrid(x, y, z) + points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] + poly = pv.PolyData(points).delaunay_3d() + return pv.UnstructuredGrid(poly) + + +def box_hex_mesh(x0, y0, z0, x1, y1, z1, nx, ny, nz): + """Structured hexahedral mesh on [x0,x1] × [y0,y1] × [z0,z1]. + + Returns a pyvista UnstructuredGrid with (nx-1)*(ny-1)*(nz-1) hexahedra. + """ + if pv is None: + raise ImportError("pyvista is required") + sg = pv.ImageData( + dimensions=(nx, ny, nz), + origin=(x0, y0, z0), + spacing=( + (x1 - x0) / (nx - 1), + (y1 - y0) / (ny - 1), + (z1 - z0) / (nz - 1), + ), + ) + return sg.cast_to_unstructured_grid() diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 1bba63f..358d4f4 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -159,6 +159,7 @@ template void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) { using MeshViewT = cutcells::MeshView; + using LevelSetMeshDataT = cutcells::LevelSetMeshData; using LevelSetT = cutcells::LevelSetFunction; const std::string mesh_name = "MeshView_" + suffix; @@ -244,6 +245,150 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) }, nb::rv_policy::reference_internal); + const std::string ls_mesh_name = "LevelSetMeshData_" + suffix; + nb::class_(m, ls_mesh_name.c_str(), "Discrete level-set mesh data") + .def(nb::init<>()) + .def_prop_ro("gdim", [](const LevelSetMeshDataT& self) { return self.gdim; }) + .def_prop_ro("tdim", [](const LevelSetMeshDataT& self) { return self.tdim; }) + .def_prop_ro("degree", [](const LevelSetMeshDataT& self) { return self.degree; }) + .def("num_dofs", &LevelSetMeshDataT::num_dofs) + .def("num_cells", &LevelSetMeshDataT::num_cells) + .def("cell_num_dofs", &LevelSetMeshDataT::cell_num_dofs, nb::arg("cell_id")) + .def_prop_ro( + "dof_coordinates", + [](const LevelSetMeshDataT& self) + { + const std::size_t gdim = static_cast(self.gdim); + if (gdim == 0) + return nb::ndarray(nullptr, {0, 0}, nb::handle()); + const std::size_t n = self.dof_coordinates.size() / gdim; + return nb::ndarray( + self.dof_coordinates.data(), + {n, gdim}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_dofs", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.cell_dofs.data(), + {self.cell_dofs.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_offsets", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.cell_offsets.data(), + {self.cell_offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_types", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.cell_types.data(), + {self.cell_types.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "dof_parent_dim", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.dof_parent_dim.data(), + {self.dof_parent_dim.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "dof_parent_id", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.dof_parent_id.data(), + {self.dof_parent_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "dof_parent_param", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.dof_parent_param.data(), + {self.dof_parent_param.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "dof_parent_param_offset", + [](const LevelSetMeshDataT& self) + { + return nb::ndarray( + self.dof_parent_param_offset.data(), + {self.dof_parent_param_offset.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal); + + m.def( + "create_level_set_mesh_data", + [](const MeshViewT& mesh, int degree, T merge_tol) + { + nb::gil_scoped_release release; + return cutcells::create_level_set_mesh_data(mesh, degree, merge_tol); + }, + nb::arg("mesh"), + nb::arg("degree"), + nb::arg("merge_tol") = T(-1)); + + m.def( + "create_level_set_mesh_data", + [](const ndarray2& dof_coordinates, + const ndarray1& cell_dofs, + const ndarray1& cell_offsets, + int degree, + int tdim, + nb::object cell_types_obj) + { + std::optional> cell_types; + if (!cell_types_obj.is_none()) + cell_types = nb::cast>(cell_types_obj); + + std::span cell_types_span; + if (cell_types.has_value()) + { + cell_types_span = std::span( + cell_types->data(), static_cast(cell_types->size())); + } + + return cutcells::create_level_set_mesh_data( + static_cast(dof_coordinates.shape(1)), + tdim, + degree, + std::span(dof_coordinates.data(), + static_cast(dof_coordinates.size())), + std::span(cell_dofs.data(), + static_cast(cell_dofs.size())), + std::span(cell_offsets.data(), + static_cast(cell_offsets.size())), + cell_types_span); + }, + nb::arg("dof_coordinates"), + nb::arg("cell_dofs"), + nb::arg("cell_offsets"), + nb::arg("degree"), + nb::arg("tdim"), + nb::arg("cell_types") = nb::none()); + const std::string ls_name = "LevelSetFunction_" + suffix; nb::class_(m, ls_name.c_str(), "Level-set function") .def( @@ -335,6 +480,8 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) .def("has_value", &LevelSetT::has_value) .def("has_gradient", &LevelSetT::has_gradient) .def("has_nodal_values", &LevelSetT::has_nodal_values) + .def("has_mesh_data", &LevelSetT::has_mesh_data) + .def("has_dof_values", &LevelSetT::has_dof_values) .def( "value", [](const LevelSetT& self, const ndarray1& x, int cell_id) @@ -369,8 +516,82 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) {self.nodal_values.size()}, nb::handle()); }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "dof_values", + [](const LevelSetT& self) + { + if (self.dof_values.empty()) + return nb::ndarray(nullptr, {0}, nb::handle()); + return nb::ndarray( + self.dof_values.data(), + {self.dof_values.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "mesh_data", + [](const LevelSetT& self) -> nb::object + { + if (!self.mesh_data) + return nb::none(); + return nb::cast(self.mesh_data); + }, nb::rv_policy::reference_internal); + m.def( + "create_level_set_function", + [](const LevelSetMeshDataT& mesh_data, const ndarray1& dof_values) + { + auto mesh_data_ptr = std::make_shared(mesh_data); + return cutcells::create_level_set_function( + std::move(mesh_data_ptr), + std::span( + dof_values.data(), + static_cast(dof_values.size()))); + }, + nb::arg("mesh_data"), + nb::arg("dof_values"), + "Create a polynomial LevelSetFunction from mesh_data and global dof values."); + + m.def( + "interpolate_level_set", + [](const MeshViewT& mesh, nb::callable phi, int degree) + { + LevelSetMeshDataT mesh_data; + { + nb::gil_scoped_release release; + mesh_data = cutcells::create_level_set_mesh_data(mesh, degree, T(-1)); + } + + const std::size_t num_dofs = static_cast(mesh_data.num_dofs()); + const std::size_t gdim = static_cast(mesh_data.gdim); + nb::ndarray x( + mesh_data.dof_coordinates.data(), + {num_dofs, gdim}, + nb::handle()); + + // Intentional single batched callback invocation. + nb::object values_obj = phi(x); + auto values = nb::cast>(values_obj); + if (static_cast(values.size()) != num_dofs) + { + throw std::runtime_error( + "interpolate_level_set: callback must return a 1D array with length num_dofs"); + } + + auto mesh_data_ptr = std::make_shared(std::move(mesh_data)); + return cutcells::create_level_set_function( + std::move(mesh_data_ptr), + std::span( + values.data(), + static_cast(values.size()))); + }, + nb::arg("mesh"), + nb::arg("phi"), + nb::arg("degree"), + "Interpolate a batched callable phi(X) at higher-order level-set dof coordinates."); + // ---- cut_mesh_view ---- // Cuts a MeshView using a LevelSetFunction, returning a CutMesh. // Nodal level-set values are taken from ls.nodal_values if available, @@ -428,7 +649,19 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) // Simple aliases for Python if constexpr (std::is_same_v) { + m.def( + "write_level_set_vtu", + [](const std::string& filename, const LevelSetT& ls, const std::string& field_name) + { + nb::gil_scoped_release release; + io::write_level_set_vtu(filename, ls, field_name); + }, + nb::arg("filename"), + nb::arg("level_set"), + nb::arg("field_name") = "phi"); + m.attr("MeshView") = m.attr(mesh_name.c_str()); + m.attr("LevelSetMeshData") = m.attr(ls_mesh_name.c_str()); m.attr("LevelSetFunction") = m.attr(ls_name.c_str()); } } diff --git a/python/demo/demo_level_set_ho_curved_vtk.py b/python/demo/demo_level_set_ho_curved_vtk.py new file mode 100644 index 0000000..5b16540 --- /dev/null +++ b/python/demo/demo_level_set_ho_curved_vtk.py @@ -0,0 +1,213 @@ +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT + +import argparse +from pathlib import Path + +import numpy as np + +import cutcells + + +def _parse_degrees(arg: str) -> list[int]: + if arg == "all": + return [2, 3, 4] + return [int(arg)] + + +def _triangle_basix_nodes(degree: int) -> np.ndarray: + points: list[tuple[float, float, float]] = [ + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + ] + + for k in range(1, degree): + t = k / degree + points.append((1.0 - t, t, 0.0)) + for k in range(1, degree): + t = k / degree + points.append((0.0, t, 0.0)) + for k in range(1, degree): + t = k / degree + points.append((t, 0.0, 0.0)) + + for j in range(1, degree - 1): + for i in range(1, degree - j): + points.append((i / degree, j / degree, 0.0)) + + return np.asarray(points, dtype=np.float64) + + +def _tetra_basix_nodes(degree: int) -> np.ndarray: + points: list[tuple[float, float, float]] = [ + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + (0.0, 0.0, 1.0), + ] + + for k in range(1, degree): + t = k / degree + points.append((0.0, 1.0 - t, t)) + for k in range(1, degree): + t = k / degree + points.append((1.0 - t, 0.0, t)) + for k in range(1, degree): + t = k / degree + points.append((1.0 - t, t, 0.0)) + for k in range(1, degree): + t = k / degree + points.append((0.0, 0.0, t)) + for k in range(1, degree): + t = k / degree + points.append((0.0, t, 0.0)) + for k in range(1, degree): + t = k / degree + points.append((t, 0.0, 0.0)) + + faces = ( + ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), + ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)), + ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)), + ) + for x0, x1, x2 in faces: + for j in range(1, degree - 1): + for i in range(1, degree - j): + u = i / degree + v = j / degree + w0 = 1.0 - u - v + points.append( + ( + w0 * x0[0] + u * x1[0] + v * x2[0], + w0 * x0[1] + u * x1[1] + v * x2[1], + w0 * x0[2] + u * x1[2] + v * x2[2], + ) + ) + + for k in range(1, degree - 2): + for j in range(1, degree - k - 1): + for i in range(1, degree - j - k): + points.append((i / degree, j / degree, k / degree)) + + return np.asarray(points, dtype=np.float64) + + +def _curved_triangle_nodes(degree: int) -> np.ndarray: + X = _triangle_basix_nodes(degree).copy() + x = X[:, 0] + y = X[:, 1] + l0 = 1.0 - x - y + l1 = x + l2 = y + X[:, 0] = x + 0.18 * l0 * l2 - 0.10 * l1 * l2 + X[:, 1] = y + 0.24 * l0 * l1 + 0.08 * l1 * l2 + return X + + +def _curved_tetra_nodes(degree: int) -> np.ndarray: + X = _tetra_basix_nodes(degree).copy() + x = X[:, 0] + y = X[:, 1] + z = X[:, 2] + l0 = 1.0 - x - y - z + l1 = x + l2 = y + l3 = z + X[:, 0] = x + 0.10 * l0 * l2 - 0.05 * l1 * l3 + X[:, 1] = y + 0.10 * l0 * l1 + 0.05 * l2 * l3 + X[:, 2] = z + 0.18 * l0 * l1 + 0.10 * l0 * l2 - 0.08 * l1 * l2 + return X + + +def _phi(X: np.ndarray) -> np.ndarray: + return np.sqrt((X[:, 0] - 0.35) ** 2 + (X[:, 1] - 0.30) ** 2 + (X[:, 2] - 0.20) ** 2) - 0.28 + + +def _make_level_set(cell_family: str, degree: int) -> cutcells.LevelSetFunction: + if cell_family == "triangle": + dof_coordinates = _curved_triangle_nodes(degree) + vtk_type = np.array([5], dtype=np.int32) + tdim = 2 + expected_local_dofs = (degree + 1) * (degree + 2) // 2 + elif cell_family == "tetra": + dof_coordinates = _curved_tetra_nodes(degree) + vtk_type = np.array([10], dtype=np.int32) + tdim = 3 + expected_local_dofs = (degree + 1) * (degree + 2) * (degree + 3) // 6 + else: + raise ValueError(f"Unsupported cell_family={cell_family}") + + if dof_coordinates.shape[0] != expected_local_dofs: + raise RuntimeError( + f"{cell_family} p{degree}: generated {dof_coordinates.shape[0]} dofs, " + f"expected {expected_local_dofs}" + ) + + cell_dofs = np.arange(expected_local_dofs, dtype=np.int32) + cell_offsets = np.array([0, expected_local_dofs], dtype=np.int32) + mesh_data = cutcells.create_level_set_mesh_data( + dof_coordinates=dof_coordinates, + cell_dofs=cell_dofs, + cell_offsets=cell_offsets, + degree=degree, + tdim=tdim, + cell_types=vtk_type, + ) + dof_values = _phi(dof_coordinates) + return cutcells.create_level_set_function(mesh_data, dof_values) + + +def _run_case(cell_family: str, degree: int, output_dir: Path) -> None: + ls = _make_level_set(cell_family, degree) + output_path = output_dir / f"curved_{cell_family}_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output_path), ls, field_name="phi") + + print(f"[{cell_family} p{degree}] cell_dofs={ls.mesh_data.cell_num_dofs(0)}") + print(f"[{cell_family} p{degree}] num_dofs={ls.mesh_data.num_dofs()}") + print(f"[{cell_family} p{degree}] wrote {output_path}") + print( + f"[{cell_family} p{degree}] ParaView: set 'Nonlinear Subdivision Level' > 0 " + "to visualize the curved high-order cell." + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Write single curved high-order Lagrange cells for ParaView inspection." + ) + parser.add_argument( + "--cell-family", + choices=["triangle", "tetra", "both"], + default="both", + help="Cell family to write.", + ) + parser.add_argument( + "--degree", + choices=["2", "3", "4", "all"], + default="all", + help="Polynomial degree.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("demo_level_set_ho_curved_vtu"), + help="Output directory for VTU files.", + ) + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + degrees = _parse_degrees(args.degree) + families = ["triangle", "tetra"] if args.cell_family == "both" else [args.cell_family] + + for family in families: + for degree in degrees: + _run_case(family, degree, args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/python/demo/demo_level_set_ho_vtk.py b/python/demo/demo_level_set_ho_vtk.py new file mode 100644 index 0000000..52baf38 --- /dev/null +++ b/python/demo/demo_level_set_ho_vtk.py @@ -0,0 +1,97 @@ +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT + +import argparse +from pathlib import Path + +import numpy as np + +import cutcells +from cutcells import box_tetrahedron_mesh, mesh_from_pyvista, rectangle_triangle_mesh + + +def _parse_degrees(arg: str) -> list[int]: + if arg == "all": + return [2, 3, 4] + return [int(arg)] + + +def _phi(X: np.ndarray) -> np.ndarray: + x = X[:, 0] + y = X[:, 1] + z = X[:, 2] if X.shape[1] > 2 else 0.0 + return np.sqrt((x - 0.15) ** 2 + (y + 0.1) ** 2 + (z - 0.05) ** 2) - 0.55 + + +def _run_case(mesh, family: str, degree: int, output_dir: Path) -> None: + callback_info: dict[str, tuple[int, int]] = {} + + def phi(X: np.ndarray) -> np.ndarray: + callback_info["shape"] = tuple(X.shape) + return _phi(X) + + ls = cutcells.interpolate_level_set(mesh, phi, degree=degree) + output_path = output_dir / f"{family}_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output_path), ls, field_name="phi") + + print(f"[{family} p{degree}] cells={mesh.num_cells()}") + print(f"[{family} p{degree}] dofs={ls.mesh_data.num_dofs()}") + print(f"[{family} p{degree}] callback batch shape={callback_info.get('shape')}") + print(f"[{family} p{degree}] wrote {output_path}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Interpolate a higher-order level set and write Lagrange VTU files." + ) + parser.add_argument( + "--cell-family", + choices=["triangle", "tetra", "both"], + default="both", + help="Cell family to run.", + ) + parser.add_argument( + "--degree", + choices=["2", "3", "4", "all"], + default="all", + help="Polynomial degree.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("demo_level_set_ho_vtu"), + help="Output directory for VTU files.", + ) + parser.add_argument( + "--n", + type=int, + default=8, + help="Structured mesh resolution parameter.", + ) + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + degrees = _parse_degrees(args.degree) + families = ( + ["triangle", "tetra"] if args.cell_family == "both" else [args.cell_family] + ) + + for family in families: + if family == "triangle": + grid = rectangle_triangle_mesh(-1.0, -1.0, 1.0, 1.0, args.n, args.n) + mesh = mesh_from_pyvista(grid, tdim=2) + else: + grid = box_tetrahedron_mesh( + -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, args.n, args.n, args.n + ) + mesh = mesh_from_pyvista(grid, tdim=3) + + for degree in degrees: + _run_case(mesh, family, degree, args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/python/demo/demo_meshview.py b/python/demo/demo_meshview.py new file mode 100644 index 0000000..806b9e9 --- /dev/null +++ b/python/demo/demo_meshview.py @@ -0,0 +1,290 @@ +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: MeshView + LevelSetFunction Python wrappers +================================================== + +Demonstrates: + * Building a ``cutcells.MeshView`` from a pyvista ``UnstructuredGrid``. + * Building a ``cutcells.LevelSetFunction`` from a plain Python callable. + * Calling ``cutcells.cut_mesh_view`` to obtain cut meshes for + – the interface (phi = 0) + – the inside (phi < 0) + * Visualising the results with pyvista. + +The background mesh is a 2-D Delaunay triangulation of a square [-1, 1]^2. +The level-set is a circle of radius 0.7 centred at the origin: + phi(x) = sqrt(x0^2 + x1^2) - 0.7 +""" + +import argparse + +import numpy as np + +try: + import pyvista as pv +except ImportError as exc: + raise SystemExit( + "pyvista is required for this demo. " + "Install with: python -m pip install pyvista\n" + f"Import error: {exc}" + ) + +import cutcells +from cutcells import ( + mesh_from_pyvista, + rectangle_triangle_mesh, +) + + +# --------------------------------------------------------------------------- +# Level-set definition +# --------------------------------------------------------------------------- + +RADIUS = 0.7 +CENTER = np.array([0.0, 0.0, 0.0]) + + +def circle_phi(x: np.ndarray) -> float: + """Signed-distance to a circle of radius RADIUS centred at CENTER. + + ``x`` is a 1-D NumPy array of length 3 (pyvista always stores 3-D coords). + Returns a float: negative inside the circle, positive outside. + """ + return float(np.sqrt(np.sum((x - CENTER) ** 2)) - RADIUS) + + +# --------------------------------------------------------------------------- +# Helper: build MeshView from a pyvista UnstructuredGrid +# --------------------------------------------------------------------------- +# +# NOTE: Using mesh_from_pyvista from cutcells.mesh_utils module + + +# --------------------------------------------------------------------------- +# Mesh generation +# --------------------------------------------------------------------------- +# +# NOTE: Using rectangle_triangle_mesh from cutcells.mesh_utils module + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Demo: cut_mesh_view with MeshView and LevelSetFunction" + ) + parser.add_argument( + "--n", + type=int, + default=30, + help="Grid points per axis (default: 30)", + ) + parser.add_argument( + "--no-plot", + action="store_true", + help="Skip interactive visualisation (useful for CI / off-screen runs)", + ) + args = parser.parse_args() + + N = int(args.n) + + # ------------------------------------------------------------------ + # 1. Build the background pyvista grid and wrap it in a MeshView + # ------------------------------------------------------------------ + print(f"Creating {N}×{N} Delaunay triangle mesh …") + grid = rectangle_triangle_mesh(-1.0, -1.0, 1.0, 1.0, N, N) + print(f" nodes: {grid.n_points}, cells: {grid.n_cells}") + + mesh_view = mesh_from_pyvista(grid, tdim=2) + print( + f" MeshView gdim={mesh_view.gdim}, tdim={mesh_view.tdim}, " + f"num_nodes={mesh_view.num_nodes()}, num_cells={mesh_view.num_cells()}" + ) + + # ------------------------------------------------------------------ + # 2. Build a LevelSetFunction from a Python callable + # ------------------------------------------------------------------ + # The callable receives a length-3 NumPy array (x) and may optionally + # accept a second argument cell_id (ignored here). + level_set = cutcells.LevelSetFunction( + value=circle_phi, + gdim=mesh_view.gdim, # 3 (pyvista always stores 3-D coords) + ) + print( + f"\nLevelSetFunction has_value={level_set.has_value()}, " + f"has_nodal_values={level_set.has_nodal_values()}" + ) + + # ------------------------------------------------------------------ + # 3. Cut the mesh – interface (phi = 0) + # ------------------------------------------------------------------ + print("\nCutting mesh for phi=0 (interface) …") + cut_interface = cutcells.cut_mesh_view( + mesh_view, level_set, "phi=0", triangulate=True + ) + print( + f" Interface cut mesh: {len(cut_interface.vtk_types)} cells, " + f"{len(cut_interface.vertex_coords)} vertices" + ) + + # ------------------------------------------------------------------ + # 4. Cut the mesh – interior (phi < 0) + # ------------------------------------------------------------------ + print("Cutting mesh for phi<0 (inside) …") + cut_inside = cutcells.cut_mesh_view(mesh_view, level_set, "phi<0", triangulate=True) + print( + f" Inside cut mesh: {len(cut_inside.vtk_types)} cells, " + f"{len(cut_inside.vertex_coords)} vertices" + ) + + # ------------------------------------------------------------------ + # 5. (Optional) also build nodal values and use LevelSetFunction + # with nodal_values instead of value callable – same result + # ------------------------------------------------------------------ + print("\nBuilding nodal level-set values manually …") + ls_nodal = np.array([circle_phi(p) for p in grid.points], dtype=np.float64) + level_set_nodal = cutcells.LevelSetFunction( + nodal_values=ls_nodal, + gdim=mesh_view.gdim, + ) + print( + f" LevelSetFunction (nodal) has_nodal_values={level_set_nodal.has_nodal_values()}" + ) + + cut_inside_nodal = cutcells.cut_mesh_view( + mesh_view, level_set_nodal, "phi<0", triangulate=True + ) + assert len(cut_inside_nodal.vtk_types) == len(cut_inside.vtk_types), ( + "Nodal and callable level-set should give identical cut meshes" + ) + print(" Nodal and callable results agree ✓") + + # ------------------------------------------------------------------ + # 6. Runtime Quadrature and Physical Mapping + # ------------------------------------------------------------------ + print("\nComputing runtime quadrature for phi<0 …") + order = 2 + # Use the flat arrays from the grid + points_flat = np.asarray(grid.points, dtype=np.float64).ravel() + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offsets = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) + + # Calculate quadrature rules in reference space for phi<0 + q_rules_inside = cutcells.runtime_quadrature( + ls_nodal, + points_flat, + connectivity, + offsets, + celltypes, + "phi<0", + triangulate=True, + order=order, + ) + print(f" Inside: Generated {len(q_rules_inside.weights)} total quadrature points.") + + # Calculate quadrature rules in reference space for phi=0 + print("Computing runtime quadrature for phi=0 …") + q_rules_interface = cutcells.runtime_quadrature( + ls_nodal, + points_flat, + connectivity, + offsets, + celltypes, + "phi=0", + triangulate=True, + order=order, + ) + print( + f" Interface: Generated {len(q_rules_interface.weights)} total quadrature points." + ) + + # Map to physical space + print(" Mapping quadrature points to physical space …") + q_points_inside_phys = cutcells.physical_points( + q_rules_inside, points_flat, connectivity, offsets, celltypes + ).reshape(-1, 3) + + q_points_interface_phys = cutcells.physical_points( + q_rules_interface, points_flat, connectivity, offsets, celltypes + ).reshape(-1, 3) + + # ------------------------------------------------------------------ + # 7. Visualise + # ------------------------------------------------------------------ + if args.no_plot: + print("\n--no-plot specified, skipping visualisation.") + return + + # -- wrap cut results as pyvista grids -- + pv_interface = pv.UnstructuredGrid( + cut_interface.cells, + cut_interface.vtk_types, + np.asarray(cut_interface.vertex_coords, dtype=np.float64), + ) + pv_inside = pv.UnstructuredGrid( + cut_inside.cells, + cut_inside.vtk_types, + np.asarray(cut_inside.vertex_coords, dtype=np.float64), + ) + + # -- inside background cells (entirely inside the circle) -- + points_flat = np.asarray(grid.points, dtype=np.float64).ravel() + connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) + offsets = np.asarray(grid.offset, dtype=np.int32) + celltypes = np.asarray(grid.celltypes, dtype=np.int32) + inside_ids = cutcells.locate_cells( + ls_nodal, points_flat, connectivity, offsets, celltypes, "phi<0" + ) + pv_inside_bg = grid.extract_cells(inside_ids) + + # -- combined inside view: background inside cells + cut cells -- + pv_combined = pv_inside_bg.merge(pv_inside) + + # -- wrap quadrature points as pyvista points -- + pv_q_points_inside = pv.PolyData(q_points_inside_phys) + pv_q_points_interface = pv.PolyData(q_points_interface_phys) + + # Plot + pl = pv.Plotter(shape=(1, 2), title="CutCells – MeshView + LevelSetFunction demo") + + pl.subplot(0, 0) + pl.add_title("Interface (phi = 0) + Quad Points") + pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) + pl.add_mesh(pv_interface, show_edges=True, color="red") + pl.add_mesh( + pv_q_points_interface, + color="yellow", + point_size=10, + render_points_as_spheres=True, + label=f"Interface Points ({len(q_rules_interface.weights)})", + ) + pl.add_legend() + pl.view_xy() + + pl.subplot(0, 1) + pl.add_title("Inside domain (phi < 0) + Quad Points") + pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) + pl.add_mesh(pv_combined, show_edges=True, color="steelblue", opacity=0.8) + pl.add_mesh( + pv_q_points_inside, + color="orange", + point_size=10, + render_points_as_spheres=True, + label=f"Inside Points ({len(q_rules_inside.weights)})", + ) + pl.add_legend() + pl.view_xy() + + pl.show() + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_level_set_interpolate.py b/python/tests/test_level_set_interpolate.py new file mode 100644 index 0000000..d5f7609 --- /dev/null +++ b/python/tests/test_level_set_interpolate.py @@ -0,0 +1,123 @@ +import numpy as np +import pytest + +import cutcells + + +def _make_mesh(cell_family: str): + if cell_family == "triangle": + coordinates = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2], dtype=np.int32) + offsets = np.array([0, 3], dtype=np.int32) + cell_types = np.array([5], dtype=np.int32) # VTK_TRIANGLE + tdim = 2 + elif cell_family == "tetra": + coordinates = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3], dtype=np.int32) + offsets = np.array([0, 4], dtype=np.int32) + cell_types = np.array([10], dtype=np.int32) # VTK_TETRA + tdim = 3 + else: + raise ValueError(f"Unsupported cell_family={cell_family}") + + return cutcells.MeshView( + coordinates=coordinates, + connectivity=connectivity, + offsets=offsets, + cell_types=cell_types, + tdim=tdim, + ) + + +@pytest.mark.parametrize("cell_family", ["triangle", "tetra"]) +@pytest.mark.parametrize("degree", [2, 3, 4]) +def test_interpolate_level_set_calls_callback_once_and_sets_discrete_data(cell_family, degree): + mesh = _make_mesh(cell_family) + callback_count = 0 + callback_shapes = [] + + def phi(X): + nonlocal callback_count + callback_count += 1 + callback_shapes.append(X.shape) + return X[:, 0] + 2.0 * X[:, 1] - 0.5 * X[:, 2] + + ls = cutcells.interpolate_level_set(mesh, phi, degree=degree) + + assert callback_count == 1 + assert callback_shapes[0] == (ls.mesh_data.num_dofs(), mesh.gdim) + + assert ls.has_mesh_data() is True + assert ls.has_dof_values() is True + assert ls.mesh_data.degree == degree + assert len(ls.dof_values) == ls.mesh_data.num_dofs() + + coords = np.asarray(ls.mesh_data.dof_coordinates) + np.testing.assert_allclose(np.asarray(ls.dof_values), phi(coords)) + + +def test_create_level_set_function_from_mesh_data_and_values(): + dof_coordinates = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.5, 0.5, 0.0], + [0.0, 0.5, 0.0], + [0.5, 0.0, 0.0], + ], + dtype=np.float64, + ) + cell_dofs = np.array([0, 1, 2, 3, 4, 5], dtype=np.int32) + cell_offsets = np.array([0, 6], dtype=np.int32) + cell_types = np.array([5], dtype=np.int32) + dof_values = np.linspace(-1.0, 1.0, 6, dtype=np.float64) + + mesh_data = cutcells.create_level_set_mesh_data( + dof_coordinates=dof_coordinates, + cell_dofs=cell_dofs, + cell_offsets=cell_offsets, + degree=2, + tdim=2, + cell_types=cell_types, + ) + ls = cutcells.create_level_set_function(mesh_data, dof_values) + + assert ls.has_mesh_data() is True + assert ls.has_dof_values() is True + np.testing.assert_allclose(np.asarray(ls.dof_values), dof_values) + + +def test_legacy_level_set_constructor_paths_still_work(): + ls_callable = cutcells.LevelSetFunction( + value=lambda x: float(x[0] - 0.25), + gdim=3, + ) + assert ls_callable.has_value() is True + assert ls_callable.has_nodal_values() is False + assert ls_callable.has_mesh_data() is False + assert ls_callable.has_dof_values() is False + value = ls_callable.value(np.array([0.5, 0.0, 0.0], dtype=np.float64)) + assert value == pytest.approx(0.25) + + nodal = np.array([0.1, -0.2, 0.3], dtype=np.float64) + ls_nodal = cutcells.LevelSetFunction(nodal_values=nodal, gdim=3) + assert ls_nodal.has_nodal_values() is True + assert ls_nodal.value_at_node(1) == pytest.approx(-0.2) + assert ls_nodal.has_mesh_data() is False + assert ls_nodal.has_dof_values() is False diff --git a/python/tests/test_level_set_mesh_data.py b/python/tests/test_level_set_mesh_data.py new file mode 100644 index 0000000..09a3ef8 --- /dev/null +++ b/python/tests/test_level_set_mesh_data.py @@ -0,0 +1,93 @@ +import numpy as np + +import cutcells + + +def test_create_level_set_mesh_data_triangle_p2_shared_dofs(): + coords = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0], + [1.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 1, 3, 2], dtype=np.int32) + offsets = np.array([0, 3, 6], dtype=np.int32) + cell_types = np.array([5, 5], dtype=np.int32) # VTK_TRIANGLE + + mesh = cutcells.MeshView(coords, connectivity, offsets, cell_types, 2) + data = cutcells.create_level_set_mesh_data(mesh, 2) + + assert data.gdim == 2 + assert data.tdim == 2 + assert data.degree == 2 + assert data.num_cells() == 2 + assert data.num_dofs() == 9 + + cell_offsets = np.asarray(data.cell_offsets) + cell_dofs = np.asarray(data.cell_dofs) + dof_coords = np.asarray(data.dof_coordinates) + dof_parent_dim = np.asarray(data.dof_parent_dim) + dof_parent_id = np.asarray(data.dof_parent_id) + dof_parent_param = np.asarray(data.dof_parent_param) + dof_parent_param_offset = np.asarray(data.dof_parent_param_offset) + + first_cell = cell_dofs[cell_offsets[0] : cell_offsets[1]] + second_cell = cell_dofs[cell_offsets[1] : cell_offsets[2]] + + # Basix Triangle6 ordering: + # vertices (0,1,2), then edge dofs on (1,2), (0,2), (0,1). + expected_first = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0], + [0.5, 0.5], + [0.0, 0.5], + [0.5, 0.0], + ] + ) + np.testing.assert_allclose(dof_coords[first_cell], expected_first) + + shared = set(first_cell).intersection(set(second_cell)) + assert len(shared) == 3 + + # First 4 dofs are parent vertices; remaining 5 are edge-born. + np.testing.assert_array_equal(dof_parent_dim[:4], np.zeros(4, dtype=np.int8)) + np.testing.assert_array_equal(dof_parent_id[:4], np.arange(4, dtype=np.int32)) + np.testing.assert_array_equal(dof_parent_dim[4:], np.ones(5, dtype=np.int8)) + assert dof_parent_param_offset.shape[0] == data.num_dofs() + 1 + + # The shared midpoint on the diagonal edge should have one edge parameter at t=0.5. + shared_edge_dof = first_cell[3] + pb = dof_parent_param_offset[shared_edge_dof] + pe = dof_parent_param_offset[shared_edge_dof + 1] + np.testing.assert_allclose(dof_parent_param[pb:pe], np.array([0.5])) + + +def test_create_level_set_mesh_data_from_arrays(): + dof_coordinates = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0], + ], + dtype=np.float64, + ) + cell_dofs = np.array([0, 1, 2], dtype=np.int32) + cell_offsets = np.array([0, 3], dtype=np.int32) + cell_types = np.array([5], dtype=np.int32) + + data = cutcells.create_level_set_mesh_data( + dof_coordinates, cell_dofs, cell_offsets, degree=1, tdim=2, cell_types=cell_types + ) + + assert data.gdim == 2 + assert data.tdim == 2 + assert data.degree == 1 + np.testing.assert_array_equal(np.asarray(data.cell_dofs), cell_dofs) + np.testing.assert_array_equal(np.asarray(data.cell_offsets), cell_offsets) + np.testing.assert_allclose(np.asarray(data.dof_coordinates), dof_coordinates) + assert np.asarray(data.dof_parent_dim).size == 0 diff --git a/python/tests/test_level_set_vtu.py b/python/tests/test_level_set_vtu.py new file mode 100644 index 0000000..822e339 --- /dev/null +++ b/python/tests/test_level_set_vtu.py @@ -0,0 +1,210 @@ +from pathlib import Path +import xml.etree.ElementTree as ET + +import numpy as np +import pytest + +import cutcells + + +TRIANGLE_BASIX_TO_VTK = { + 2: [0, 1, 2, 5, 3, 4], + 3: [0, 1, 2, 7, 8, 3, 4, 6, 5, 9], + 4: [0, 1, 2, 9, 10, 11, 3, 4, 5, 8, 7, 6, 12, 13, 14], +} + +TETRA_BASIX_TO_VTK = { + 2: [0, 1, 2, 3, 9, 6, 8, 7, 5, 4], + 3: [0, 1, 2, 3, 14, 15, 8, 9, 13, 12, 10, 11, 6, 7, 4, 5, 18, 16, 17, 19], + 4: [ + 0, + 1, + 2, + 3, + 19, + 20, + 21, + 10, + 11, + 12, + 18, + 17, + 16, + 13, + 14, + 15, + 7, + 8, + 9, + 4, + 5, + 6, + 28, + 29, + 30, + 23, + 24, + 22, + 25, + 27, + 26, + 31, + 33, + 32, + 34, + ], +} + + +def _read_dataarray(path: Path, name: str) -> str: + root = ET.parse(path).getroot() + for data_array in root.iter("DataArray"): + if data_array.attrib.get("Name") == name: + return (data_array.text or "").strip() + raise AssertionError(f"DataArray '{name}' not found in {path}") + + +def _find_dataarray(path: Path, name: str) -> ET.Element: + root = ET.parse(path).getroot() + for data_array in root.iter("DataArray"): + if data_array.attrib.get("Name") == name: + return data_array + raise AssertionError(f"DataArray '{name}' not found in {path}") + + +def _make_single_cell_level_set(cell_family: str, degree: int): + if cell_family == "triangle": + local_dofs = (degree + 1) * (degree + 2) // 2 + vtk_type = 5 + tdim = 2 + elif cell_family == "tetra": + local_dofs = (degree + 1) * (degree + 2) * (degree + 3) // 6 + vtk_type = 10 + tdim = 3 + else: + raise ValueError(f"Unsupported cell_family={cell_family}") + + dof_coordinates = np.zeros((local_dofs, 3), dtype=np.float64) + dof_coordinates[:, 0] = np.arange(local_dofs, dtype=np.float64) + cell_dofs = np.arange(local_dofs, dtype=np.int32) + cell_offsets = np.array([0, local_dofs], dtype=np.int32) + cell_types = np.array([vtk_type], dtype=np.int32) + dof_values = np.arange(local_dofs, dtype=np.float64) + 0.125 + + mesh_data = cutcells.create_level_set_mesh_data( + dof_coordinates=dof_coordinates, + cell_dofs=cell_dofs, + cell_offsets=cell_offsets, + degree=degree, + tdim=tdim, + cell_types=cell_types, + ) + ls = cutcells.create_level_set_function(mesh_data, dof_values) + return ls, dof_values + + +@pytest.mark.parametrize("degree", [2, 3, 4]) +def test_write_level_set_vtu_triangle_connectivity_permutation(tmp_path, degree): + ls, dof_values = _make_single_cell_level_set("triangle", degree) + output = tmp_path / f"triangle_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output), ls, field_name="phi") + + connectivity = [int(x) for x in _read_dataarray(output, "connectivity").split()] + types = [int(x) for x in _read_dataarray(output, "types").split()] + phi = np.fromstring(_read_dataarray(output, "phi"), sep=" ") + + assert connectivity == TRIANGLE_BASIX_TO_VTK[degree] + assert types == [69] # VTK_LAGRANGE_TRIANGLE + np.testing.assert_allclose(phi, dof_values) + + +@pytest.mark.parametrize("degree", [2, 3, 4]) +def test_write_level_set_vtu_tetra_connectivity_permutation(tmp_path, degree): + ls, dof_values = _make_single_cell_level_set("tetra", degree) + output = tmp_path / f"tetra_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output), ls, field_name="phi") + + connectivity = [int(x) for x in _read_dataarray(output, "connectivity").split()] + types = [int(x) for x in _read_dataarray(output, "types").split()] + phi = np.fromstring(_read_dataarray(output, "phi"), sep=" ") + + assert connectivity == TETRA_BASIX_TO_VTK[degree] + assert types == [71] # VTK_LAGRANGE_TETRAHEDRON + np.testing.assert_allclose(phi, dof_values) + + +def test_write_level_set_vtu_xml_matches_vtk_expectations(tmp_path): + ls, _ = _make_single_cell_level_set("triangle", 3) + output = tmp_path / "triangle_p3.vtu" + cutcells.write_level_set_vtu(str(output), ls, field_name="phi") + + root = ET.parse(output).getroot() + assert root.attrib["type"] == "UnstructuredGrid" + assert root.attrib["version"] == "0.1" + assert root.attrib["byte_order"] in {"LittleEndian", "BigEndian"} + + types = _find_dataarray(output, "types") + assert types.attrib["type"] == "UInt8" + + +@pytest.mark.parametrize( + ("cell_family", "degree", "expected_type", "expected_local_dofs"), + [ + ("triangle", 2, 69, 6), + ("triangle", 3, 69, 10), + ("triangle", 4, 69, 15), + ("tetra", 2, 71, 10), + ("tetra", 3, 71, 20), + ("tetra", 4, 71, 35), + ], +) +def test_write_level_set_vtu_cell_widths_match_degree( + tmp_path, cell_family, degree, expected_type, expected_local_dofs +): + ls, _ = _make_single_cell_level_set(cell_family, degree) + output = tmp_path / f"{cell_family}_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output), ls, field_name="phi") + + connectivity = [int(x) for x in _read_dataarray(output, "connectivity").split()] + offsets = [int(x) for x in _read_dataarray(output, "offsets").split()] + types = [int(x) for x in _read_dataarray(output, "types").split()] + + prev = 0 + widths = [] + for off in offsets: + widths.append(off - prev) + prev = off + + assert widths == [expected_local_dofs] + assert len(connectivity) == expected_local_dofs + assert types == [expected_type] + + +@pytest.mark.parametrize("cell_family", ["triangle", "tetra"]) +@pytest.mark.parametrize("degree", [2, 3, 4]) +def test_write_level_set_vtu_roundtrip_with_pyvista(tmp_path, cell_family, degree): + pv = pytest.importorskip("pyvista") + + if cell_family == "triangle": + grid = cutcells.rectangle_triangle_mesh(-1.0, -1.0, 1.0, 1.0, 5, 5) + mesh = cutcells.mesh_from_pyvista(grid, tdim=2) + expected_type = 69 + else: + grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 4, 4, 4) + mesh = cutcells.mesh_from_pyvista(grid, tdim=3) + expected_type = 71 + + def phi(X): + return np.sqrt((X[:, 0] - 0.15) ** 2 + (X[:, 1] + 0.1) ** 2 + (X[:, 2] - 0.05) ** 2) - 0.55 + + ls = cutcells.interpolate_level_set(mesh, phi, degree=degree) + output = tmp_path / f"{cell_family}_p{degree}.vtu" + cutcells.write_level_set_vtu(str(output), ls, field_name="phi") + + out_grid = pv.read(output) + assert out_grid.n_points == ls.mesh_data.num_dofs() + assert "phi" in out_grid.point_data + assert len(np.asarray(out_grid.point_data["phi"])) == ls.mesh_data.num_dofs() + assert np.all(np.asarray(out_grid.celltypes) == expected_type) + assert all(out_grid.GetCell(i).GetOrder() == degree for i in range(out_grid.n_cells)) + assert all(not out_grid.GetCell(i).IsLinear() for i in range(out_grid.n_cells)) diff --git a/python/tests/test_mesh_utils.py b/python/tests/test_mesh_utils.py new file mode 100644 index 0000000..9f57f3b --- /dev/null +++ b/python/tests/test_mesh_utils.py @@ -0,0 +1,17 @@ +import pytest +import cutcells + + +def test_rectangle_triangle(): + grid = cutcells.rectangle_triangle_mesh(-1, -1, 1, 1, 10, 10) + assert grid.n_points == 100 + assert grid.n_cells > 0 + assert int(grid.celltypes[0]) == 5 # VTK_TRIANGLE + + +def test_mesh_from_pyvista_infers_tdim(): + grid = cutcells.rectangle_triangle_mesh(-1, -1, 1, 1, 5, 5) + mesh = cutcells.mesh_from_pyvista(grid) + assert mesh.tdim == 2 + assert mesh.gdim == 3 + assert mesh.num_cells() == grid.n_cells From b066492f47a2c14eb9ac41603a37014dccf439de Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 11 Apr 2026 15:11:18 +0200 Subject: [PATCH 07/23] new higher order cutting path --- cpp/src/adapt_cell.cpp | 160 +++++ cpp/src/adapt_cell.h | 518 ++++++++++++++ cpp/src/bernstein.cpp | 960 +++++++++++++++++++++++++ cpp/src/bernstein.h | 115 +++ cpp/src/cell_certification.cpp | 1209 ++++++++++++++++++++++++++++++++ cpp/src/cell_certification.h | 150 ++++ cpp/src/cell_subdivision.h | 62 +- cpp/src/cell_topology.h | 123 ++-- cpp/src/cell_types.h | 2 +- cpp/src/cut_cell.cpp | 8 +- cpp/src/cut_mesh.cpp | 24 +- cpp/src/cut_tetrahedron.cpp | 22 +- cpp/src/cut_triangle.cpp | 10 +- cpp/src/edge_certification.cpp | 1103 +++++++++++++++++++++++++++++ cpp/src/edge_certification.h | 198 ++++++ cpp/src/edge_root.h | 1148 ++++++++++++++++++++++++++++++ cpp/src/ho_cut_mesh.cpp | 424 +++++++++++ cpp/src/ho_cut_mesh.h | 172 +++++ cpp/src/level_set.cpp | 94 +-- cpp/src/level_set.h | 13 +- cpp/src/level_set_cell.cpp | 219 ++++++ cpp/src/level_set_cell.h | 65 ++ cpp/src/mesh_view.h | 13 +- cpp/src/reference_cell.h | 270 +++++++ cpp/src/refine_cell.cpp | 773 ++++++++++++++++++++ cpp/src/refine_cell.h | 88 +++ cpp/src/selection_expr.cpp | 196 ++++++ cpp/src/selection_expr.h | 77 ++ cpp/src/triangulation.h | 17 - cpp/src/write_vtk.cpp | 12 +- python/cutcells/__init__.py | 141 +++- python/cutcells/wrapper.cpp | 661 ++++++++++++++++- 32 files changed, 8789 insertions(+), 258 deletions(-) create mode 100644 cpp/src/adapt_cell.cpp create mode 100644 cpp/src/adapt_cell.h create mode 100644 cpp/src/bernstein.cpp create mode 100644 cpp/src/bernstein.h create mode 100644 cpp/src/cell_certification.cpp create mode 100644 cpp/src/cell_certification.h create mode 100644 cpp/src/edge_certification.cpp create mode 100644 cpp/src/edge_certification.h create mode 100644 cpp/src/edge_root.h create mode 100644 cpp/src/ho_cut_mesh.cpp create mode 100644 cpp/src/ho_cut_mesh.h create mode 100644 cpp/src/level_set_cell.cpp create mode 100644 cpp/src/level_set_cell.h create mode 100644 cpp/src/reference_cell.h create mode 100644 cpp/src/refine_cell.cpp create mode 100644 cpp/src/refine_cell.h create mode 100644 cpp/src/selection_expr.cpp create mode 100644 cpp/src/selection_expr.h diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp new file mode 100644 index 0000000..a70bbf0 --- /dev/null +++ b/cpp/src/adapt_cell.cpp @@ -0,0 +1,160 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "adapt_cell.h" +#include "cell_topology.h" +#include "reference_cell.h" + +#include +#include +#include +#include + +namespace cutcells +{ +// --------------------------------------------------------------------------- +// make_adapt_cell +// --------------------------------------------------------------------------- + +template +AdaptCell make_adapt_cell(const MeshView& mesh, I cell_id) +{ + if (!mesh.has_cell_types()) + throw std::runtime_error( + "make_adapt_cell: MeshView must have cell types"); + + const cell::type ctype = mesh.cell_type(cell_id); + + const int tdim = cell::get_tdim(ctype); + const int gdim = mesh.gdim; + const int nv = cell::get_num_vertices(ctype); + + AdaptCell ac; + ac.gdim = gdim; + ac.tdim = tdim; + ac.parent_cell_type = ctype; + ac.parent_cell_id = static_cast(cell_id); + + // Reference vertex coordinates: canonical corners of the reference element. + // Stored as nv * tdim values. + ac.vertex_coords = reference_vertices(ctype); + + // Vertex provenance: all vertices coincide with parent-mesh vertices (dim 0). + ac.vertex_parent_dim.assign(static_cast(nv), std::int8_t(0)); + ac.vertex_parent_id.resize(static_cast(nv)); + for (int v = 0; v < nv; ++v) + ac.vertex_parent_id[static_cast(v)] = + static_cast(mesh.cell_node(cell_id, static_cast(v))); + + // No parametric coordinates for parent vertices (empty param per vertex). + ac.vertex_parent_param_offset.assign(static_cast(nv + 1), + std::int32_t(0)); + // vertex_parent_param stays empty. + ac.vertex_source_edge_id.assign(static_cast(nv), std::int32_t(-1)); + + // Level-set sign masks start unset (bit not set = value is positive). + ac.zero_mask_per_vertex.assign(static_cast(nv), + std::uint64_t(0)); + ac.negative_mask_per_vertex.assign(static_cast(nv), + std::uint64_t(0)); + + // Entity pool at topological dimension: one entity = the full background cell. + ac.entity_types[tdim].push_back(ctype); + ac.entity_to_vertex[tdim].offsets = {std::int32_t(0), std::int32_t(nv)}; + ac.entity_to_vertex[tdim].indices.resize(static_cast(nv)); + for (int v = 0; v < nv; ++v) + ac.entity_to_vertex[tdim].indices[static_cast(v)] = + std::int32_t(v); + + return ac; +} + +// --------------------------------------------------------------------------- +// fill_vertex_signs +// --------------------------------------------------------------------------- + +template +void fill_vertex_signs(AdaptCell& ac, + std::span vertex_ls_values, + int ls_index, + T tol){ + const int nv = ac.n_vertices(); + assert(static_cast(vertex_ls_values.size()) >= nv + && "fill_vertex_signs: vertex_ls_values too short"); + assert(ls_index >= 0 && ls_index < 64 + && "fill_vertex_signs: ls_index out of [0,63]"); + + const std::uint64_t bit = std::uint64_t(1) << ls_index; + + for (int v = 0; v < nv; ++v) + { + const T val = vertex_ls_values[static_cast(v)]; + if (std::fabs(val) < tol) + ac.zero_mask_per_vertex[static_cast(v)] |= bit; + else if (val < T(0)) + ac.negative_mask_per_vertex[static_cast(v)] |= bit; + } +} + +// --------------------------------------------------------------------------- +// Explicit template instantiations +// --------------------------------------------------------------------------- + +template AdaptCell make_adapt_cell(const MeshView&, int); +template AdaptCell make_adapt_cell(const MeshView&, int); +template AdaptCell make_adapt_cell(const MeshView&, long); +template AdaptCell make_adapt_cell(const MeshView&, long); + +template void fill_vertex_signs(AdaptCell&, std::span, int, double); +template void fill_vertex_signs(AdaptCell&, std::span, int, float); + +// --------------------------------------------------------------------------- +// build_edges +// --------------------------------------------------------------------------- + +template +void build_edges(AdaptCell& ac) +{ + const int tdim = ac.tdim; + + // Clear existing 1D entity pool. + ac.entity_types[1].clear(); + ac.entity_to_vertex[1].offsets.clear(); + ac.entity_to_vertex[1].indices.clear(); + ac.entity_to_vertex[1].offsets.push_back(std::int32_t(0)); + + // Track unique edges as (min_v, max_v) → edge_id. + std::map, int> edge_map; + + const int n_cells = ac.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + const cell::type ctype = ac.entity_types[tdim][static_cast(c)]; + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + auto cell_edges = cell::edges(ctype); + + for (const auto& ce : cell_edges) + { + const std::int32_t lv0 = cell_verts[static_cast(ce[0])]; + const std::int32_t lv1 = cell_verts[static_cast(ce[1])]; + const auto key = std::make_pair(std::min(lv0, lv1), std::max(lv0, lv1)); + + if (edge_map.find(key) == edge_map.end()) + { + edge_map[key] = ac.n_entities(1); + ac.entity_types[1].push_back(cell::type::interval); + ac.entity_to_vertex[1].indices.push_back(lv0); + ac.entity_to_vertex[1].indices.push_back(lv1); + ac.entity_to_vertex[1].offsets.push_back( + static_cast(ac.entity_to_vertex[1].indices.size())); + } + } + } +} + +template void build_edges(AdaptCell&); +template void build_edges(AdaptCell&); + +} // namespace cutcells diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h new file mode 100644 index 0000000..06f7150 --- /dev/null +++ b/cpp/src/adapt_cell.h @@ -0,0 +1,518 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +#include "cell_types.h" +#include "mesh_view.h" + +namespace cutcells +{ + +// --------------------------------------------------------------- +// Tag enumerations for certification +// --------------------------------------------------------------- + +/// Per-edge root classification for one level set. +enum class EdgeRootTag : std::uint8_t +{ + not_classified = 0, + no_root = 1, + one_root = 2, + multiple_roots = 3, + zero = 4 +}; + +/// Per-leaf-cell certification state for one level set. +enum class CellCertTag : std::uint8_t +{ + not_classified = 0, + positive = 1, + negative = 2, + cut = 3, + zero = 4, + ambiguous = 5, + ready_to_cut = 6 +}; + +/// Per-face certification state for one level set. +/// A face is a 2D entity; its sign/cut state is analogous to a cell. +enum class FaceCertTag : std::uint8_t +{ + not_classified = 0, + positive = 1, + negative = 2, + cut = 3, + zero = 4, + ambiguous = 5 +}; + +/// Compact Sparse Row (CSR) adjacency map between entities. +/// +/// Stores a variable-length list of adjacent entity indices for each source entity. +/// `offsets[i]` and `offsets[i+1]` delimit the slice of `indices` for entity `i`. +/// +/// Used for: +/// - `entity_to_vertex[d]`: maps entity i of dimension d to its vertex indices +/// - `connectivity[d0][d1]`: maps entities of dimension d0 to entities of dimension d1 +struct EntityAdjacency +{ + /// offsets into `indices`; size = n_entities + 1. offsets[0] == 0 always. + std::vector offsets; + + /// Flat list of adjacent entity ids. + std::vector indices; + + /// Number of source entities. + std::int32_t size() const noexcept + { + return offsets.empty() ? 0 : static_cast(offsets.size()) - 1; + } + + /// Return the adjacency list for source entity `i`. + std::span operator[](std::int32_t i) const + { + return {indices.data() + offsets[i], + static_cast(offsets[i + 1] - offsets[i])}; + } + + /// Return a mutable adjacency list for source entity `i`. + std::span operator[](std::int32_t i) + { + return {indices.data() + offsets[i], + static_cast(offsets[i + 1] - offsets[i])}; + } +}; + +/// AdaptCell is the main local container for one background cell. +/// It is a dynamic mesh in reference coordinates: vertices carry the rich +/// semantic data (coordinates, level-set signs, provenance), and entities of +/// dimension 1, 2, 3 are defined purely through their vertex references. +/// Only leaf entities are stored — no refinement tree. +template +struct AdaptCell +{ + // --------------------------------------------------------------- + // Metadata + // --------------------------------------------------------------- + + int gdim = 0; ///< geometric dimension of the embedding space + int tdim = 0; ///< topological dimension (2 for triangles/quads, 3 for tets/hexes) + + cell::type parent_cell_type = cell::type::point; ///< type of the original background cell + int parent_cell_id = -1; ///< index of the background cell in the mesh + + ///< bit i set → level set i is active on this cell (cuts through it) + std::uint64_t active_level_set_mask = 0; + + // --------------------------------------------------------------- + // Vertices (dimension 0) + // + // Vertices are the primary data carriers. They are NOT stored in + // the entity pools below. All arrays are parallel, indexed by + // vertex id v = 0, 1, ..., n_vertices()-1. + // --------------------------------------------------------------- + + /// Reference coordinates of each vertex, flattened: [v0_xi0, v0_xi1, ..., v1_xi0, ...]. + /// These are coordinates in the reference cell of the parent background cell. + std::vector vertex_coords; + + /// Tri-state sign storage per vertex and per level set. + /// For level set i (0-indexed): + /// bit i set in zero_mask[v] → φ_i(v) = 0 + /// bit i set in negative_mask[v] → φ_i(v) < 0 + /// neither → φ_i(v) > 0 + std::vector zero_mask_per_vertex; + std::vector negative_mask_per_vertex; + + /// Provenance: where in the ORIGINAL parent background cell did this vertex come from? + /// This is the key to global stitching without geometric deduplication. + /// + /// parent_entity_dim: 0 = parent vertex, 1 = parent edge, 2 = parent face, 3 = parent cell interior + /// parent_entity_id: GLOBAL mesh entity id (e.g. global edge 47, global vertex 12). + /// Using global ids makes stitching across background cells trivial: + /// two AdaptCells sharing an edge both reference the same global edge id, + /// so matching vertices requires only (parent_entity_id, parent_param) comparison. + /// parent_local_param: parametric coordinate(s) within the parent entity. + /// For a vertex born on a parent edge: one parameter t ∈ [0,1]. + /// For a vertex born on a parent face: two parameters (u,v). + /// For a vertex coinciding with a parent vertex: empty. + /// For a vertex in the cell interior: tdim parameters. + /// Stored flat; use parent_entity_dim to know how many params per vertex. + std::vector vertex_parent_dim; + std::vector vertex_parent_id; + std::vector vertex_parent_param; ///< flat, variable-length per vertex + std::vector vertex_parent_param_offset; ///< size = n_vertices + 1 + std::vector vertex_source_edge_id; ///< leaf-edge provenance; -1 if not created from a leaf edge + + // --------------------------------------------------------------- + // Entity pools (dimensions 1 .. tdim) + // + // Each entity of dimension d has: + // - a cell type (e.g. interval for d=1, triangle/quad for d=2, tet/hex for d=3) + // - an ordered list of vertex indices (via entity_to_vertex[d]) + // + // Entities are numbered 0, 1, ..., n_entities(d)-1 within each dimension. + // Only leaf (final, active) entities are stored. + // --------------------------------------------------------------- + + static constexpr int max_dim = 4; + + /// Cell type of each entity. entity_types[d][i] is the type of entity i at dimension d. + /// For d=1 this is always cell::type::interval, but stored for uniformity. + std::array, max_dim> entity_types; + + /// Canonical topology: entity → vertex. + /// entity_to_vertex[d] stores the vertex indices for each entity of dimension d. + /// This is CSR-style: offsets + flat indices. + std::array entity_to_vertex; + + // --------------------------------------------------------------- + // Connectivity cache (d0 → d1) + // + // Generic adjacency between entities of different dimensions. + // Built on demand, not stored by default. + // The canonical source is always entity_to_vertex; everything + // else is derived from it. + // --------------------------------------------------------------- + + std::array, max_dim> connectivity; + std::array, max_dim> has_connectivity = {}; + + // --------------------------------------------------------------- + // Zero-entity inventory (semantic layer for interfaces and intersections) + // + // A zero entity is an entity where one or more level sets vanish. + // Each entry references an entity in the topology pools above. + // This is the layer used for selections like "phi = 0" and + // "phi1 = 0 and phi2 = 0". + // + // All arrays are parallel, indexed by zero-entity id z = 0, 1, ... + // --------------------------------------------------------------- + + std::vector zero_entity_dim; ///< dimension of the entity (1, 2, ...) + std::vector zero_entity_id; ///< index into entity_types[dim] / entity_to_vertex[dim] + std::vector zero_entity_zero_mask; ///< which level sets vanish: bit i set → φ_i = 0 on this entity + std::vector zero_entity_is_owned; ///< ownership flag for global assembly (avoids double-counting) + + /// Provenance of the zero entity back to the parent background cell. + /// A zero entity born on parent face 2 gets parent_dim=2, parent_id=2. + /// This enables global stitching of interface meshes. + std::vector zero_entity_parent_dim; ///< -1 if not on a shared parent entity + std::vector zero_entity_parent_id; ///< -1 if not on a shared parent entity + + std::uint32_t zero_entity_version = 0; ///< bumped when zero-entity inventory changes + + // --------------------------------------------------------------- + // Per-level-set edge tags + // + // Flat storage: edge_root_tag[ls_id * n_edges(1) + edge_id] + // --------------------------------------------------------------- + + std::vector edge_root_tag; + int edge_root_tag_num_level_sets = 0; + + EdgeRootTag get_edge_root_tag(int level_set_id, int edge_id) const + { + return edge_root_tag[static_cast( + level_set_id * n_entities(1) + edge_id)]; + } + + void set_edge_root_tag(int level_set_id, int edge_id, EdgeRootTag tag) + { + edge_root_tag[static_cast( + level_set_id * n_entities(1) + edge_id)] = tag; + } + + void resize_edge_root_tags(int num_level_sets) + { + const int old_num_level_sets = edge_root_tag_num_level_sets; + const int old_n_edges = (old_num_level_sets > 0) + ? static_cast(edge_root_tag.size()) + / old_num_level_sets + : 0; + const int new_n_edges = n_entities(1); + + std::vector new_tags( + static_cast(num_level_sets * new_n_edges), + EdgeRootTag::not_classified); + + const int copy_ls = std::min(old_num_level_sets, num_level_sets); + const int copy_edges = std::min(old_n_edges, new_n_edges); + for (int ls = 0; ls < copy_ls; ++ls) + { + for (int e = 0; e < copy_edges; ++e) + { + new_tags[static_cast(ls * new_n_edges + e)] = + edge_root_tag[static_cast(ls * old_n_edges + e)]; + } + } + + edge_root_tag_num_level_sets = num_level_sets; + edge_root_tag = std::move(new_tags); + } + + // --------------------------------------------------------------- + // Per-level-set cell certification tags + // + // Flat storage: cell_cert_tag[ls_id * n_entities(tdim) + cell_id] + // --------------------------------------------------------------- + + std::vector cell_cert_tag; + int cell_cert_tag_num_level_sets = 0; + + CellCertTag get_cell_cert_tag(int level_set_id, int cell_id) const + { + return cell_cert_tag[static_cast( + level_set_id * n_entities(tdim) + cell_id)]; + } + + void set_cell_cert_tag(int level_set_id, int cell_id, CellCertTag tag) + { + cell_cert_tag[static_cast( + level_set_id * n_entities(tdim) + cell_id)] = tag; + } + + void resize_cell_cert_tags(int num_level_sets) + { + const int old_num_level_sets = cell_cert_tag_num_level_sets; + const int old_n_cells = (old_num_level_sets > 0) + ? static_cast(cell_cert_tag.size()) + / old_num_level_sets + : 0; + const int new_n_cells = n_entities(tdim); + + std::vector new_tags( + static_cast(num_level_sets * new_n_cells), + CellCertTag::not_classified); + + const int copy_ls = std::min(old_num_level_sets, num_level_sets); + const int copy_cells = std::min(old_n_cells, new_n_cells); + for (int ls = 0; ls < copy_ls; ++ls) + { + for (int c = 0; c < copy_cells; ++c) + { + new_tags[static_cast(ls * new_n_cells + c)] = + cell_cert_tag[static_cast(ls * old_n_cells + c)]; + } + } + + cell_cert_tag_num_level_sets = num_level_sets; + cell_cert_tag = std::move(new_tags); + } + + // --------------------------------------------------------------- + // Green-split metadata per edge per level set + // + // Flat storage: edge_green_split_param[ls_id * n_edges + edge_id] + // edge_green_split_has_value: nonzero if the param is valid + // --------------------------------------------------------------- + + std::vector edge_green_split_param; + std::vector edge_green_split_has_value; + + void resize_green_split_data(int num_level_sets) + { + const int new_n_edges = n_entities(1); + const int old_num_level_sets = (!edge_green_split_param.empty() && new_n_edges > 0) + ? static_cast(edge_green_split_param.size()) + / new_n_edges + : 0; + const int old_n_edges = new_n_edges; + const auto n = static_cast(num_level_sets * new_n_edges); + + std::vector new_params(n, T(0)); + std::vector new_has_value(n, std::uint8_t(0)); + + const int copy_ls = std::min(old_num_level_sets, num_level_sets); + const int copy_edges = std::min(old_n_edges, new_n_edges); + for (int ls = 0; ls < copy_ls; ++ls) + { + for (int e = 0; e < copy_edges; ++e) + { + new_params[static_cast(ls * new_n_edges + e)] = + edge_green_split_param[static_cast(ls * old_n_edges + e)]; + new_has_value[static_cast(ls * new_n_edges + e)] = + edge_green_split_has_value[static_cast(ls * old_n_edges + e)]; + } + } + + edge_green_split_param = std::move(new_params); + edge_green_split_has_value = std::move(new_has_value); + } + + // --------------------------------------------------------------- + // One-root metadata per edge per level set + // + // Stores the localized root parameter on leaf edges tagged one_root + // together with the AdaptCell vertex id created for that root. + // --------------------------------------------------------------- + + std::vector edge_one_root_param; + std::vector edge_one_root_vertex_id; + std::vector edge_one_root_has_value; + + void resize_one_root_data(int num_level_sets) + { + const int new_n_edges = n_entities(1); + const int old_num_level_sets = (!edge_one_root_param.empty() && new_n_edges > 0) + ? static_cast(edge_one_root_param.size()) + / new_n_edges + : 0; + const int old_n_edges = new_n_edges; + const auto n = static_cast(num_level_sets * new_n_edges); + + std::vector new_params(n, T(0)); + std::vector new_vertex_ids(n, std::int32_t(-1)); + std::vector new_has_value(n, std::uint8_t(0)); + + const int copy_ls = std::min(old_num_level_sets, num_level_sets); + const int copy_edges = std::min(old_n_edges, new_n_edges); + for (int ls = 0; ls < copy_ls; ++ls) + { + for (int e = 0; e < copy_edges; ++e) + { + new_params[static_cast(ls * new_n_edges + e)] = + edge_one_root_param[static_cast(ls * old_n_edges + e)]; + new_vertex_ids[static_cast(ls * new_n_edges + e)] = + edge_one_root_vertex_id[static_cast(ls * old_n_edges + e)]; + new_has_value[static_cast(ls * new_n_edges + e)] = + edge_one_root_has_value[static_cast(ls * old_n_edges + e)]; + } + } + + edge_one_root_param = std::move(new_params); + edge_one_root_vertex_id = std::move(new_vertex_ids); + edge_one_root_has_value = std::move(new_has_value); + } + + // --------------------------------------------------------------- + // Convenience accessors + // --------------------------------------------------------------- + + /// Number of vertices in this cell + int n_vertices() const + { + return tdim > 0 ? static_cast(vertex_coords.size()) / tdim : 0; + } + + /// Number of entities of dimension d + int n_entities(int d) const + { + return (d >= 1 && d < max_dim) + ? static_cast(entity_types[d].size()) + : 0; + } + + /// Check if connectivity map between dimensions d0 and d1 exists + bool has_connectivity_map(int d0, int d1) const + { + return (d0 >= 1 && d0 < max_dim && d1 >= 1 && d1 < max_dim) + ? (has_connectivity[d0][d1] != 0) + : false; + } + + /// Number of zero entities + int n_zero_entities() const + { + return static_cast(zero_entity_id.size()); + } + + // --------------------------------------------------------------- + // Per-level-set face certification tags (3D only) + // + // Flat storage: face_cert_tag[ls_id * n_entities(2) + face_id] + // --------------------------------------------------------------- + + std::vector face_cert_tag; + int face_cert_tag_num_level_sets = 0; + + FaceCertTag get_face_cert_tag(int level_set_id, int face_id) const + { + return face_cert_tag[static_cast( + level_set_id * n_entities(2) + face_id)]; + } + + void set_face_cert_tag(int level_set_id, int face_id, FaceCertTag tag) + { + face_cert_tag[static_cast( + level_set_id * n_entities(2) + face_id)] = tag; + } + + void resize_face_cert_tags(int num_level_sets) + { + const int old_num_level_sets = face_cert_tag_num_level_sets; + const int old_n_faces = (old_num_level_sets > 0) + ? static_cast(face_cert_tag.size()) + / old_num_level_sets + : 0; + const int new_n_faces = n_entities(2); + + std::vector new_tags( + static_cast(num_level_sets * new_n_faces), + FaceCertTag::not_classified); + + const int copy_ls = std::min(old_num_level_sets, num_level_sets); + const int copy_faces = std::min(old_n_faces, new_n_faces); + for (int ls = 0; ls < copy_ls; ++ls) + { + for (int f = 0; f < copy_faces; ++f) + { + new_tags[static_cast(ls * new_n_faces + f)] = + face_cert_tag[static_cast(ls * old_n_faces + f)]; + } + } + + face_cert_tag_num_level_sets = num_level_sets; + face_cert_tag = std::move(new_tags); + } +}; + +/// Create an AdaptCell initialised from a single background cell in a MeshView. +/// +/// The returned cell owns its own storage. Every vertex is tagged with +/// provenance dimension 0 (= parent vertex) and the global mesh node id. +/// Reference coordinates follow the canonical ordering defined in mapping.h. +/// Level-set sign masks are zeroed (all-positive) and must be filled +/// separately with fill_vertex_signs(). +/// +/// @param mesh Background mesh. MeshView::cell_types must be present +/// (cutcells integer codes). +/// @param cell_id Index of the cell to extract. +/// @return Initialised AdaptCell in reference space. +template +AdaptCell make_adapt_cell(const MeshView& mesh, I cell_id); + +/// Fill the vertex-level-set sign masks of an AdaptCell for one level set. +/// +/// @param ac AdaptCell whose masks are updated in place. +/// @param vertex_ls_values Level-set values at the vertices of the cell, +/// in the same vertex ordering as ac.vertex_coords. +/// Must have at least ac.n_vertices() entries. +/// @param ls_index Which level set (bit position, 0–63). +/// @param tol Absolute tolerance for the zero test. +template +void fill_vertex_signs(AdaptCell& ac, + std::span vertex_ls_values, + int ls_index, + T tol = T(1e-14)); + +/// Build (or rebuild) entity_to_vertex[1] from all top-dimensional leaf cells. +/// +/// Clears any existing 1D entity pool, then re-derives edges by iterating +/// every cell in entity_to_vertex[tdim] and adding vertex-pairs from the +/// cell type's edge table (cell_topology.h ordering). Duplicate edges are +/// suppressed; each unique (min_v, max_v) pair appears once. +/// +/// @param ac AdaptCell to update in place. +template +void build_edges(AdaptCell& ac); + +} // namespace cutcells diff --git a/cpp/src/bernstein.cpp b/cpp/src/bernstein.cpp new file mode 100644 index 0000000..13b2fb9 --- /dev/null +++ b/cpp/src/bernstein.cpp @@ -0,0 +1,960 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "bernstein.h" + +#include +#include +#include +#include +#include + +namespace cutcells::bernstein +{ +namespace +{ + +// --------------------------------------------------------------------------- +// Arithmetic helpers +// --------------------------------------------------------------------------- + +/// Compute base^exp for non-negative integer exponent. +/// Handles 0^0 = 1 correctly. +template +T integer_pow(T base, int exp) +{ + T result = T(1); + for (int i = 0; i < exp; ++i) + result *= base; + return result; +} + +/// Binomial coefficient C(n, k) computed without overflow for moderate n. +template +T binomial(int n, int k) +{ + if (k < 0 || k > n) + return T(0); + if (k == 0 || k == n) + return T(1); + if (k > n - k) + k = n - k; + T result = T(1); + for (int i = 0; i < k; ++i) + { + result *= T(n - i); + result /= T(i + 1); + } + return result; +} + +/// Multinomial coefficient n! / (alpha[0]! * ... * alpha[d]!) +/// computed as a product of binomial coefficients. +/// alpha must satisfy sum(alpha) == n. +template +T multinomial(int n, const int* alpha, int num_components) +{ + T result = T(1); + int remaining = n; + for (int c = 1; c < num_components; ++c) + { + result *= binomial(remaining, alpha[c]); + remaining -= alpha[c]; + } + return result; +} + +// --------------------------------------------------------------------------- +// Simplex multi-index → linear index +// +// Enumeration order (matching evaluate loops): +// tdim=1: i = 0..n → alpha = (n-i, i) +// tdim=2: j = 0..n, i = 0..n-j → alpha = (n-i-j, i, j) +// tdim=3: k = 0..n, j = 0..n-k, i = 0..n-k-j → alpha = (n-i-j-k, i, j, k) +// --------------------------------------------------------------------------- + +inline int simplex_index_1d(int i, int /*n*/) +{ + return i; +} + +inline int simplex_index_2d(int i, int j, int n) +{ + return j * (n + 1) - j * (j - 1) / 2 + i; +} + +inline int simplex_index_3d(int i, int j, int k, int n) +{ + // Offset for layers 0..k-1 + int offset = 0; + for (int kk = 0; kk < k; ++kk) + { + int m = n - kk; + offset += (m + 1) * (m + 2) / 2; + } + // Within layer k, it is a triangle of degree n-k + return offset + simplex_index_2d(i, j, n - k); +} + +// --------------------------------------------------------------------------- +// 1D Bernstein helpers (used by tensor-product evaluation) +// --------------------------------------------------------------------------- + +/// Evaluate all (degree+1) 1D Bernstein basis functions at t. +template +void bernstein_1d_all(int degree, T t, T* out) +{ + int n = degree; + for (int i = 0; i <= n; ++i) + out[i] = binomial(n, i) * integer_pow(t, i) * integer_pow(T(1) - t, n - i); +} + +/// Evaluate all degree 1D Bernstein basis functions of degree (n-1) at t, +/// and also return the derivative of each degree-n Bernstein basis function. +/// dB^n_i(t) = n * [B^{n-1}_{i-1}(t) - B^{n-1}_i(t)] +template +void bernstein_1d_deriv(int degree, T t, T* dbasis) +{ + int n = degree; + if (n == 0) + return; + + // Evaluate degree-(n-1) basis + std::vector b(static_cast(n)); // n = degree, n-1+1 = n entries + bernstein_1d_all(n - 1, t, b.data()); + + for (int i = 0; i <= n; ++i) + { + T bim1 = (i > 0) ? b[static_cast(i - 1)] : T(0); + T bi = (i < n) ? b[static_cast(i)] : T(0); + dbasis[i] = T(n) * (bim1 - bi); + } +} + +// --------------------------------------------------------------------------- +// Dense linear solver (Gaussian elimination with partial pivoting) +// Row-major storage: A[row * n + col] +// --------------------------------------------------------------------------- + +template +void solve_dense(int n, std::vector& A, std::vector& b) +{ + // Forward elimination + for (int col = 0; col < n; ++col) + { + // Partial pivoting + int max_row = col; + T max_val = std::abs(A[static_cast(col) * static_cast(n) + + static_cast(col)]); + for (int row = col + 1; row < n; ++row) + { + T val = std::abs(A[static_cast(row) * static_cast(n) + + static_cast(col)]); + if (val > max_val) + { + max_val = val; + max_row = row; + } + } + if (max_val < T(1e-14)) + throw std::runtime_error("bernstein: singular matrix in Lagrange-to-Bernstein conversion"); + + // Swap rows + if (max_row != col) + { + for (int j = col; j < n; ++j) + std::swap(A[static_cast(col) * static_cast(n) + + static_cast(j)], + A[static_cast(max_row) * static_cast(n) + + static_cast(j)]); + std::swap(b[static_cast(col)], + b[static_cast(max_row)]); + } + + // Eliminate below pivot + for (int row = col + 1; row < n; ++row) + { + T factor = A[static_cast(row) * static_cast(n) + + static_cast(col)] + / A[static_cast(col) * static_cast(n) + + static_cast(col)]; + for (int j = col + 1; j < n; ++j) + { + A[static_cast(row) * static_cast(n) + + static_cast(j)] + -= factor * A[static_cast(col) * static_cast(n) + + static_cast(j)]; + } + b[static_cast(row)] -= factor * b[static_cast(col)]; + } + } + + // Back substitution + for (int row = n - 1; row >= 0; --row) + { + for (int j = row + 1; j < n; ++j) + { + b[static_cast(row)] + -= A[static_cast(row) * static_cast(n) + + static_cast(j)] + * b[static_cast(j)]; + } + b[static_cast(row)] + /= A[static_cast(row) * static_cast(n) + + static_cast(row)]; + } +} + +// --------------------------------------------------------------------------- +// Simplex Bernstein evaluation +// --------------------------------------------------------------------------- + +template +T evaluate_simplex(int tdim, int degree, const T* coeffs, const T* xi) +{ + const int n = degree; + + // Barycentric coordinates: lambda[0] = 1 - sum(xi), lambda[k+1] = xi[k] + T lambda[4]; + T sum = T(0); + for (int d = 0; d < tdim; ++d) + sum += xi[d]; + lambda[0] = T(1) - sum; + for (int d = 0; d < tdim; ++d) + lambda[d + 1] = xi[d]; + + T result = T(0); + int idx = 0; + + if (tdim == 1) + { + for (int i = 0; i <= n; ++i) + { + int alpha[2] = {n - i, i}; + T b = multinomial(n, alpha, 2) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]); + result += coeffs[idx++] * b; + } + } + else if (tdim == 2) + { + for (int j = 0; j <= n; ++j) + { + for (int i = 0; i <= n - j; ++i) + { + int alpha[3] = {n - i - j, i, j}; + T b = multinomial(n, alpha, 3) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]); + result += coeffs[idx++] * b; + } + } + } + else if (tdim == 3) + { + for (int k = 0; k <= n; ++k) + { + for (int j = 0; j <= n - k; ++j) + { + for (int i = 0; i <= n - k - j; ++i) + { + int alpha[4] = {n - i - j - k, i, j, k}; + T b = multinomial(n, alpha, 4) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]) + * integer_pow(lambda[3], alpha[3]); + result += coeffs[idx++] * b; + } + } + } + } + + return result; +} + +// --------------------------------------------------------------------------- +// Tensor-product Bernstein evaluation +// --------------------------------------------------------------------------- + +template +T evaluate_tensor(int tdim, int degree, const T* coeffs, const T* xi) +{ + const int n = degree; + const int n1 = n + 1; + + if (tdim == 1) + { + // Same as simplex 1D + T result = T(0); + for (int i = 0; i <= n; ++i) + result += coeffs[i] * binomial(n, i) + * integer_pow(xi[0], i) + * integer_pow(T(1) - xi[0], n - i); + return result; + } + else if (tdim == 2) + { + // Precompute 1D basis in each direction + std::vector Bx(static_cast(n1)), By(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + + T result = T(0); + for (int i = 0; i <= n; ++i) + for (int j = 0; j <= n; ++j) + result += coeffs[i * n1 + j] * Bx[static_cast(i)] + * By[static_cast(j)]; + return result; + } + else if (tdim == 3) + { + std::vector Bx(static_cast(n1)), + By(static_cast(n1)), + Bz(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + bernstein_1d_all(n, xi[2], Bz.data()); + + T result = T(0); + for (int i = 0; i <= n; ++i) + for (int j = 0; j <= n; ++j) + for (int k = 0; k <= n; ++k) + result += coeffs[(i * n1 + j) * n1 + k] + * Bx[static_cast(i)] + * By[static_cast(j)] + * Bz[static_cast(k)]; + return result; + } + + return T(0); +} + +// --------------------------------------------------------------------------- +// Simplex Bernstein gradient +// +// Uses the derivative formula: +// df/d(xi_k) = n * sum_{|beta|=n-1} (c[beta+e_{k+1}] - c[beta+e_0]) * B^{n-1}_beta(xi) +// +// where e_0 corresponds to lambda_0 and e_{k+1} to lambda_{k+1} = xi_k. +// --------------------------------------------------------------------------- + +template +void gradient_simplex(int tdim, int degree, const T* coeffs, const T* xi, T* grad) +{ + const int n = degree; + + for (int d = 0; d < tdim; ++d) + grad[d] = T(0); + + if (n == 0) + return; + + // Barycentric coordinates + T lambda[4]; + T sum = T(0); + for (int d = 0; d < tdim; ++d) + sum += xi[d]; + lambda[0] = T(1) - sum; + for (int d = 0; d < tdim; ++d) + lambda[d + 1] = xi[d]; + + const int nm1 = n - 1; + + if (tdim == 1) + { + // df/dxi = n * sum_{i=0}^{n-1} (c[i+1] - c[i]) * B^{n-1}_i(xi) + T g = T(0); + for (int i = 0; i <= nm1; ++i) + { + int alpha[2] = {nm1 - i, i}; + T b = multinomial(nm1, alpha, 2) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]); + T dc = coeffs[i + 1] - coeffs[i]; + g += dc * b; + } + grad[0] = T(n) * g; + } + else if (tdim == 2) + { + // For direction k (0-indexed reference direction): + // df/d(xi_k) = n * sum_{beta with |beta|=n-1} + // (c[beta + e_{k+1}] - c[beta + e_0]) * B^{n-1}_beta(xi) + // + // beta = (nm1-i-j, i, j), enumerated by (j, i) + // beta + e_0 has index in degree-n array: simplex_index_2d(i, j, n) + // beta + e_1 has index: simplex_index_2d(i+1, j, n) + // beta + e_2 has index: simplex_index_2d(i, j+1, n) + + T g0 = T(0), g1 = T(0); + for (int j = 0; j <= nm1; ++j) + { + for (int i = 0; i <= nm1 - j; ++i) + { + int alpha[3] = {nm1 - i - j, i, j}; + T b = multinomial(nm1, alpha, 3) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]); + + int idx_e0 = simplex_index_2d(i, j, n); + int idx_e1 = simplex_index_2d(i + 1, j, n); + int idx_e2 = simplex_index_2d(i, j + 1, n); + + g0 += (coeffs[idx_e1] - coeffs[idx_e0]) * b; + g1 += (coeffs[idx_e2] - coeffs[idx_e0]) * b; + } + } + grad[0] = T(n) * g0; + grad[1] = T(n) * g1; + } + else if (tdim == 3) + { + T g0 = T(0), g1 = T(0), g2 = T(0); + for (int k = 0; k <= nm1; ++k) + { + for (int j = 0; j <= nm1 - k; ++j) + { + for (int i = 0; i <= nm1 - k - j; ++i) + { + int alpha[4] = {nm1 - i - j - k, i, j, k}; + T b = multinomial(nm1, alpha, 4) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]) + * integer_pow(lambda[3], alpha[3]); + + int idx_e0 = simplex_index_3d(i, j, k, n); + int idx_e1 = simplex_index_3d(i + 1, j, k, n); + int idx_e2 = simplex_index_3d(i, j + 1, k, n); + int idx_e3 = simplex_index_3d(i, j, k + 1, n); + + g0 += (coeffs[idx_e1] - coeffs[idx_e0]) * b; + g1 += (coeffs[idx_e2] - coeffs[idx_e0]) * b; + g2 += (coeffs[idx_e3] - coeffs[idx_e0]) * b; + } + } + } + grad[0] = T(n) * g0; + grad[1] = T(n) * g1; + grad[2] = T(n) * g2; + } +} + +// --------------------------------------------------------------------------- +// Tensor-product Bernstein gradient +// --------------------------------------------------------------------------- + +template +void gradient_tensor(int tdim, int degree, const T* coeffs, const T* xi, T* grad) +{ + const int n = degree; + const int n1 = n + 1; + + for (int d = 0; d < tdim; ++d) + grad[d] = T(0); + + if (tdim == 1) + { + // Same as simplex 1D gradient + if (n == 0) + return; + std::vector bm1(static_cast(n)); + bernstein_1d_all(n - 1, xi[0], bm1.data()); + + T g = T(0); + for (int i = 0; i <= n; ++i) + { + T bim1 = (i > 0) ? bm1[static_cast(i - 1)] : T(0); + T bi = (i < n) ? bm1[static_cast(i)] : T(0); + g += coeffs[i] * T(n) * (bim1 - bi); + } + grad[0] = g; + } + else if (tdim == 2) + { + std::vector Bx(static_cast(n1)), + By(static_cast(n1)); + std::vector dBx(static_cast(n1)), + dBy(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + bernstein_1d_deriv(n, xi[0], dBx.data()); + bernstein_1d_deriv(n, xi[1], dBy.data()); + + T g0 = T(0), g1 = T(0); + for (int i = 0; i <= n; ++i) + { + for (int j = 0; j <= n; ++j) + { + T c = coeffs[i * n1 + j]; + g0 += c * dBx[static_cast(i)] + * By[static_cast(j)]; + g1 += c * Bx[static_cast(i)] + * dBy[static_cast(j)]; + } + } + grad[0] = g0; + grad[1] = g1; + } + else if (tdim == 3) + { + std::vector Bx(static_cast(n1)), + By(static_cast(n1)), + Bz(static_cast(n1)); + std::vector dBx(static_cast(n1)), + dBy(static_cast(n1)), + dBz(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + bernstein_1d_all(n, xi[2], Bz.data()); + bernstein_1d_deriv(n, xi[0], dBx.data()); + bernstein_1d_deriv(n, xi[1], dBy.data()); + bernstein_1d_deriv(n, xi[2], dBz.data()); + + T g0 = T(0), g1 = T(0), g2 = T(0); + for (int i = 0; i <= n; ++i) + { + for (int j = 0; j <= n; ++j) + { + for (int k = 0; k <= n; ++k) + { + T c = coeffs[(i * n1 + j) * n1 + k]; + T bx = Bx[static_cast(i)]; + T by = By[static_cast(j)]; + T bz = Bz[static_cast(k)]; + g0 += c * dBx[static_cast(i)] * by * bz; + g1 += c * bx * dBy[static_cast(j)] * bz; + g2 += c * bx * by * dBz[static_cast(k)]; + } + } + } + grad[0] = g0; + grad[1] = g1; + grad[2] = g2; + } +} + +// --------------------------------------------------------------------------- +// Simplex evaluate_all_basis +// --------------------------------------------------------------------------- + +template +void evaluate_all_simplex(int tdim, int degree, const T* xi, T* out) +{ + const int n = degree; + + T lambda[4]; + T sum = T(0); + for (int d = 0; d < tdim; ++d) + sum += xi[d]; + lambda[0] = T(1) - sum; + for (int d = 0; d < tdim; ++d) + lambda[d + 1] = xi[d]; + + int idx = 0; + + if (tdim == 1) + { + for (int i = 0; i <= n; ++i) + { + int alpha[2] = {n - i, i}; + out[idx++] = multinomial(n, alpha, 2) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]); + } + } + else if (tdim == 2) + { + for (int j = 0; j <= n; ++j) + { + for (int i = 0; i <= n - j; ++i) + { + int alpha[3] = {n - i - j, i, j}; + out[idx++] = multinomial(n, alpha, 3) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]); + } + } + } + else if (tdim == 3) + { + for (int k = 0; k <= n; ++k) + { + for (int j = 0; j <= n - k; ++j) + { + for (int i = 0; i <= n - k - j; ++i) + { + int alpha[4] = {n - i - j - k, i, j, k}; + out[idx++] = multinomial(n, alpha, 4) + * integer_pow(lambda[0], alpha[0]) + * integer_pow(lambda[1], alpha[1]) + * integer_pow(lambda[2], alpha[2]) + * integer_pow(lambda[3], alpha[3]); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Tensor-product evaluate_all_basis +// --------------------------------------------------------------------------- + +template +void evaluate_all_tensor(int tdim, int degree, const T* xi, T* out) +{ + const int n = degree; + const int n1 = n + 1; + + if (tdim == 1) + { + bernstein_1d_all(n, xi[0], out); + } + else if (tdim == 2) + { + std::vector Bx(static_cast(n1)), + By(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + + int idx = 0; + for (int i = 0; i <= n; ++i) + for (int j = 0; j <= n; ++j) + out[idx++] = Bx[static_cast(i)] + * By[static_cast(j)]; + } + else if (tdim == 3) + { + std::vector Bx(static_cast(n1)), + By(static_cast(n1)), + Bz(static_cast(n1)); + bernstein_1d_all(n, xi[0], Bx.data()); + bernstein_1d_all(n, xi[1], By.data()); + bernstein_1d_all(n, xi[2], Bz.data()); + + int idx = 0; + for (int i = 0; i <= n; ++i) + for (int j = 0; j <= n; ++j) + for (int k = 0; k <= n; ++k) + out[idx++] = Bx[static_cast(i)] + * By[static_cast(j)] + * Bz[static_cast(k)]; + } +} + +} // anonymous namespace + +// =========================================================================== +// Public API +// =========================================================================== + +int num_polynomials(cell::type ctype, int degree) +{ + const int n = degree; + switch (ctype) + { + case cell::type::interval: + return n + 1; + case cell::type::triangle: + return (n + 1) * (n + 2) / 2; + case cell::type::tetrahedron: + return (n + 1) * (n + 2) * (n + 3) / 6; + case cell::type::quadrilateral: + return (n + 1) * (n + 1); + case cell::type::hexahedron: + return (n + 1) * (n + 1) * (n + 1); + default: + throw std::invalid_argument( + "bernstein::num_polynomials: unsupported cell type"); + } +} + +// --- evaluate --- + +template +T evaluate(cell::type ctype, int degree, + std::span coeffs, std::span xi) +{ + if (is_simplex(ctype)) + return evaluate_simplex(cell::get_tdim(ctype), degree, + coeffs.data(), xi.data()); + else if (is_tensor_product(ctype)) + return evaluate_tensor(cell::get_tdim(ctype), degree, + coeffs.data(), xi.data()); + else + throw std::invalid_argument("bernstein::evaluate: unsupported cell type"); +} + +template double evaluate(cell::type, int, std::span, std::span); +template float evaluate(cell::type, int, std::span, std::span); + +// --- gradient --- + +template +void gradient(cell::type ctype, int degree, + std::span coeffs, std::span xi, + std::span grad) +{ + if (is_simplex(ctype)) + gradient_simplex(cell::get_tdim(ctype), degree, + coeffs.data(), xi.data(), grad.data()); + else if (is_tensor_product(ctype)) + gradient_tensor(cell::get_tdim(ctype), degree, + coeffs.data(), xi.data(), grad.data()); + else + throw std::invalid_argument("bernstein::gradient: unsupported cell type"); +} + +template void gradient(cell::type, int, std::span, std::span, std::span); +template void gradient(cell::type, int, std::span, std::span, std::span); + +// --- evaluate_basis --- + +template +void evaluate_basis(cell::type ctype, int degree, + std::span xi, std::span out) +{ + if (is_simplex(ctype)) + evaluate_all_simplex(cell::get_tdim(ctype), degree, + xi.data(), out.data()); + else if (is_tensor_product(ctype)) + evaluate_all_tensor(cell::get_tdim(ctype), degree, + xi.data(), out.data()); + else + throw std::invalid_argument("bernstein::evaluate_basis: unsupported cell type"); +} + +template void evaluate_basis(cell::type, int, std::span, std::span); +template void evaluate_basis(cell::type, int, std::span, std::span); + +// --- lagrange_to_bernstein --- + +template +void lagrange_to_bernstein(cell::type ctype, int degree, + std::span ref_points, + std::span nodal_values, + std::vector& coeffs) +{ + const int N = num_polynomials(ctype, degree); + const int tdim = cell::get_tdim(ctype); + const int ndofs = static_cast(nodal_values.size()); + + if (ndofs != N) + throw std::invalid_argument( + "bernstein::lagrange_to_bernstein: nodal_values size (" + + std::to_string(ndofs) + + ") does not match num_polynomials (" + + std::to_string(N) + ")"); + + if (static_cast(ref_points.size()) != ndofs * tdim) + throw std::invalid_argument( + "bernstein::lagrange_to_bernstein: ref_points size mismatch"); + + // Build the Bernstein evaluation matrix V: V[i][j] = B_j(ref_point_i) + std::vector V(static_cast(N) * static_cast(N)); + std::vector basis_row(static_cast(N)); + + for (int i = 0; i < N; ++i) + { + const T* pt = ref_points.data() + static_cast(i) * static_cast(tdim); + std::span pt_span(pt, static_cast(tdim)); + std::span row_span(basis_row); + + evaluate_basis(ctype, degree, pt_span, row_span); + + for (int j = 0; j < N; ++j) + V[static_cast(i) * static_cast(N) + + static_cast(j)] = basis_row[static_cast(j)]; + } + + // Solve V * coeffs = nodal_values + coeffs.assign(nodal_values.begin(), nodal_values.end()); + solve_dense(N, V, coeffs); +} + +template void lagrange_to_bernstein(cell::type, int, + std::span, std::span, + std::vector&); +template void lagrange_to_bernstein(cell::type, int, + std::span, std::span, + std::vector&); + +// --- derivative_coefficients --- + +template +void derivative_coefficients(cell::type ctype, int degree, + std::span coeffs, + int direction, + std::vector& deriv_coeffs) +{ + const int n = degree; + if (n <= 0) + { + deriv_coeffs.clear(); + return; + } + + const int tdim = cell::get_tdim(ctype); + if (direction < 0 || direction >= tdim) + throw std::invalid_argument( + "derivative_coefficients: direction out of range"); + + if (is_simplex(ctype)) + { + // Derivative of degree-n simplex Bernstein → degree-(n-1) expansion. + // c'_beta = n * (c_{beta + e_{dir+1}} - c_{beta + e_0}) + // where beta ranges over multi-indices with |beta| = n-1. + const int nm1 = n - 1; + const int n_out = num_polynomials(ctype, nm1); + deriv_coeffs.resize(static_cast(n_out)); + + if (tdim == 1) + { + // df/dx: c'_i = n * (c_{i+1} - c_i), i = 0..n-1 + for (int i = 0; i <= nm1; ++i) + deriv_coeffs[static_cast(i)] = + T(n) * (coeffs[static_cast(i + 1)] + - coeffs[static_cast(i)]); + } + else if (tdim == 2) + { + int idx = 0; + for (int j = 0; j <= nm1; ++j) + { + for (int i = 0; i <= nm1 - j; ++i, ++idx) + { + int idx_e0 = simplex_index_2d(i, j, n); + int idx_ek; + if (direction == 0) + idx_ek = simplex_index_2d(i + 1, j, n); + else + idx_ek = simplex_index_2d(i, j + 1, n); + + deriv_coeffs[static_cast(idx)] = + T(n) * (coeffs[static_cast(idx_ek)] + - coeffs[static_cast(idx_e0)]); + } + } + } + else if (tdim == 3) + { + int idx = 0; + for (int k = 0; k <= nm1; ++k) + { + for (int j = 0; j <= nm1 - k; ++j) + { + for (int i = 0; i <= nm1 - k - j; ++i, ++idx) + { + int idx_e0 = simplex_index_3d(i, j, k, n); + int idx_ek; + if (direction == 0) + idx_ek = simplex_index_3d(i + 1, j, k, n); + else if (direction == 1) + idx_ek = simplex_index_3d(i, j + 1, k, n); + else + idx_ek = simplex_index_3d(i, j, k + 1, n); + + deriv_coeffs[static_cast(idx)] = + T(n) * (coeffs[static_cast(idx_ek)] + - coeffs[static_cast(idx_e0)]); + } + } + } + } + } + else if (is_tensor_product(ctype)) + { + // Tensor-product: derivative in direction k reduces degree from n to n-1 + // in that direction; degree stays n in other directions. + const int nm1 = n - 1; + const int n1 = n + 1; + + if (tdim == 1) + { + deriv_coeffs.resize(static_cast(n)); + for (int i = 0; i < n; ++i) + deriv_coeffs[static_cast(i)] = + T(n) * (coeffs[static_cast(i + 1)] + - coeffs[static_cast(i)]); + } + else if (tdim == 2) + { + // Index: coeffs[ix * n1 + iy] + if (direction == 0) + { + // d/dx: degree (n-1) in x, n in y → n * (n+1) entries + deriv_coeffs.resize(static_cast(n * n1)); + for (int ix = 0; ix < n; ++ix) + for (int iy = 0; iy <= n; ++iy) + deriv_coeffs[static_cast(ix * n1 + iy)] = + T(n) * (coeffs[static_cast((ix + 1) * n1 + iy)] + - coeffs[static_cast(ix * n1 + iy)]); + } + else + { + // d/dy: degree n in x, (n-1) in y → (n+1) * n entries + deriv_coeffs.resize(static_cast(n1 * n)); + for (int ix = 0; ix <= n; ++ix) + for (int iy = 0; iy < n; ++iy) + deriv_coeffs[static_cast(ix * n + iy)] = + T(n) * (coeffs[static_cast(ix * n1 + iy + 1)] + - coeffs[static_cast(ix * n1 + iy)]); + } + } + else if (tdim == 3) + { + // Index: coeffs[(ix * n1 + iy) * n1 + iz] + if (direction == 0) + { + deriv_coeffs.resize(static_cast(n * n1 * n1)); + for (int ix = 0; ix < n; ++ix) + for (int iy = 0; iy <= n; ++iy) + for (int iz = 0; iz <= n; ++iz) + deriv_coeffs[static_cast((ix * n1 + iy) * n1 + iz)] = + T(n) * (coeffs[static_cast(((ix + 1) * n1 + iy) * n1 + iz)] + - coeffs[static_cast((ix * n1 + iy) * n1 + iz)]); + } + else if (direction == 1) + { + deriv_coeffs.resize(static_cast(n1 * n * n1)); + for (int ix = 0; ix <= n; ++ix) + for (int iy = 0; iy < n; ++iy) + for (int iz = 0; iz <= n; ++iz) + deriv_coeffs[static_cast((ix * n + iy) * n1 + iz)] = + T(n) * (coeffs[static_cast((ix * n1 + iy + 1) * n1 + iz)] + - coeffs[static_cast((ix * n1 + iy) * n1 + iz)]); + } + else + { + deriv_coeffs.resize(static_cast(n1 * n1 * n)); + for (int ix = 0; ix <= n; ++ix) + for (int iy = 0; iy <= n; ++iy) + for (int iz = 0; iz < n; ++iz) + deriv_coeffs[static_cast((ix * n1 + iy) * n + iz)] = + T(n) * (coeffs[static_cast((ix * n1 + iy) * n1 + iz + 1)] + - coeffs[static_cast((ix * n1 + iy) * n1 + iz)]); + } + } + } + else + { + throw std::invalid_argument( + "derivative_coefficients: unsupported cell type"); + } +} + +template void derivative_coefficients(cell::type, int, + std::span, int, + std::vector&); +template void derivative_coefficients(cell::type, int, + std::span, int, + std::vector&); + +} // namespace cutcells::bernstein diff --git a/cpp/src/bernstein.h b/cpp/src/bernstein.h new file mode 100644 index 0000000..b688b49 --- /dev/null +++ b/cpp/src/bernstein.h @@ -0,0 +1,115 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "cell_types.h" + +namespace cutcells::bernstein +{ + +/// Number of Bernstein basis functions for a given cell type and degree. +/// +/// Simplex cells (interval, triangle, tetrahedron): C(n+d, d) +/// Tensor-product cells (quadrilateral, hexahedron): (n+1)^d +int num_polynomials(cell::type ctype, int degree); + +/// Evaluate a Bernstein expansion at a point xi in reference coordinates. +/// +/// f(xi) = sum_i coeffs[i] * B_i(xi) +/// +/// @param ctype Cell type +/// @param degree Polynomial degree +/// @param coeffs Bernstein coefficients (size = num_polynomials(ctype, degree)) +/// @param xi Point in reference coordinates (size = tdim) +/// @return Value of the expansion at xi +template +T evaluate(cell::type ctype, int degree, + std::span coeffs, std::span xi); + +/// Gradient of a Bernstein expansion at xi in reference coordinates. +/// +/// grad[k] = d/d(xi_k) sum_i coeffs[i] * B_i(xi) +/// +/// @param ctype Cell type +/// @param degree Polynomial degree +/// @param coeffs Bernstein coefficients (size = num_polynomials(ctype, degree)) +/// @param xi Point in reference coordinates (size = tdim) +/// @param[out] grad Gradient vector (size = tdim) +template +void gradient(cell::type ctype, int degree, + std::span coeffs, std::span xi, + std::span grad); + +/// Evaluate all N basis functions at a point. +/// +/// out[i] = B_i(xi) for i = 0, ..., N-1 +/// +/// @param ctype Cell type +/// @param degree Polynomial degree +/// @param xi Point in reference coordinates (size = tdim) +/// @param[out] out Basis function values (size = num_polynomials(ctype, degree)) +template +void evaluate_basis(cell::type ctype, int degree, + std::span xi, std::span out); + +/// Convert Lagrange nodal values to Bernstein coefficients. +/// +/// Builds the Bernstein evaluation matrix V (V_{ij} = B_j(ref_point_i)), +/// then solves V * coeffs = nodal_values. +/// +/// @param ctype Cell type +/// @param degree Polynomial degree +/// @param ref_points Reference coordinates of Lagrange nodes (flat: ndofs * tdim) +/// @param nodal_values Function values at Lagrange nodes (ndofs) +/// @param[out] coeffs Bernstein coefficients (resized to num_polynomials) +template +void lagrange_to_bernstein(cell::type ctype, int degree, + std::span ref_points, + std::span nodal_values, + std::vector& coeffs); + +/// Compute Bernstein coefficients of the partial derivative d/d(xi_direction) +/// of a degree-n polynomial on a simplex or tensor-product cell. +/// +/// For simplex cells the result is a degree-(n-1) Bernstein expansion with +/// coefficients c'_beta = n * (c_{beta + e_{dir+1}} - c_{beta + e_0}). +/// +/// For tensor-product cells the result is a degree-(n-1) expansion in the +/// differentiated direction and degree-n in the others. +/// +/// @param ctype Cell type (interval, triangle, tetrahedron, quad, hex). +/// @param degree Polynomial degree of the input. +/// @param coeffs Bernstein coefficients (size = num_polynomials(ctype, degree)). +/// @param direction Coordinate direction 0..tdim-1. +/// @param[out] deriv_coeffs Derivative Bernstein coefficients. +template +void derivative_coefficients(cell::type ctype, int degree, + std::span coeffs, + int direction, + std::vector& deriv_coeffs); + +// ---- Helper utilities exposed for testing ---- + +/// True for interval, triangle, tetrahedron. +inline bool is_simplex(cell::type ct) +{ + return ct == cell::type::interval + || ct == cell::type::triangle + || ct == cell::type::tetrahedron; +} + +/// True for quadrilateral, hexahedron. +inline bool is_tensor_product(cell::type ct) +{ + return ct == cell::type::quadrilateral + || ct == cell::type::hexahedron; +} + +} // namespace cutcells::bernstein diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp new file mode 100644 index 0000000..ea9f4d6 --- /dev/null +++ b/cpp/src/cell_certification.cpp @@ -0,0 +1,1209 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "cell_certification.h" +#include "bernstein.h" +#include "cell_topology.h" +#include "cut_cell.h" +#include "cut_tetrahedron.h" +#include "cut_triangle.h" +#include "edge_certification.h" +#include "reference_cell.h" +#include "refine_cell.h" + +#include +#include +#include +#include +#include +#include + +namespace cutcells +{ +namespace +{ + +template +void set_vertex_sign_for_level_set(AdaptCell& adapt_cell, + int vertex_id, + int level_set_id, + T value, + T zero_tol) +{ + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + auto& zero_mask = adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)]; + auto& negative_mask = adapt_cell.negative_mask_per_vertex[static_cast(vertex_id)]; + + zero_mask &= ~bit; + negative_mask &= ~bit; + + if (std::fabs(value) <= zero_tol) + zero_mask |= bit; + else if (value < T(0)) + negative_mask |= bit; +} + +template +std::map, int> build_leaf_edge_lookup(const AdaptCell& adapt_cell) +{ + std::map, int> edge_lookup; + const int n_edges = adapt_cell.n_entities(1); + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + edge_lookup[{std::min(static_cast(ev[0]), static_cast(ev[1])), + std::max(static_cast(ev[0]), static_cast(ev[1]))}] = e; + } + return edge_lookup; +} + +void append_top_cell_local(std::vector& types, + EntityAdjacency& adj, + cell::type ctype, + std::span verts) +{ + types.push_back(ctype); + for (int v : verts) + adj.indices.push_back(static_cast(v)); + adj.offsets.push_back(static_cast(adj.indices.size())); +} + +template +std::vector gather_leaf_cell_vertex_level_set_values( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int cell_id) +{ + const int tdim = adapt_cell.tdim; + auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; + std::vector values(cell_verts.size(), T(0)); + + for (std::size_t j = 0; j < cell_verts.size(); ++j) + { + const int gv = static_cast(cell_verts[j]); + std::span xi( + adapt_cell.vertex_coords.data() + + static_cast(gv) * static_cast(tdim), + static_cast(tdim)); + values[j] = ls_cell.value(xi); + } + + return values; +} + +template +int append_vertex_with_parent_info(AdaptCell& adapt_cell, + std::span coords, + int parent_dim, + int parent_id, + std::span parent_param, + int source_edge_id) +{ + const int new_v = adapt_cell.n_vertices(); + adapt_cell.vertex_coords.insert(adapt_cell.vertex_coords.end(), + coords.begin(), coords.end()); + adapt_cell.vertex_parent_dim.push_back(static_cast(parent_dim)); + adapt_cell.vertex_parent_id.push_back(parent_id); + const std::int32_t param_offset = + static_cast(adapt_cell.vertex_parent_param.size()); + adapt_cell.vertex_parent_param.insert(adapt_cell.vertex_parent_param.end(), + parent_param.begin(), + parent_param.end()); + adapt_cell.vertex_parent_param_offset.push_back( + param_offset + static_cast(parent_param.size())); + adapt_cell.zero_mask_per_vertex.push_back(0); + adapt_cell.negative_mask_per_vertex.push_back(0); + adapt_cell.vertex_source_edge_id.push_back(source_edge_id); + return new_v; +} + +template +void gather_adapt_edge_bernstein(const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int edge_id, + std::vector& edge_coeffs) +{ + int parent_edge_id = -1; + if (edge_is_on_single_parent_edge(adapt_cell, edge_id, parent_edge_id)) + { + extract_parent_edge_bernstein( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + parent_edge_id, edge_coeffs); + return; + } + + auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + const int tdim = adapt_cell.tdim; + std::span xi_a( + adapt_cell.vertex_coords.data() + + static_cast(verts[0]) * static_cast(tdim), + static_cast(tdim)); + std::span xi_b( + adapt_cell.vertex_coords.data() + + static_cast(verts[1]) * static_cast(tdim), + static_cast(tdim)); + + restrict_edge_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + xi_a, xi_b, edge_coeffs); +} + +/// Returns true if the Bernstein expansion has a sign-definite partial +/// derivative in at least one reference direction on the subcell. +/// A monotone function with no boundary root cannot have an interior zero. +template +bool has_monotone_direction(cell::type subcell_type, int degree, + std::span subcell_coeffs, T sign_tol) +{ + const int tdim = cell::get_tdim(subcell_type); + std::vector deriv_coeffs; + for (int dir = 0; dir < tdim; ++dir) + { + bernstein::derivative_coefficients(subcell_type, degree, + subcell_coeffs, dir, deriv_coeffs); + if (deriv_coeffs.empty()) + continue; + std::span dc(deriv_coeffs); + if (bernstein_all_positive(dc, sign_tol) + || bernstein_all_negative(dc, sign_tol)) + { + return true; + } + } + return false; +} + +/// Check whether any edge incident to cell_id has a root (one_root, multiple_roots, or zero). +template +bool cell_has_any_edge_root(const AdaptCell& adapt_cell, + int level_set_id, int cell_id) +{ + const int tdim = adapt_cell.tdim; + auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; + const int n_edges = adapt_cell.n_entities(1); + + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + bool v0_in = false, v1_in = false; + for (auto cv : cell_verts) + { + if (cv == ev[0]) v0_in = true; + if (cv == ev[1]) v1_in = true; + } + if (!v0_in || !v1_in) + continue; + + const EdgeRootTag etag = adapt_cell.get_edge_root_tag(level_set_id, e); + if (etag == EdgeRootTag::one_root + || etag == EdgeRootTag::multiple_roots + || etag == EdgeRootTag::zero) + { + return true; + } + } + return false; +} + +template +bool vertex_parameter_on_parent_edge(const AdaptCell& adapt_cell, + int vertex_id, + int parent_edge_id, + T& t) +{ + const int dim = adapt_cell.vertex_parent_dim[static_cast(vertex_id)]; + if (dim == 0) + { + const auto parent_edge = + cell::edges(adapt_cell.parent_cell_type)[static_cast(parent_edge_id)]; + if (parent_edge[0] == vertex_id) + { + t = T(0); + return true; + } + if (parent_edge[1] == vertex_id) + { + t = T(1); + return true; + } + return false; + } + + if (dim != 1) + return false; + if (adapt_cell.vertex_parent_id[static_cast(vertex_id)] != parent_edge_id) + return false; + + const auto begin = + static_cast(adapt_cell.vertex_parent_param_offset[static_cast(vertex_id)]); + const auto end = + static_cast(adapt_cell.vertex_parent_param_offset[static_cast(vertex_id + 1)]); + if (end - begin != 1) + return false; + + t = adapt_cell.vertex_parent_param[begin]; + return true; +} + +template +int ensure_one_root_vertex_on_edge(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int edge_id, + T zero_tol, + T sign_tol, + int edge_max_depth) +{ + const int n_edges = adapt_cell.n_entities(1); + if (adapt_cell.edge_one_root_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + { + adapt_cell.resize_one_root_data(level_set_id + 1); + } + + const auto idx = static_cast(level_set_id * n_edges + edge_id); + if (adapt_cell.edge_one_root_vertex_id[idx] >= 0) + return adapt_cell.edge_one_root_vertex_id[idx]; + + if (adapt_cell.get_edge_root_tag(level_set_id, edge_id) != EdgeRootTag::one_root) + return -1; + + std::vector edge_coeffs; + gather_adapt_edge_bernstein(adapt_cell, ls_cell, edge_id, edge_coeffs); + + T root_t = T(0); + if (!locate_one_root_parameter(std::span(edge_coeffs), + zero_tol, sign_tol, edge_max_depth, root_t)) + { + throw std::runtime_error( + "ensure_one_root_vertex_on_edge: failed to localize unique edge root"); + } + + auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + const int v0 = static_cast(verts[0]); + const int v1 = static_cast(verts[1]); + const int tdim = adapt_cell.tdim; + std::vector coords(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + { + const T x0 = adapt_cell.vertex_coords[static_cast(v0 * tdim + d)]; + const T x1 = adapt_cell.vertex_coords[static_cast(v1 * tdim + d)]; + coords[static_cast(d)] = (T(1) - root_t) * x0 + root_t * x1; + } + + int parent_dim = adapt_cell.tdim; + int parent_id = adapt_cell.parent_cell_id; + std::vector parent_param(coords.begin(), coords.end()); + + int parent_edge_id = -1; + T parent_t0 = T(0); + T parent_t1 = T(1); + if (edge_is_on_single_parent_edge(adapt_cell, edge_id, parent_edge_id) + && vertex_parameter_on_parent_edge(adapt_cell, v0, parent_edge_id, parent_t0) + && vertex_parameter_on_parent_edge(adapt_cell, v1, parent_edge_id, parent_t1)) + { + parent_dim = 1; + parent_id = parent_edge_id; + parent_param.assign( + 1, (T(1) - root_t) * parent_t0 + root_t * parent_t1); + } + + const int vertex_id = append_vertex_with_parent_info( + adapt_cell, + std::span(coords), + parent_dim, + parent_id, + std::span(parent_param), + edge_id); + + set_vertex_sign_for_level_set(adapt_cell, vertex_id, level_set_id, T(0), zero_tol); + adapt_cell.edge_one_root_param[idx] = root_t; + adapt_cell.edge_one_root_vertex_id[idx] = vertex_id; + adapt_cell.edge_one_root_has_value[idx] = 1; + return vertex_id; +} + +template +void append_ready_cut_part_cells( + AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int old_cell_id, + const cell::CutCell& part, + cell::type leaf_cell_type, + std::span old_cell_vertices, + std::span old_edge_ids_by_local_edge, + std::vector& new_types, + EntityAdjacency& new_cells, + std::vector& old_cell_ids_for_new_cells, + std::vector& explicit_current_ls_tags, + std::map& token_to_vertex, + T zero_tol, + CellCertTag side_tag) +{ + const int parent_tdim = adapt_cell.tdim; + + for (std::size_t lv = 0; lv < part._vertex_parent_entity.size(); ++lv) + { + const int raw_token = part._vertex_parent_entity[lv]; + const int token = cell::vtk_parent_entity_token_to_basix(leaf_cell_type, raw_token); + if (token_to_vertex.contains(token)) + continue; + + int vertex_id = -1; + if (token >= 100 && token < 200) + { + const int local_vid = token - 100; + vertex_id = old_cell_vertices[static_cast(local_vid)]; + } + else + { + std::span x( + part._vertex_coords.data() + + lv * static_cast(parent_tdim), + static_cast(parent_tdim)); + + int parent_dim = parent_tdim; + int parent_id = -1; + + if (token >= 0 && token < 100) + { + const int local_edge_id = token; + if (local_edge_id >= 0 + && local_edge_id < static_cast(old_edge_ids_by_local_edge.size())) + { + const int old_edge_id = old_edge_ids_by_local_edge[static_cast(local_edge_id)]; + int parent_edge_id = -1; + if (old_edge_id >= 0 + && edge_is_on_single_parent_edge(adapt_cell, old_edge_id, parent_edge_id)) + { + parent_dim = 1; + parent_id = parent_edge_id; + } + } + } + + vertex_id = append_vertex_with_parent_info( + adapt_cell, x, parent_dim, parent_id, + std::span(), /*source_edge_id=*/-1); + + const T value = ls_cell.value(x); + set_vertex_sign_for_level_set( + adapt_cell, vertex_id, level_set_id, value, zero_tol); + } + + token_to_vertex[token] = vertex_id; + } + + const int n_part_cells = cell::num_cells(part); + for (int pc = 0; pc < n_part_cells; ++pc) + { + auto part_vertices = cell::cell_vertices(part, pc); + std::vector mapped(part_vertices.size(), -1); + for (std::size_t j = 0; j < part_vertices.size(); ++j) + { + const int part_lv = part_vertices[j]; + const int raw_token = part._vertex_parent_entity[static_cast(part_lv)]; + const int token = cell::vtk_parent_entity_token_to_basix(leaf_cell_type, raw_token); + mapped[j] = token_to_vertex.at(token); + } + + append_top_cell_local(new_types, new_cells, + part._types[static_cast(pc)], + std::span(mapped)); + old_cell_ids_for_new_cells.push_back(-1); + explicit_current_ls_tags.push_back(side_tag); + } +} + +template +CellCertTag classify_ready_to_cut_topology(const AdaptCell& adapt_cell, + int level_set_id, + int cell_id) +{ + const int tdim = adapt_cell.tdim; + auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; + const cell::type subcell_type = adapt_cell.entity_types[tdim][static_cast(cell_id)]; + const int n_edges = adapt_cell.n_entities(1); + + int num_one_root_edges = 0; + bool has_multiple_roots = false; + bool has_zero_edge = false; + + for (int e = 0; e < n_edges; ++e) + { + auto edge_verts = adapt_cell.entity_to_vertex[1][static_cast(e)]; + bool v0_in = false; + bool v1_in = false; + for (auto cv : cell_verts) + { + if (cv == edge_verts[0]) v0_in = true; + if (cv == edge_verts[1]) v1_in = true; + } + if (!v0_in || !v1_in) + continue; + + const EdgeRootTag etag = adapt_cell.get_edge_root_tag(level_set_id, e); + if (etag == EdgeRootTag::one_root) + ++num_one_root_edges; + else if (etag == EdgeRootTag::multiple_roots) + has_multiple_roots = true; + else if (etag == EdgeRootTag::zero) + has_zero_edge = true; + } + + if (has_multiple_roots || has_zero_edge) + return CellCertTag::cut; + + if (subcell_type == cell::type::triangle && num_one_root_edges == 2) + return CellCertTag::ready_to_cut; + + if (subcell_type == cell::type::tetrahedron + && (num_one_root_edges == 3 || num_one_root_edges == 4)) + { + return CellCertTag::ready_to_cut; + } + + if (num_one_root_edges > 0) + return CellCertTag::cut; + + return CellCertTag::not_classified; +} + +} // anonymous namespace + +// ===================================================================== +// restrict_subcell_bernstein_exact +// ===================================================================== + +template +void restrict_subcell_bernstein_exact(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + cell::type subcell_type, + std::span subcell_vertex_coords, + std::vector& subcell_coeffs) +{ + // Strategy: generate reference points in the subcell, map them to the + // parent reference frame using the subcell vertex coordinates as an + // affine map, evaluate the parent Bernstein at each mapped point, + // then convert from nodal values to Bernstein on the subcell. + + const int subcell_tdim = cell::get_tdim(subcell_type); + const int parent_tdim = cell::get_tdim(parent_cell_type); + const int n_dofs = bernstein::num_polynomials(subcell_type, degree); + + // Number of subcell vertices. + int n_subcell_verts = 0; + switch (subcell_type) + { + case cell::type::interval: n_subcell_verts = 2; break; + case cell::type::triangle: n_subcell_verts = 3; break; + case cell::type::tetrahedron: n_subcell_verts = 4; break; + case cell::type::quadrilateral: n_subcell_verts = 4; break; + case cell::type::hexahedron: n_subcell_verts = 8; break; + default: + throw std::runtime_error( + "restrict_subcell_bernstein_exact: unsupported subcell type"); + } + + // Generate equispaced reference points on the subcell. + // For simplices: use lattice of order = degree. + // For tensor-product: use tensor-product grid of order = degree. + // These serve as Lagrange interpolation nodes. + std::vector ref_points_flat(static_cast(n_dofs * subcell_tdim)); + + if (bernstein::is_simplex(subcell_type)) + { + // Equispaced lattice on the reference simplex. + int idx = 0; + if (subcell_tdim == 1) + { + for (int i = 0; i <= degree; ++i, ++idx) + ref_points_flat[static_cast(idx)] = + (degree > 0) ? T(i) / T(degree) : T(0); + } + else if (subcell_tdim == 2) + { + for (int j = 0; j <= degree; ++j) + for (int i = 0; i <= degree - j; ++i, ++idx) + { + ref_points_flat[static_cast(idx * 2)] = + (degree > 0) ? T(i) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 2 + 1)] = + (degree > 0) ? T(j) / T(degree) : T(0); + } + } + else if (subcell_tdim == 3) + { + for (int k = 0; k <= degree; ++k) + for (int j = 0; j <= degree - k; ++j) + for (int i = 0; i <= degree - k - j; ++i, ++idx) + { + ref_points_flat[static_cast(idx * 3)] = + (degree > 0) ? T(i) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 3 + 1)] = + (degree > 0) ? T(j) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 3 + 2)] = + (degree > 0) ? T(k) / T(degree) : T(0); + } + } + } + else // tensor-product + { + int idx = 0; + if (subcell_tdim == 2) + { + for (int iy = 0; iy <= degree; ++iy) + for (int ix = 0; ix <= degree; ++ix, ++idx) + { + ref_points_flat[static_cast(idx * 2)] = + (degree > 0) ? T(ix) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 2 + 1)] = + (degree > 0) ? T(iy) / T(degree) : T(0); + } + } + else if (subcell_tdim == 3) + { + for (int iz = 0; iz <= degree; ++iz) + for (int iy = 0; iy <= degree; ++iy) + for (int ix = 0; ix <= degree; ++ix, ++idx) + { + ref_points_flat[static_cast(idx * 3)] = + (degree > 0) ? T(ix) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 3 + 1)] = + (degree > 0) ? T(iy) / T(degree) : T(0); + ref_points_flat[static_cast(idx * 3 + 2)] = + (degree > 0) ? T(iz) / T(degree) : T(0); + } + } + } + + // Map each subcell reference point to parent reference coordinates, + // then evaluate the parent polynomial. + std::vector nodal_values(static_cast(n_dofs)); + std::vector xi_parent(static_cast(parent_tdim)); + + for (int d = 0; d < n_dofs; ++d) + { + // Map subcell reference point → parent reference. + // For simplex subcells: xi_parent = (1 - sum(xi_sub)) * v0 + xi_sub[0]*v1 + xi_sub[1]*v2 + ... + // For tensor-product subcells: bilinear/trilinear map. + std::fill(xi_parent.begin(), xi_parent.end(), T(0)); + + if (bernstein::is_simplex(subcell_type)) + { + // Barycentric coordinates: lambda_0 = 1 - sum(xi), lambda_i = xi[i-1] + T lambda0 = T(1); + for (int s = 0; s < subcell_tdim; ++s) + lambda0 -= ref_points_flat[static_cast(d * subcell_tdim + s)]; + + for (int c = 0; c < parent_tdim; ++c) + xi_parent[static_cast(c)] = + lambda0 * subcell_vertex_coords[static_cast(0 * parent_tdim + c)]; + + for (int v = 1; v < n_subcell_verts; ++v) + { + T lambda = ref_points_flat[static_cast(d * subcell_tdim + (v - 1))]; + for (int c = 0; c < parent_tdim; ++c) + xi_parent[static_cast(c)] += + lambda * subcell_vertex_coords[static_cast(v * parent_tdim + c)]; + } + } + else + { + // Tensor-product: bilinear/trilinear map. + if (subcell_tdim == 2) + { + T u = ref_points_flat[static_cast(d * 2)]; + T v = ref_points_flat[static_cast(d * 2 + 1)]; + // Bilinear: (1-u)(1-v)*v0 + u(1-v)*v1 + uv*v2 + (1-u)v*v3 + T w00 = (T(1) - u) * (T(1) - v); + T w10 = u * (T(1) - v); + T w11 = u * v; + T w01 = (T(1) - u) * v; + for (int c = 0; c < parent_tdim; ++c) + xi_parent[static_cast(c)] = + w00 * subcell_vertex_coords[static_cast(0 * parent_tdim + c)] + + w10 * subcell_vertex_coords[static_cast(1 * parent_tdim + c)] + + w11 * subcell_vertex_coords[static_cast(2 * parent_tdim + c)] + + w01 * subcell_vertex_coords[static_cast(3 * parent_tdim + c)]; + } + else if (subcell_tdim == 3) + { + T u = ref_points_flat[static_cast(d * 3)]; + T v = ref_points_flat[static_cast(d * 3 + 1)]; + T w = ref_points_flat[static_cast(d * 3 + 2)]; + T weights[8] = { + (T(1)-u)*(T(1)-v)*(T(1)-w), + u *(T(1)-v)*(T(1)-w), + u * v *(T(1)-w), + (T(1)-u)* v *(T(1)-w), + (T(1)-u)*(T(1)-v)* w, + u *(T(1)-v)* w, + u * v * w, + (T(1)-u)* v * w + }; + for (int c = 0; c < parent_tdim; ++c) + { + xi_parent[static_cast(c)] = T(0); + for (int vv = 0; vv < 8; ++vv) + xi_parent[static_cast(c)] += + weights[vv] * subcell_vertex_coords[ + static_cast(vv * parent_tdim + c)]; + } + } + } + + nodal_values[static_cast(d)] = + bernstein::evaluate(parent_cell_type, degree, parent_coeffs, + std::span(xi_parent)); + } + + // Convert sampled nodal values to Bernstein on the subcell. + bernstein::lagrange_to_bernstein( + subcell_type, degree, + std::span(ref_points_flat), + std::span(nodal_values), + subcell_coeffs); +} + +// ===================================================================== +// classify_leaf_cell +// ===================================================================== + +template +CellCertTag classify_leaf_cell(const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int cell_id, + T zero_tol, T sign_tol) +{ + // A. Check the incident-edge topology first. + { + const CellCertTag edge_topology_tag = + classify_ready_to_cut_topology(adapt_cell, level_set_id, cell_id); + if (edge_topology_tag != CellCertTag::not_classified) + return edge_topology_tag; + } + + // B. No edge roots: use exact subcell Bernstein restriction. + const int tdim = adapt_cell.tdim; + auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; + // Get the subcell vertex coordinates in parent reference frame. + const int parent_tdim = adapt_cell.tdim; + const int n_cell_verts = static_cast(cell_verts.size()); + std::vector subcell_vertex_coords( + static_cast(n_cell_verts * parent_tdim)); + + for (int v = 0; v < n_cell_verts; ++v) + { + int gv = cell_verts[static_cast(v)]; + for (int d = 0; d < parent_tdim; ++d) + subcell_vertex_coords[static_cast(v * parent_tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * parent_tdim + d)]; + } + + cell::type subcell_type = adapt_cell.entity_types[tdim][static_cast(cell_id)]; + + std::vector subcell_coeffs; + restrict_subcell_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + subcell_type, + std::span(subcell_vertex_coords), + subcell_coeffs); + + // Sign-hull classification. + std::span sc(subcell_coeffs); + + if (bernstein_all_zero(sc, zero_tol)) + return CellCertTag::zero; + if (bernstein_all_positive(sc, sign_tol)) + return CellCertTag::positive; + if (bernstein_all_negative(sc, sign_tol)) + return CellCertTag::negative; + + // Mixed Bernstein signs — apply additional filters before declaring ambiguous. + + // B4a. MONOTONICITY FILTER (cheap, all dimensions): + // If phi has a sign-definite partial derivative in some direction on + // the subcell AND no incident edge has a root, then phi cannot have + // an interior zero-crossing. Use vertex evaluation to determine sign. + const bool no_edge_root = !cell_has_any_edge_root(adapt_cell, level_set_id, cell_id); + + if (no_edge_root + && has_monotone_direction(subcell_type, ls_cell.bernstein_order, sc, sign_tol)) + { + // Evaluate phi at the first vertex of the subcell. + std::span xi_v0( + adapt_cell.vertex_coords.data() + + static_cast(cell_verts[0]) * static_cast(parent_tdim), + static_cast(parent_tdim)); + const T val = bernstein::evaluate( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), xi_v0); + return (val >= T(0)) ? CellCertTag::positive : CellCertTag::negative; + } + + // B4b. FACE CERTIFICATION FILTER (3D only): + // For each face of the subcell, restrict the subcell Bernstein to + // the face and check if the face can have a root. If ALL faces are + // sign-definite and no edge has a root, the cell is sign-definite. + if (tdim == 3 && no_edge_root && subcell_type == cell::type::tetrahedron) + { + const int n_faces = cell::num_faces(subcell_type); + bool all_faces_sign_definite = true; + + for (int fi = 0; fi < n_faces; ++fi) + { + auto face_v = cell::tet_face(fi); + // Build face vertex coordinates in parent reference frame + // (using the subcell's local vertex ordering). + std::vector face_vertex_coords(static_cast(3 * parent_tdim)); + for (int fv = 0; fv < 3; ++fv) + { + const int local_v = face_v[static_cast(fv)]; + for (int d = 0; d < parent_tdim; ++d) + face_vertex_coords[static_cast(fv * parent_tdim + d)] = + subcell_vertex_coords[static_cast(local_v * parent_tdim + d)]; + } + + // Restrict the parent polynomial to the face. + std::vector face_coeffs; + restrict_subcell_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + cell::type::triangle, + std::span(face_vertex_coords), + face_coeffs); + + std::span fc(face_coeffs); + if (bernstein_all_positive(fc, sign_tol) + || bernstein_all_negative(fc, sign_tol) + || bernstein_all_zero(fc, zero_tol)) + { + continue; // This face is sign-definite or zero — no root here. + } + + // Face has mixed Bernstein signs — possible root. + all_faces_sign_definite = false; + break; + } + + if (all_faces_sign_definite) + { + // No root can enter the cell through any face; evaluate at a vertex. + std::span xi_v0( + adapt_cell.vertex_coords.data() + + static_cast(cell_verts[0]) * static_cast(parent_tdim), + static_cast(parent_tdim)); + const T val = bernstein::evaluate( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), xi_v0); + return (val >= T(0)) ? CellCertTag::positive : CellCertTag::negative; + } + } + + return CellCertTag::ambiguous; +} + +// ===================================================================== +// classify_leaf_cells +// ===================================================================== + +template +void classify_leaf_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol) +{ + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + + // Ensure tag storage. + if (adapt_cell.cell_cert_tag_num_level_sets <= level_set_id) + adapt_cell.resize_cell_cert_tags(level_set_id + 1); + + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) + != CellCertTag::not_classified) + continue; + + CellCertTag tag = classify_leaf_cell( + adapt_cell, ls_cell, level_set_id, c, zero_tol, sign_tol); + adapt_cell.set_cell_cert_tag(level_set_id, c, tag); + } +} + +// ===================================================================== +// fill_all_vertex_signs_from_level_set +// ===================================================================== + +template +void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol) +{ + const int n_vertices = adapt_cell.n_vertices(); + const int tdim = adapt_cell.tdim; + for (int v = 0; v < n_vertices; ++v) + { + std::span xi( + adapt_cell.vertex_coords.data() + + static_cast(v) * static_cast(tdim), + static_cast(tdim)); + set_vertex_sign_for_level_set( + adapt_cell, v, level_set_id, ls_cell.value(xi), zero_tol); + } +} + +// ===================================================================== +// process_ready_to_cut_cells +// ===================================================================== + +template +void process_ready_to_cut_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, + T sign_tol, + int edge_max_depth) +{ + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + + bool has_ready = false; + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) == CellCertTag::ready_to_cut) + { + has_ready = true; + break; + } + } + if (!has_ready) + return; + + const std::vector old_types = adapt_cell.entity_types[tdim]; + const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; + const auto edge_lookup = build_leaf_edge_lookup(adapt_cell); + + EntityAdjacency new_cells; + new_cells.offsets.push_back(0); + std::vector new_types; + std::vector old_cell_ids_for_new_cells; + std::vector explicit_current_ls_tags; + + for (int c = 0; c < n_cells; ++c) + { + auto old_cell_vertices = old_cells[static_cast(c)]; + const cell::type leaf_cell_type = old_types[static_cast(c)]; + + if (adapt_cell.get_cell_cert_tag(level_set_id, c) != CellCertTag::ready_to_cut) + { + std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); + append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + explicit_current_ls_tags.push_back(CellCertTag::not_classified); + continue; + } + + if (leaf_cell_type != cell::type::triangle + && leaf_cell_type != cell::type::tetrahedron) + { + throw std::runtime_error( + "process_ready_to_cut_cells: ready_to_cut only implemented for triangle and tetrahedron leaves"); + } + + std::vector vertex_coords( + static_cast(old_cell_vertices.size() * tdim), T(0)); + for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) + { + const int gv = old_cell_vertices[j]; + for (int d = 0; d < tdim; ++d) + { + vertex_coords[static_cast(j * tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * tdim + d)]; + } + } + const std::vector ls_values = + gather_leaf_cell_vertex_level_set_values(adapt_cell, ls_cell, c); + + std::array old_edge_ids_by_local_edge; + old_edge_ids_by_local_edge.fill(-1); + const auto ledges = cell::edges(leaf_cell_type); + for (std::size_t le = 0; le < ledges.size(); ++le) + { + const int a = old_cell_vertices[static_cast(ledges[le][0])]; + const int b = old_cell_vertices[static_cast(ledges[le][1])]; + old_edge_ids_by_local_edge[le] = + edge_lookup.at({std::min(a, b), std::max(a, b)}); + } + + std::map token_to_vertex; + for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) + token_to_vertex[100 + static_cast(j)] = old_cell_vertices[j]; + for (std::size_t le = 0; le < ledges.size(); ++le) + { + const int old_edge_id = old_edge_ids_by_local_edge[le]; + if (old_edge_id < 0) + continue; + if (adapt_cell.get_edge_root_tag(level_set_id, old_edge_id) + != EdgeRootTag::one_root) + { + continue; + } + + const int root_vertex_id = ensure_one_root_vertex_on_edge( + adapt_cell, ls_cell, level_set_id, old_edge_id, + zero_tol, sign_tol, edge_max_depth); + if (root_vertex_id >= 0) + token_to_vertex[static_cast(le)] = root_vertex_id; + } + + if (leaf_cell_type == cell::type::triangle) + { + cell::CutCell negative_part; + cell::CutCell positive_part; + cell::triangle::cut( + std::span(vertex_coords), + tdim, + std::span(ls_values), + "phi<0", + negative_part, + /*triangulate=*/false); + cell::triangle::cut( + std::span(vertex_coords), + tdim, + std::span(ls_values), + "phi>0", + positive_part, + /*triangulate=*/false); + + append_ready_cut_part_cells( + adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, + old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 3), + new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + token_to_vertex, zero_tol, CellCertTag::negative); + append_ready_cut_part_cells( + adapt_cell, ls_cell, level_set_id, c, positive_part, leaf_cell_type, + old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 3), + new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + token_to_vertex, zero_tol, CellCertTag::positive); + } + else + { + cell::CutCell negative_part; + cell::CutCell positive_part; + cell::tetrahedron::cut( + std::span(vertex_coords), tdim, + std::span(ls_values), "phi<0", + negative_part, /*triangulate=*/false); + cell::tetrahedron::cut( + std::span(vertex_coords), tdim, + std::span(ls_values), "phi>0", + positive_part, /*triangulate=*/false); + + append_ready_cut_part_cells( + adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, + old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 6), + new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + token_to_vertex, zero_tol, CellCertTag::negative); + append_ready_cut_part_cells( + adapt_cell, ls_cell, level_set_id, c, positive_part, leaf_cell_type, + old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 6), + new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + token_to_vertex, zero_tol, CellCertTag::positive); + } + } + + apply_topology_update_preserve_certification( + adapt_cell, std::move(new_types), std::move(new_cells), + std::span(old_cell_ids_for_new_cells)); + + const int new_num_cells = adapt_cell.n_entities(tdim); + for (int c = 0; c < new_num_cells; ++c) + { + if (explicit_current_ls_tags[static_cast(c)] == CellCertTag::not_classified) + continue; + adapt_cell.set_cell_cert_tag( + level_set_id, c, explicit_current_ls_tags[static_cast(c)]); + } + +} + +// ===================================================================== +// certify_and_refine +// ===================================================================== + +template +void certify_and_refine(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth) +{ + for (int iter = 0; iter < max_iterations; ++iter) + { + // 1. Classify edges. + classify_new_edges(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, edge_max_depth); + + // 2. Classify cells. + classify_leaf_cells(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol); + + // 3. Green refinement: multiple_roots edges. + bool did_green = false; + const int n_edges = adapt_cell.n_entities(1); + for (int e = 0; e < n_edges; ++e) + { + if (adapt_cell.get_edge_root_tag(level_set_id, e) + == EdgeRootTag::multiple_roots) + { + did_green = true; + break; + } + } + + if (did_green) + { + if (refine_green_on_multiple_root_edges(adapt_cell, level_set_id)) + continue; + } + + // 4. Red refinement: ambiguous cells. + bool did_red = false; + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) + == CellCertTag::ambiguous) + { + did_red = true; + break; + } + } + + if (did_red) + { + if (refine_red_on_ambiguous_cells(adapt_cell, level_set_id)) + continue; + } + + // 5. No refinement needed — stop. + break; + } +} + +template +void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth) +{ + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); + certify_and_refine(adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, edge_max_depth); + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); + process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, edge_max_depth); + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); +} + +// ===================================================================== +// Explicit template instantiations +// ===================================================================== + +template void restrict_subcell_bernstein_exact(cell::type, int, + std::span, + cell::type, + std::span, + std::vector&); +template void restrict_subcell_bernstein_exact(cell::type, int, + std::span, + cell::type, + std::span, + std::vector&); + +template CellCertTag classify_leaf_cell(const AdaptCell&, + const LevelSetCell&, + int, int, double, double); +template CellCertTag classify_leaf_cell(const AdaptCell&, + const LevelSetCell&, + int, int, float, float); +template CellCertTag classify_leaf_cell(const AdaptCell&, + const LevelSetCell&, + int, int, double, double); + +template void classify_leaf_cells(AdaptCell&, + const LevelSetCell&, + int, double, double); +template void classify_leaf_cells(AdaptCell&, + const LevelSetCell&, + int, float, float); +template void classify_leaf_cells(AdaptCell&, + const LevelSetCell&, + int, double, double); + +template void fill_all_vertex_signs_from_level_set(AdaptCell&, + const LevelSetCell&, + int, double); +template void fill_all_vertex_signs_from_level_set(AdaptCell&, + const LevelSetCell&, + int, float); +template void fill_all_vertex_signs_from_level_set(AdaptCell&, + const LevelSetCell&, + int, double); +template void fill_all_vertex_signs_from_level_set(AdaptCell&, + const LevelSetCell&, + int, float); + +template void process_ready_to_cut_cells(AdaptCell&, + const LevelSetCell&, + int, double, double, int); +template void process_ready_to_cut_cells(AdaptCell&, + const LevelSetCell&, + int, float, float, int); +template void process_ready_to_cut_cells(AdaptCell&, + const LevelSetCell&, + int, double, double, int); +template void process_ready_to_cut_cells(AdaptCell&, + const LevelSetCell&, + int, float, float, int); + +template void certify_and_refine(AdaptCell&, + const LevelSetCell&, + int, int, double, double, int); +template void certify_and_refine(AdaptCell&, + const LevelSetCell&, + int, int, float, float, int); +template void certify_and_refine(AdaptCell&, + const LevelSetCell&, + int, int, double, double, int); + +template void certify_refine_and_process_ready_cells(AdaptCell&, + const LevelSetCell&, + int, int, double, double, int); +template void certify_refine_and_process_ready_cells(AdaptCell&, + const LevelSetCell&, + int, int, float, float, int); +template void certify_refine_and_process_ready_cells(AdaptCell&, + const LevelSetCell&, + int, int, double, double, int); +template void certify_refine_and_process_ready_cells(AdaptCell&, + const LevelSetCell&, + int, int, float, float, int); + +} // namespace cutcells diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h new file mode 100644 index 0000000..4d07880 --- /dev/null +++ b/cpp/src/cell_certification.h @@ -0,0 +1,150 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +#include "adapt_cell.h" +#include "cell_types.h" +#include "level_set_cell.h" + +namespace cutcells +{ + +// ===================================================================== +// Exact subcell Bernstein restriction +// ===================================================================== + +/// Compute the Bernstein coefficients of a parent polynomial restricted +/// to a leaf subcell. +/// +/// The subcell is defined by its vertex reference coordinates (in the +/// parent reference frame). The restriction is exact: sample at +/// (degree+1)^tdim control points, then invert the Bernstein matrix. +/// +/// @param parent_cell_type Type of the parent cell. +/// @param degree Polynomial degree. +/// @param parent_coeffs Bernstein coefficients on the parent cell. +/// @param subcell_type Type of the subcell (triangle, tet, quad, ...). +/// @param subcell_vertex_coords Reference coordinates of subcell vertices (flat: nverts * tdim). +/// @param[out] subcell_coeffs Bernstein coefficients on the subcell. +template +void restrict_subcell_bernstein_exact(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + cell::type subcell_type, + std::span subcell_vertex_coords, + std::vector& subcell_coeffs); + +// ===================================================================== +// Cell classifier +// ===================================================================== + +/// Classify a single leaf cell for one level set. +/// +/// Logic: +/// A. If the incident edge pattern is a directly cuttable simplex case: +/// - triangle with exactly 2 one_root edges and no multiple_roots/zero +/// - tetrahedron with exactly 3 or 4 one_root edges and no multiple_roots/zero +/// → ready_to_cut. +/// B. Else if any incident edge has tag one_root, multiple_roots, or zero → cut. +/// B. Otherwise, restrict the parent Bernstein to the subcell and check +/// the sign hull: +/// - all positive → positive +/// - all negative → negative +/// - all zero → zero +/// - mixed → ambiguous +/// +/// @param adapt_cell The AdaptCell. +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set. +/// @param cell_id Index of the leaf cell in entity_to_vertex[tdim]. +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +/// @return CellCertTag. +template +CellCertTag classify_leaf_cell(const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int cell_id, + T zero_tol, T sign_tol); + +/// Classify all not-yet-classified leaf cells for one level set. +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set. +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +template +void classify_leaf_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol); + +/// Evaluate the level set on every current AdaptCell vertex and update the +/// sign/zero masks for that level set id. +template +void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol); + +/// Replace leaf cells marked ready_to_cut by the non-triangulated LUT cut +/// decomposition on the positive and negative side. +template +void process_ready_to_cut_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, + T sign_tol, + int edge_max_depth); + +// ===================================================================== +// Top-level certification + refinement driver +// ===================================================================== + +/// Iterative certification and refinement loop. +/// +/// Per iteration: +/// 1. classify_new_edges(...) +/// 2. classify_leaf_cells(...) +/// 3. If any multiple_roots edges → green refine (priority) +/// 4. Else if any ambiguous cells → red refine +/// 5. Stop if no refinement occurred. +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set. +/// @param max_iterations Maximum number of refine-reclassify iterations. +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +/// @param edge_max_depth Maximum subdivision depth for edge root search. +template +void certify_and_refine(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth); + +/// Full local single-level-set pipeline: +/// 1. stamp vertex signs +/// 2. certify + green/red refine until stable +/// 3. replace ready_to_cut cells by the LUT decomposition +/// 4. restamp vertex signs on the final leaf mesh +template +void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth); + +} // namespace cutcells diff --git a/cpp/src/cell_subdivision.h b/cpp/src/cell_subdivision.h index 2bc5056..d7d57eb 100644 --- a/cpp/src/cell_subdivision.h +++ b/cpp/src/cell_subdivision.h @@ -3,32 +3,64 @@ // This file is part of CutFEMx // // SPDX-License-Identifier: MIT +#pragma once + #include //These tables describe the subdivision of cells by inserting one node at each edge //of the cell namespace cutcells::cell { - std::array, 2> interval_subdivision_table{ - {{0,2}, - {2,1}} - }; + inline constexpr std::array, 2> interval_subdivision_table = {{ + {0, 2}, + {2, 1} + }}; - std::array, 4> triangle_subdivision_table{ - {{0,5,4}, - {5,1,3}, - {4,3,2}, - {5,3,4}} - }; + inline constexpr std::array, 4> triangle_subdivision_table = {{ + {0, 5, 4}, + {5, 1, 3}, + {4, 3, 2}, + {5, 3, 4} + }}; //Notation: 0,1,2,3,e01(9),e02(8),e03(7),e12(6),e13(5),e23(4) //where exy denotes middle point on edge between x and y //Node ordering is chosen according to basix (fenicsx) - std::array, 8> tetrahedron_subdivision_table{ - {// four tetrahedra from each vertex - {0,9,8,7},{1,9,5,6},{2,6,4,8},{3,7,4,5},{9,6,8,7},{9,5,7,8},{6,8,7,4},{6,7,5,4} - } - }; + inline constexpr std::array, 8> tetrahedron_subdivision_table = {{ + {0, 9, 8, 7}, {1, 9, 5, 6}, {2, 6, 4, 8}, {3, 7, 4, 5}, + {9, 6, 8, 7}, {9, 5, 6, 7}, {6, 8, 7, 4}, {6, 7, 5, 4} + }}; + +// Quadrilateral P2 node numbering in Basix ordering: +// 0,1,2,3 (vertices) +// e01=4, e02=5, e13=6, e23=7 (edge midpoints; Basix edge order) +// f0123=8 (cell center) +// +// Subdivision into 4 quads (each keeps hexa/quad vertex layout used in this codebase). + inline constexpr std::array, 4> quadrilateral_subdivision_table = {{ + {0, 4, 5, 8}, + {4, 1, 8, 6}, + {5, 8, 2, 7}, + {8, 6, 7, 3} + }}; + +// Hexahedron P2 node numbering in Basix ordering: +// Vertices: 0..7 +// Edge midpoints (Basix edge order in cell_topology.h): 8..19 +// Face centers (Basix face order): 20..25 +// Cell center: 26 +// +// 8-way octree-like subdivision into hexahedra. + inline constexpr std::array, 8> hexahedron_subdivision_table = {{ + {0, 8, 9, 20, 10, 21, 22, 26}, + {8, 1, 20, 11, 21, 12, 26, 23}, + {9, 20, 2, 13, 22, 26, 14, 24}, + {20, 11, 13, 3, 26, 23, 24, 15}, + {10, 21, 22, 26, 4, 16, 17, 25}, + {21, 12, 26, 23, 16, 5, 25, 18}, + {22, 26, 14, 24, 17, 25, 6, 19}, + {26, 23, 24, 15, 25, 18, 19, 7} + }}; // basix -> vtk map 0, 1, 2, 3, 9, 8, 5, 7, 6, 4 // basix: e01: 9, e12: 6, e02: 8, e03: 7, e13: 5, e23: 4 diff --git a/cpp/src/cell_topology.h b/cpp/src/cell_topology.h index 266cbff..cbccbfa 100644 --- a/cpp/src/cell_topology.h +++ b/cpp/src/cell_topology.h @@ -25,95 +25,105 @@ inline constexpr std::array, 1> interval_edges = {{ }}; /// Triangle: 3 edges -/// VTK edge order: (0,1), (1,2), (2,0) +/// Basix vertex order: 0=(0,0), 1=(1,0), 2=(0,1) +/// Basix edge order: 0=(1,2), 1=(0,2), 2=(0,1) inline constexpr std::array, 3> triangle_edges = {{ - {0, 1}, {1, 2}, {2, 0} + {1, 2}, {0, 2}, {0, 1} }}; /// Quadrilateral: 4 edges -/// VTK edge order: (0,1), (1,2), (2,3), (3,0) +/// Basix vertex order: 0=(0,0), 1=(1,0), 2=(0,1), 3=(1,1) +/// Basix edge order: 0=(0,1), 1=(0,2), 2=(1,3), 3=(2,3) /// -/// 3 --- 2 +/// 2 --- 3 /// | | /// 0 --- 1 inline constexpr std::array, 4> quadrilateral_edges = {{ - {0, 1}, {1, 2}, {2, 3}, {3, 0} + {0, 1}, {0, 2}, {1, 3}, {2, 3} }}; +/// Tetrahedron: 4 triangular faces +/// Basix face order: opposite vertex 0, 1, 2, 3 +inline constexpr std::array, 4> tetrahedron_faces = {{ + {1, 2, 3}, // face 0: opposite vertex 0 + {0, 2, 3}, // face 1: opposite vertex 1 + {0, 1, 3}, // face 2: opposite vertex 2 + {0, 1, 2} // face 3: opposite vertex 3 +}}; +inline constexpr std::array tetrahedron_face_sizes = {3, 3, 3, 3}; + /// Tetrahedron: 6 edges -/// VTK edge order: (0,1), (1,2), (2,0), (0,3), (1,3), (2,3) +/// Basix vertex order: 0=(0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(0,0,1) +/// Basix edge order: 0=(2,3), 1=(1,3), 2=(1,2), 3=(0,3), 4=(0,2), 5=(0,1) inline constexpr std::array, 6> tetrahedron_edges = {{ - {0, 1}, {1, 2}, {2, 0}, {0, 3}, {1, 3}, {2, 3} + {2, 3}, {1, 3}, {1, 2}, {0, 3}, {0, 2}, {0, 1} }}; /// Hexahedron: 12 edges -/// VTK vertex numbering: -/// 7 -------- 6 +/// Basix vertex numbering: +/// 0=(0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(1,1,0), +/// 4=(0,0,1), 5=(1,0,1), 6=(0,1,1), 7=(1,1,1) +/// +/// 6 -------- 7 /// /| /| /// / | / | /// 4 -------- 5 | -/// | 3 ------|-- 2 +/// | 2 ------|-- 3 /// | / | / /// |/ |/ /// 0 -------- 1 /// -/// VTK edge order: -/// Base: (0,1), (1,2), (2,3), (3,0) -/// Top: (4,5), (5,6), (6,7), (7,4) -/// Verticals: (0,4), (1,5), (2,6), (3,7) +/// Basix edge order: +/// Bottom face: (0,1),(0,2),(1,3),(2,3) +/// Verticals: (0,4),(1,5),(2,6),(3,7) +/// Top face: (4,5),(4,6),(5,7),(6,7) inline constexpr std::array, 12> hexahedron_edges = {{ - // Base edges (0-3) - {0, 1}, {1, 2}, {2, 3}, {3, 0}, - // Top edges (4-7) - {4, 5}, {5, 6}, {6, 7}, {7, 4}, - // Vertical edges (8-11) - {0, 4}, {1, 5}, {2, 6}, {3, 7} + // Bottom face edges (0-3) + {0, 1}, {0, 2}, {1, 3}, {2, 3}, + // Vertical edges (4-7) + {0, 4}, {1, 5}, {2, 6}, {3, 7}, + // Top face edges (8-11) + {4, 5}, {4, 6}, {5, 7}, {6, 7} }}; /// Prism (Wedge): 9 edges -/// VTK vertex numbering: -/// 2 -/// /|\ -/// / | \ -/// 0-----1 -/// | | | -/// | 5 | -/// | /|\ | -/// |/ | \| -/// 3-----4 +/// Basix vertex numbering (same as VTK for prism): +/// 0=(0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(0,0,1), 4=(1,0,1), 5=(0,1,1) /// -/// VTK edge order: -/// Bottom triangle: (0,1), (1,2), (2,0) -/// Top triangle: (3,4), (4,5), (5,3) -/// Verticals: (0,3), (1,4), (2,5) +/// Basix edge order: +/// Bottom triangle: (0,1),(0,2),(1,2) +/// Verticals: (0,3),(1,4),(2,5) +/// Top triangle: (3,4),(3,5),(4,5) inline constexpr std::array, 9> prism_edges = {{ // Bottom triangle (0-2) - {0, 1}, {1, 2}, {2, 0}, - // Top triangle (3-5) - {3, 4}, {4, 5}, {5, 3}, - // Vertical edges (6-8) - {0, 3}, {1, 4}, {2, 5} + {0, 1}, {0, 2}, {1, 2}, + // Vertical edges (3-5) + {0, 3}, {1, 4}, {2, 5}, + // Top triangle (6-8) + {3, 4}, {3, 5}, {4, 5} }}; /// Pyramid: 8 edges -/// VTK vertex numbering: +/// Basix vertex numbering: +/// 0=(0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(1,1,0), 4=(0,0,1) (apex) +/// /// 4 /// /|\ /// / | \ /// / | \ /// / | \ -/// 3----|----2 +/// 2----|----3 /// | | | /// 0---------1 /// -/// VTK edge order: -/// Base: (0,1), (1,2), (2,3), (3,0) -/// Apex connections: (0,4), (1,4), (2,4), (3,4) +/// Basix edge order: +/// Base: (0,1),(0,2),(1,3),(2,3) +/// Apex connections: (0,4),(1,4),(3,4),(2,4) inline constexpr std::array, 8> pyramid_edges = {{ // Base edges (0-3) - {0, 1}, {1, 2}, {2, 3}, {3, 0}, + {0, 1}, {0, 2}, {1, 3}, {2, 3}, // Apex edges (4-7) - {0, 4}, {1, 4}, {2, 4}, {3, 4} + {0, 4}, {1, 4}, {3, 4}, {2, 4} }}; //----------------------------------------------------------------------------- @@ -227,4 +237,25 @@ inline constexpr int num_faces(type cell_type) } } +/// Get the number of vertices per face for a cell type. +inline std::span face_sizes(type cell_type) +{ + switch (cell_type) + { + case type::tetrahedron: return std::span(tetrahedron_face_sizes); + case type::hexahedron: return std::span(hexahedron_face_sizes); + case type::prism: return std::span(prism_face_sizes); + case type::pyramid: return std::span(pyramid_face_sizes); + default: + throw std::invalid_argument("Unknown cell type in face_sizes"); + } +} + +/// Get the vertex indices for face `face_id` of a tetrahedron. +/// Returns a span of 3 vertex indices. +inline std::span tet_face(int face_id) +{ + return std::span(tetrahedron_faces[static_cast(face_id)]); +} + } // namespace cutcells::cell diff --git a/cpp/src/cell_types.h b/cpp/src/cell_types.h index 71c9450..cfbcf7b 100644 --- a/cpp/src/cell_types.h +++ b/cpp/src/cell_types.h @@ -15,7 +15,7 @@ namespace cutcells namespace cell { /// Cell type - enum class type + enum class type : int { point = 0, interval = 1, diff --git a/cpp/src/cut_cell.cpp b/cpp/src/cut_cell.cpp index 7afc6df..18d27d9 100644 --- a/cpp/src/cut_cell.cpp +++ b/cpp/src/cut_cell.cpp @@ -248,13 +248,13 @@ namespace cutcells::cell{ for(std::size_t i=0;i sub_tet; + std::span sub_tet; switch(cell_type) { - case cutcells::cell::type::triangle: {sub_tet = cutcells::cell::triangle_subdivision_table[i]; + case cutcells::cell::type::triangle: {sub_tet = std::span(cutcells::cell::triangle_subdivision_table[i]); break;} - case cutcells::cell::type::tetrahedron: {sub_tet = cutcells::cell::tetrahedron_subdivision_table[i]; + case cutcells::cell::type::tetrahedron: {sub_tet = std::span(cutcells::cell::tetrahedron_subdivision_table[i]); break;} } @@ -659,4 +659,4 @@ namespace cutcells::cell{ //----------------------------------------------------------------------------- -}//end of namespace \ No newline at end of file +}//end of namespace diff --git a/cpp/src/cut_mesh.cpp b/cpp/src/cut_mesh.cpp index b6fe2b4..b742bb7 100644 --- a/cpp/src/cut_mesh.cpp +++ b/cpp/src/cut_mesh.cpp @@ -11,6 +11,7 @@ #include "utils.h" #include "cell_topology.h" +#include "reference_cell.h" #include #include #include @@ -294,10 +295,15 @@ namespace cutcells::mesh } else { - const int v0 = parent_edges[edge_id][0]; - const int v1 = parent_edges[edge_id][1]; - const int32_t gv0 = static_cast(cut_cell._parent_vertex_ids[v0]); - const int32_t gv1 = static_cast(cut_cell._parent_vertex_ids[v1]); + // token is a VTK edge index; cell_topology.h uses Basix ordering. + const int basix_eid = cell::vtk_to_basix_edge(cut_cell._parent_cell_type, edge_id); + const int bv0 = parent_edges[basix_eid][0]; + const int bv1 = parent_edges[basix_eid][1]; + // _parent_vertex_ids is indexed by VTK vertex; convert. + const int vtk_v0 = cell::basix_to_vtk_vertex(cut_cell._parent_cell_type, bv0); + const int vtk_v1 = cell::basix_to_vtk_vertex(cut_cell._parent_cell_type, bv1); + const int32_t gv0 = static_cast(cut_cell._parent_vertex_ids[vtk_v0]); + const int32_t gv1 = static_cast(cut_cell._parent_vertex_ids[vtk_v1]); key.kind = 1; key.a = std::min(gv0, gv1); key.b = std::max(gv0, gv1); @@ -588,10 +594,16 @@ namespace cutcells::mesh const int eid = static_cast(token); if (eid >= 0 && eid < static_cast(parent_edges.size())) { + // token is a VTK edge index; cell_topology.h uses Basix ordering. + const int basix_eid = cell::vtk_to_basix_edge(ctype, eid); + const int bv0 = parent_edges[basix_eid][0]; + const int bv1 = parent_edges[basix_eid][1]; + const int vtk_v0 = cell::basix_to_vtk_vertex(ctype, bv0); + const int vtk_v1 = cell::basix_to_vtk_vertex(ctype, bv1); const int32_t gv0 = static_cast( - tl_scratch._parent_vertex_ids[parent_edges[eid][0]]); + tl_scratch._parent_vertex_ids[vtk_v0]); const int32_t gv1 = static_cast( - tl_scratch._parent_vertex_ids[parent_edges[eid][1]]); + tl_scratch._parent_vertex_ids[vtk_v1]); key.kind = 1; key.a = std::min(gv0, gv1); key.b = std::max(gv0, gv1); diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index 1d1036d..f676970 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -107,18 +107,18 @@ namespace{ { -1, -1, -1, -1, -1, -1 }, // 0 { 0, 3, 2, 100, -1, -1 }, // 1 { 0, 1, 4, 101, -1, -1 }, // 2 - { 101, 1, 4, 100, 2, 3 }, // 3 + { 100, 2, 3, 101, 1, 4 }, // 3 (prism: swapped bottom/top) { 1, 2, 5, 102, -1, -1 }, // 4 - { 102, 5, 1, 100, 3, 0 }, // 5 - { 102, 2, 5, 101, 0, 4 }, // 6 - { 3, 4, 5, 100, 101, 102}, // 7 + { 100, 3, 0, 102, 5, 1 }, // 5 (prism: swapped bottom/top) + { 101, 0, 4, 102, 2, 5 }, // 6 (prism: swapped bottom/top) + { 100, 101, 102, 3, 4, 5}, // 7 (prism: swapped bottom/top) { 3, 4, 5, 103, -1, -1 }, // 8 - { 103, 4, 5, 100, 0, 2 }, // 9 - { 103, 5, 3, 101, 1, 0 }, // 10 - { 100, 101, 103, 2, 1, 5}, // 11 - { 2, 102, 1, 3, 103, 4 }, // 12 - { 0, 1, 4, 100, 102, 103}, // 13 - { 0, 3, 2, 101, 103, 102}, // 14 + { 100, 0, 2, 103, 4, 5 }, // 9 (prism: swapped bottom/top) + { 101, 1, 0, 103, 5, 3 }, // 10 (prism: swapped bottom/top) + { 2, 1, 5, 100, 101, 103}, // 11 (prism: swapped bottom/top) + { 3, 103, 4, 2, 102, 1 }, // 12 (prism: swapped bottom/top) + { 100, 102, 103, 4, 0, 1}, // 13 (prism: swapped bottom/top) + { 101, 103, 102, 0, 3, 2}, // 14 (prism: swapped bottom/top) { 100, 101, 102, 103, -1, -1} // 15 }; @@ -489,4 +489,4 @@ namespace tetrahedron{ //----------------------------------------------------------------------------- } -} \ No newline at end of file +} diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index 49f8ba4..6eff2cb 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -375,6 +375,8 @@ namespace triangle{ { create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str[i], cut_cell[i], triangulate, intersection_points, vertex_case_map); + cutcells::utils::create_vertex_parent_entity_map( + vertex_case_map, cut_cell[i]._vertex_parent_entity, 3, 3); } }; @@ -402,10 +404,16 @@ namespace triangle{ template void cut(const std::span vertex_coordinates, const int gdim, const std::span ls_values, const std::string& cut_type_str, CutCell& cut_cell, bool triangulate); + template void cut(const std::span vertex_coordinates, const int gdim, + const std::span ls_values, const std::vector& cut_type_str, + std::vector>& cut_cell, bool triangulate); + template void cut(const std::span vertex_coordinates, const int gdim, + const std::span ls_values, const std::vector& cut_type_str, + std::vector>& cut_cell, bool triangulate); template double volume(const std::span vertex_coordinates, const int gdim); template float volume(const std::span vertex_coordinates, const int gdim); //----------------------------------------------------------------------------- } -} \ No newline at end of file +} diff --git a/cpp/src/edge_certification.cpp b/cpp/src/edge_certification.cpp new file mode 100644 index 0000000..8381a74 --- /dev/null +++ b/cpp/src/edge_certification.cpp @@ -0,0 +1,1103 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "edge_certification.h" +#include "bernstein.h" +#include "cell_topology.h" + +#include +#include +#include +#include +#include + +namespace cutcells +{ +namespace +{ + +template +T integer_pow(T base, int exp) +{ + T result = T(1); + for (int i = 0; i < exp; ++i) + result *= base; + return result; +} + +template +T binomial(int n, int k) +{ + if (k < 0 || k > n) + return T(0); + if (k == 0 || k == n) + return T(1); + if (k > n - k) + k = n - k; + + T result = T(1); + for (int i = 0; i < k; ++i) + { + result *= T(n - i); + result /= T(i + 1); + } + return result; +} + +template +T multinomial(int n, const int* alpha, int num_components) +{ + T result = T(1); + int remaining = n; + for (int c = 1; c < num_components; ++c) + { + result *= binomial(remaining, alpha[c]); + remaining -= alpha[c]; + } + return result; +} + +template +std::vector linear_power_poly(T a, T b, int n) +{ + std::vector out(static_cast(n + 1), T(0)); + for (int i = 0; i <= n; ++i) + { + out[static_cast(i)] = + binomial(n, i) * integer_pow(a, n - i) * integer_pow(b, i); + } + return out; +} + +template +std::vector multiply_poly(std::span a, std::span b) +{ + std::vector out(a.size() + b.size() - 1, T(0)); + for (std::size_t i = 0; i < a.size(); ++i) + for (std::size_t j = 0; j < b.size(); ++j) + out[i + j] += a[i] * b[j]; + return out; +} + +template +void add_scaled_poly(std::vector& dst, std::span src, T scale) +{ + if (dst.size() < src.size()) + dst.resize(src.size(), T(0)); + for (std::size_t i = 0; i < src.size(); ++i) + dst[i] += scale * src[i]; +} + +template +void power_to_bernstein_1d(std::span power_coeffs, + std::vector& bernstein_coeffs) +{ + const int degree = static_cast(power_coeffs.size()) - 1; + bernstein_coeffs.assign(static_cast(degree + 1), T(0)); + + for (int i = 0; i <= degree; ++i) + { + T coeff = T(0); + for (int m = 0; m <= i; ++m) + { + coeff += power_coeffs[static_cast(m)] + * binomial(i, m) + / binomial(degree, m); + } + bernstein_coeffs[static_cast(i)] = coeff; + } +} + +// ===================================================================== +// Simplex multi-index helpers (same ordering as bernstein.cpp) +// ===================================================================== + +inline int simplex_index_1d(int i, int /*n*/) { return i; } + +inline int simplex_index_2d(int i, int j, int n) +{ + return j * (n + 1) - j * (j - 1) / 2 + i; +} + +inline int simplex_index_3d(int i, int j, int k, int n) +{ + int offset = 0; + for (int kk = 0; kk < k; ++kk) + { + int m = n - kk; + offset += (m + 1) * (m + 2) / 2; + } + return offset + simplex_index_2d(i, j, n - k); +} + +// ===================================================================== +// Tensor-product index helpers +// ===================================================================== + +inline int tp_index_2d(int ix, int iy, int n) +{ + return iy * (n + 1) + ix; +} + +inline int tp_index_3d(int ix, int iy, int iz, int n) +{ + return iz * (n + 1) * (n + 1) + iy * (n + 1) + ix; +} + +// ===================================================================== +// Edge-to-multi-index mapping for simplex parent edge extraction +// ===================================================================== + +/// For a triangle edge with local vertices (v0, v1) in the canonical numbering +/// {0, 1, 2}, the Bernstein multi-index alpha = (alpha_0, alpha_1, alpha_2) +/// with sum = degree, varies only along the edge. +/// We parameterize: alpha[v0] = n-k, alpha[v1] = k, all others = 0. +/// +/// Returns the linear Bernstein index for the k-th point on that edge. +inline int triangle_edge_coeff_index(int v0, int v1, int k, int n) +{ + // multi-index: alpha[v0] = n-k, alpha[v1] = k, alpha[other] = 0 + int alpha[3] = {0, 0, 0}; + alpha[v0] = n - k; + alpha[v1] = k; + // simplex_index_2d expects (i, j, n) where alpha = (n-i-j, i, j) + return simplex_index_2d(alpha[1], alpha[2], n); +} + +/// Same logic for tetrahedron. +inline int tetrahedron_edge_coeff_index(int v0, int v1, int k, int n) +{ + int alpha[4] = {0, 0, 0, 0}; + alpha[v0] = n - k; + alpha[v1] = k; + // simplex_index_3d expects (i, j, k_idx, n) where alpha = (n-i-j-k_idx, i, j, k_idx) + return simplex_index_3d(alpha[1], alpha[2], alpha[3], n); +} + +} // anonymous namespace + +// ===================================================================== +// subdivide_bernstein_1d +// ===================================================================== + +template +void subdivide_bernstein_1d(std::span coeffs, + T t_split, + std::vector& left, + std::vector& right) +{ + const int p = static_cast(coeffs.size()) - 1; + assert(p >= 0); + + left.resize(static_cast(p + 1)); + right.resize(static_cast(p + 1)); + + // Work array: de Casteljau triangle columns. + // Starting column = coeffs. + std::vector work(coeffs.begin(), coeffs.end()); + + // The left child's i-th coeff is work[0] after i reduction steps. + // The right child's i-th coeff is work[p-i] after (p-i) reduction steps. + left[0] = work[0]; + right[static_cast(p)] = work[static_cast(p)]; + + for (int level = 1; level <= p; ++level) + { + // Reduce: work[j] = (1-t)*work[j] + t*work[j+1] for j = 0..p-level + for (int j = 0; j <= p - level; ++j) + { + work[static_cast(j)] = + (T(1) - t_split) * work[static_cast(j)] + + t_split * work[static_cast(j + 1)]; + } + left[static_cast(level)] = work[0]; + right[static_cast(p - level)] = + work[static_cast(p - level)]; + } +} + +// ===================================================================== +// Sign-hull helpers +// ===================================================================== + +template +bool bernstein_all_positive(std::span coeffs, T tol) +{ + for (const auto& c : coeffs) + if (c <= tol) return false; + return true; +} + +template +bool bernstein_all_negative(std::span coeffs, T tol) +{ + for (const auto& c : coeffs) + if (c >= -tol) return false; + return true; +} + +template +bool bernstein_all_zero(std::span coeffs, T tol) +{ + for (const auto& c : coeffs) + if (std::fabs(c) > tol) return false; + return true; +} + +// ===================================================================== +// find_root_intervals_1d +// ===================================================================== + +template +void find_root_intervals_1d(std::span coeffs, + T t0, T t1, + T zero_tol, T sign_tol, + int depth, int max_depth, + std::vector>& intervals, + bool& has_zero_segment) +{ + // If convex hull excludes zero → no root in this interval. + if (bernstein_all_positive(coeffs, sign_tol) + || bernstein_all_negative(coeffs, sign_tol)) + return; + + // If all coefficients are zero → zero segment. + if (bernstein_all_zero(coeffs, zero_tol)) + { + has_zero_segment = true; + return; + } + + // If max depth reached, record this interval as containing a root. + if (depth >= max_depth) + { + intervals.push_back({t0, t1}); + return; + } + + // Subdivide at midpoint. + T t_mid = (t0 + t1) * T(0.5); + std::vector left_c, right_c; + subdivide_bernstein_1d(coeffs, T(0.5), left_c, right_c); + + find_root_intervals_1d( + std::span(left_c), t0, t_mid, + zero_tol, sign_tol, depth + 1, max_depth, intervals, has_zero_segment); + + find_root_intervals_1d( + std::span(right_c), t_mid, t1, + zero_tol, sign_tol, depth + 1, max_depth, intervals, has_zero_segment); +} + +// ===================================================================== +// Merge overlapping root intervals +// ===================================================================== + +namespace +{ + +template +void merge_root_intervals(std::vector>& intervals, + T merge_tol) +{ + if (intervals.size() <= 1) return; + + // Sort by t0. + std::sort(intervals.begin(), intervals.end(), + [](const EdgeRootInterval& a, const EdgeRootInterval& b) + { return a.t0 < b.t0; }); + + std::vector> merged; + merged.push_back(intervals[0]); + + for (std::size_t i = 1; i < intervals.size(); ++i) + { + auto& back = merged.back(); + if (intervals[i].t0 <= back.t1 + merge_tol) + { + // Overlapping or adjacent — extend. + back.t1 = std::max(back.t1, intervals[i].t1); + } + else + { + merged.push_back(intervals[i]); + } + } + + intervals = std::move(merged); +} + +} // anonymous namespace + +// ===================================================================== +// extract_parent_edge_bernstein +// ===================================================================== + +template +void extract_parent_edge_bernstein(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + int parent_local_edge_id, + std::vector& edge_coeffs) +{ + const int p = degree; + edge_coeffs.resize(static_cast(p + 1)); + + auto edge_verts = cell::edges(parent_cell_type); + const int v0 = edge_verts[static_cast(parent_local_edge_id)][0]; + const int v1 = edge_verts[static_cast(parent_local_edge_id)][1]; + + switch (parent_cell_type) + { + case cell::type::interval: + // Trivial: the edge IS the cell. + for (int k = 0; k <= p; ++k) + edge_coeffs[static_cast(k)] = + parent_coeffs[static_cast(k)]; + break; + + case cell::type::triangle: + for (int k = 0; k <= p; ++k) + edge_coeffs[static_cast(k)] = + parent_coeffs[static_cast( + triangle_edge_coeff_index(v0, v1, k, p))]; + break; + + case cell::type::tetrahedron: + for (int k = 0; k <= p; ++k) + edge_coeffs[static_cast(k)] = + parent_coeffs[static_cast( + tetrahedron_edge_coeff_index(v0, v1, k, p))]; + break; + + case cell::type::quadrilateral: + { + // Basix quad vertices: 0=(0,0),1=(1,0),2=(0,1),3=(1,1) + // Basix quad edges: 0={0,1}, 1={0,2}, 2={1,3}, 3={2,3} + // Bernstein tensor-product index: tp_index_2d(ix,iy,p) = iy*(p+1)+ix + for (int k = 0; k <= p; ++k) + { + int ix = 0, iy = 0; + switch (parent_local_edge_id) + { + case 0: ix = k; iy = 0; break; // {0,1}: ix varies, iy=0 + case 1: ix = 0; iy = k; break; // {0,2}: iy varies, ix=0 + case 2: ix = p; iy = k; break; // {1,3}: iy varies, ix=p + case 3: ix = k; iy = p; break; // {2,3}: ix varies, iy=p + } + edge_coeffs[static_cast(k)] = + parent_coeffs[static_cast(tp_index_2d(ix, iy, p))]; + } + break; + } + + case cell::type::hexahedron: + { + // Basix hex vertex numbering: + // 0=(0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(1,1,0), + // 4=(0,0,1), 5=(1,0,1), 6=(0,1,1), 7=(1,1,1) + // For Basix ordering: v = bx + 2*by + 4*bz + // cell_topology.h now returns Basix vertex IDs directly. + for (int k = 0; k <= p; ++k) + { + const int x0 = (v0 & 1) * p, y0 = ((v0 >> 1) & 1) * p, z0 = ((v0 >> 2) & 1) * p; + const int x1 = (v1 & 1) * p, y1 = ((v1 >> 1) & 1) * p, z1 = ((v1 >> 2) & 1) * p; + + const int ix = x0 + (x1 - x0) * k / p; + const int iy = y0 + (y1 - y0) * k / p; + const int iz = z0 + (z1 - z0) * k / p; + + edge_coeffs[static_cast(k)] = + parent_coeffs[static_cast(tp_index_3d(ix, iy, iz, p))]; + } + break; + } + + default: + throw std::runtime_error( + "extract_parent_edge_bernstein: unsupported cell type"); + } +} + +// ===================================================================== +// restrict_edge_bernstein_exact +// ===================================================================== + +template +void restrict_edge_bernstein_exact(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + std::span xi_a, + std::span xi_b, + std::vector& edge_coeffs) +{ + const int p = degree; + const int tdim = cell::get_tdim(parent_cell_type); + if (static_cast(xi_a.size()) != tdim + || static_cast(xi_b.size()) != tdim) + { + throw std::runtime_error( + "restrict_edge_bernstein_exact: endpoint dimension mismatch"); + } + + std::vector power_coeffs(static_cast(p + 1), T(0)); + + if (bernstein::is_simplex(parent_cell_type)) + { + std::vector lambda_a(static_cast(tdim + 1), T(0)); + std::vector lambda_b(static_cast(tdim + 1), T(0)); + + lambda_a[0] = T(1); + lambda_b[0] = T(1); + for (int d = 0; d < tdim; ++d) + { + lambda_a[0] -= xi_a[static_cast(d)]; + lambda_b[0] -= xi_b[static_cast(d)]; + lambda_a[static_cast(d + 1)] = xi_a[static_cast(d)]; + lambda_b[static_cast(d + 1)] = xi_b[static_cast(d)]; + } + + int coeff_index = 0; + if (tdim == 1) + { + for (int i = 0; i <= p; ++i, ++coeff_index) + { + const int alpha[2] = {p - i, i}; + std::vector term = {T(1)}; + for (int c = 0; c < 2; ++c) + { + const std::vector factor = linear_power_poly( + lambda_a[static_cast(c)], + lambda_b[static_cast(c)] - lambda_a[static_cast(c)], + alpha[c]); + term = multiply_poly(term, factor); + } + add_scaled_poly(power_coeffs, std::span(term), + parent_coeffs[static_cast(coeff_index)] + * multinomial(p, alpha, 2)); + } + } + else if (tdim == 2) + { + for (int j = 0; j <= p; ++j) + { + for (int i = 0; i <= p - j; ++i, ++coeff_index) + { + const int alpha[3] = {p - i - j, i, j}; + std::vector term = {T(1)}; + for (int c = 0; c < 3; ++c) + { + const std::vector factor = linear_power_poly( + lambda_a[static_cast(c)], + lambda_b[static_cast(c)] - lambda_a[static_cast(c)], + alpha[c]); + term = multiply_poly(term, factor); + } + add_scaled_poly(power_coeffs, std::span(term), + parent_coeffs[static_cast(coeff_index)] + * multinomial(p, alpha, 3)); + } + } + } + else if (tdim == 3) + { + for (int k = 0; k <= p; ++k) + { + for (int j = 0; j <= p - k; ++j) + { + for (int i = 0; i <= p - k - j; ++i, ++coeff_index) + { + const int alpha[4] = {p - i - j - k, i, j, k}; + std::vector term = {T(1)}; + for (int c = 0; c < 4; ++c) + { + const std::vector factor = linear_power_poly( + lambda_a[static_cast(c)], + lambda_b[static_cast(c)] - lambda_a[static_cast(c)], + alpha[c]); + term = multiply_poly(term, factor); + } + add_scaled_poly(power_coeffs, std::span(term), + parent_coeffs[static_cast(coeff_index)] + * multinomial(p, alpha, 4)); + } + } + } + } + else + { + throw std::runtime_error( + "restrict_edge_bernstein_exact: unsupported simplex tdim"); + } + } + else if (bernstein::is_tensor_product(parent_cell_type)) + { + if (tdim == 2) + { + int coeff_index = 0; + for (int iy = 0; iy <= p; ++iy) + { + for (int ix = 0; ix <= p; ++ix, ++coeff_index) + { + std::vector term = {T(1)}; + const int ids[2] = {ix, iy}; + for (int d = 0; d < 2; ++d) + { + const T a = xi_a[static_cast(d)]; + const T b = xi_b[static_cast(d)] - a; + std::vector dim_poly = linear_power_poly(a, b, ids[d]); + const std::vector one_minus_poly = + linear_power_poly(T(1) - a, -b, p - ids[d]); + dim_poly = multiply_poly(dim_poly, one_minus_poly); + const T scale = binomial(p, ids[d]); + for (T& x : dim_poly) + x *= scale; + term = multiply_poly(term, dim_poly); + } + add_scaled_poly(power_coeffs, std::span(term), + parent_coeffs[static_cast(coeff_index)]); + } + } + } + else if (tdim == 3) + { + int coeff_index = 0; + for (int iz = 0; iz <= p; ++iz) + { + for (int iy = 0; iy <= p; ++iy) + { + for (int ix = 0; ix <= p; ++ix, ++coeff_index) + { + std::vector term = {T(1)}; + const int ids[3] = {ix, iy, iz}; + for (int d = 0; d < 3; ++d) + { + const T a = xi_a[static_cast(d)]; + const T b = xi_b[static_cast(d)] - a; + std::vector dim_poly = linear_power_poly(a, b, ids[d]); + const std::vector one_minus_poly = + linear_power_poly(T(1) - a, -b, p - ids[d]); + dim_poly = multiply_poly(dim_poly, one_minus_poly); + const T scale = binomial(p, ids[d]); + for (T& x : dim_poly) + x *= scale; + term = multiply_poly(term, dim_poly); + } + add_scaled_poly(power_coeffs, std::span(term), + parent_coeffs[static_cast(coeff_index)]); + } + } + } + } + else + { + throw std::runtime_error( + "restrict_edge_bernstein_exact: unsupported tensor-product tdim"); + } + } + else + { + throw std::runtime_error( + "restrict_edge_bernstein_exact: unsupported parent cell type"); + } + + power_to_bernstein_1d(std::span(power_coeffs), edge_coeffs); +} + +// ===================================================================== +// edge_is_on_single_parent_edge +// ===================================================================== + +template +bool edge_is_on_single_parent_edge(const AdaptCell& adapt_cell, + int edge_id, + int& parent_edge_id) +{ + // Get the two vertex indices of this edge. + auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + assert(verts.size() == 2); + + const int v0 = verts[0]; + const int v1 = verts[1]; + + // Both must be on parent dimension 0 or 1 (vertex or edge). + const int dim0 = adapt_cell.vertex_parent_dim[static_cast(v0)]; + const int dim1 = adapt_cell.vertex_parent_dim[static_cast(v1)]; + + // Collect the set of parent edges each vertex lies on. + // A vertex with parent_dim == 0 (parent vertex) lies on all parent edges + // incident to that vertex. A vertex with parent_dim == 1 lies on exactly + // one parent edge (parent_id). + // For simplicity, and because the initial AdaptCell has only parent-vertex + // provenance, we handle the case where both vertices are parent vertices: + // check if they form a parent edge. + + if (dim0 == 1 && dim1 == 1) + { + // Both on parent edges; must be the same one. + int pe0 = adapt_cell.vertex_parent_id[static_cast(v0)]; + int pe1 = adapt_cell.vertex_parent_id[static_cast(v1)]; + if (pe0 == pe1) + { + parent_edge_id = pe0; + return true; + } + return false; + } + + if (dim0 == 0 && dim1 == 0) + { + // Both are initial parent vertices. In make_adapt_cell the vertices + // are stored in Basix order, so AdaptCell vertex index v == Basix + // local vertex index v. Compare local indices directly against the + // parent edge table (which also uses Basix vertex indices). + auto parent_edges = cell::edges(adapt_cell.parent_cell_type); + for (int e = 0; e < static_cast(parent_edges.size()); ++e) + { + const auto& pe = parent_edges[static_cast(e)]; + if ((pe[0] == v0 && pe[1] == v1) + || (pe[0] == v1 && pe[1] == v0)) + { + parent_edge_id = e; + return true; + } + } + return false; + } + + if ((dim0 == 0 && dim1 == 1) || (dim0 == 1 && dim1 == 0)) + { + // One is a parent vertex, the other is on a parent edge. + // Check that the parent vertex is an endpoint of that parent edge. + int vert_v = (dim0 == 0) ? v0 : v1; + int edge_v = (dim0 == 0) ? v1 : v0; + int gv = adapt_cell.vertex_parent_id[static_cast(vert_v)]; + int pe = adapt_cell.vertex_parent_id[static_cast(edge_v)]; + + auto parent_edges = cell::edges(adapt_cell.parent_cell_type); + const auto& edge_pair = parent_edges[static_cast(pe)]; + if (edge_pair[0] == gv || edge_pair[1] == gv) + { + parent_edge_id = pe; + return true; + } + return false; + } + + // Other combinations (face, interior) → not on a single parent edge. + return false; +} + +// ===================================================================== +// classify_edge_roots +// ===================================================================== + +template +EdgeRootTag classify_edge_roots(std::span edge_coeffs, + T zero_tol, T sign_tol, + int max_depth, + T& green_split_t, + bool& has_green_split_t) +{ + has_green_split_t = false; + green_split_t = T(0); + + if (edge_coeffs.empty()) + return EdgeRootTag::not_classified; + + // 1. All zero? + if (bernstein_all_zero(edge_coeffs, zero_tol)) + return EdgeRootTag::zero; + + // 2. All positive or all negative? + if (bernstein_all_positive(edge_coeffs, sign_tol)) + return EdgeRootTag::no_root; + if (bernstein_all_negative(edge_coeffs, sign_tol)) + return EdgeRootTag::no_root; + + const bool left_zero = std::fabs(edge_coeffs.front()) <= zero_tol; + const bool right_zero = std::fabs(edge_coeffs.back()) <= zero_tol; + + // 3. Find root intervals. + std::vector> intervals; + bool has_zero_segment = false; + find_root_intervals_1d(edge_coeffs, T(0), T(1), + zero_tol, sign_tol, 0, max_depth, + intervals, has_zero_segment); + + if (has_zero_segment) + return EdgeRootTag::zero; + + // --------------------------------------------------------------- + // Check: "one vertex zero AND an interior root" must be multiple_roots. + // + // If one endpoint has phi≈0 (left_zero / right_zero) AND the root + // finder also found a strictly interior interval (t0>0 or t1<1), + // merging them into a single one_root would place the cut at the + // existing vertex (wrong position) and produce degenerate geometry. + // Classify as multiple_roots here, *before* any merge step, so + // green refinement splits the edge between the vertex-zero event + // and the interior sign change. + // --------------------------------------------------------------- + if (left_zero && !intervals.empty()) + { + T ep_zero_end = T(0); // rightmost bound of zero-endpoint intervals + T first_interior = T(1); // leftmost start of strictly interior intervals + bool found_ep = false; + bool found_int = false; + for (const auto& iv : intervals) + { + if (iv.t0 == T(0)) + { + ep_zero_end = std::max(ep_zero_end, iv.t1); + found_ep = true; + } + else // iv.t0 > 0 → strictly interior + { + first_interior = std::min(first_interior, iv.t0); + found_int = true; + } + } + if (found_int) + { + T gap_lo = found_ep ? ep_zero_end : T(0); + green_split_t = (gap_lo + first_interior) * T(0.5); + green_split_t = std::clamp(green_split_t, T(0), T(1)); + has_green_split_t = true; + return EdgeRootTag::multiple_roots; + } + } + if (right_zero && !intervals.empty()) + { + T ep_zero_start = T(1); // leftmost bound of zero-endpoint intervals + T last_interior = T(0); // rightmost end of strictly interior intervals + bool found_ep = false; + bool found_int = false; + for (const auto& iv : intervals) + { + if (iv.t1 == T(1)) + { + ep_zero_start = std::min(ep_zero_start, iv.t0); + found_ep = true; + } + else // iv.t1 < 1 → strictly interior + { + last_interior = std::max(last_interior, iv.t1); + found_int = true; + } + } + if (found_int) + { + T gap_hi = found_ep ? ep_zero_start : T(1); + green_split_t = (last_interior + gap_hi) * T(0.5); + green_split_t = std::clamp(green_split_t, T(0), T(1)); + has_green_split_t = true; + return EdgeRootTag::multiple_roots; + } + } + + // Merge overlapping intervals. + T merge_tol = T(4) * std::numeric_limits::epsilon(); + if (max_depth > 0) + { + T width = T(1); + for (int i = 0; i < max_depth; ++i) + width *= T(0.5); + merge_tol = std::max(merge_tol, T(2) * width); + } + merge_root_intervals(intervals, merge_tol); + + std::vector> clusters = intervals; + + bool left_covered = false; + bool right_covered = false; + for (const auto& interval : clusters) + { + if (interval.t0 <= merge_tol) + left_covered = true; + if (interval.t1 >= T(1) - merge_tol) + right_covered = true; + } + + if (left_zero && !left_covered) + clusters.push_back({T(0), T(0)}); + if (right_zero && !right_covered) + clusters.push_back({T(1), T(1)}); + + if (clusters.empty()) + return EdgeRootTag::no_root; + + std::sort(clusters.begin(), clusters.end(), + [](const EdgeRootInterval& a, const EdgeRootInterval& b) + { return a.t0 < b.t0; }); + merge_root_intervals(clusters, merge_tol); + + if (clusters.size() == 1) + return EdgeRootTag::one_root; + + green_split_t = (clusters[0].t1 + clusters[1].t0) * T(0.5); + green_split_t = std::clamp(green_split_t, T(0), T(1)); + has_green_split_t = true; + return EdgeRootTag::multiple_roots; +} + +template +bool locate_one_root_parameter(std::span edge_coeffs, + T zero_tol, T sign_tol, + int max_depth, + T& root_t) +{ + root_t = T(0); + + if (edge_coeffs.empty()) + return false; + + if (bernstein_all_zero(edge_coeffs, zero_tol)) + return false; + + const bool left_zero = std::fabs(edge_coeffs.front()) <= zero_tol; + const bool right_zero = std::fabs(edge_coeffs.back()) <= zero_tol; + + if (left_zero && !right_zero) + { + root_t = T(0); + return true; + } + if (right_zero && !left_zero) + { + root_t = T(1); + return true; + } + + std::vector> intervals; + bool has_zero_segment = false; + find_root_intervals_1d(edge_coeffs, T(0), T(1), + zero_tol, sign_tol, 0, max_depth, + intervals, has_zero_segment); + if (has_zero_segment) + return false; + + T merge_tol = T(4) * std::numeric_limits::epsilon(); + if (max_depth > 0) + { + T width = T(1); + for (int i = 0; i < max_depth; ++i) + width *= T(0.5); + merge_tol = std::max(merge_tol, T(2) * width); + } + merge_root_intervals(intervals, merge_tol); + + std::vector> clusters = intervals; + bool left_covered = false; + bool right_covered = false; + for (const auto& interval : clusters) + { + if (interval.t0 <= merge_tol) + left_covered = true; + if (interval.t1 >= T(1) - merge_tol) + right_covered = true; + } + + if (left_zero && !left_covered) + clusters.push_back({T(0), T(0)}); + if (right_zero && !right_covered) + clusters.push_back({T(1), T(1)}); + + if (clusters.empty()) + return false; + + std::sort(clusters.begin(), clusters.end(), + [](const EdgeRootInterval& a, const EdgeRootInterval& b) + { return a.t0 < b.t0; }); + merge_root_intervals(clusters, merge_tol); + + if (clusters.size() != 1) + return false; + + const T a = clusters[0].t0; + const T b = clusters[0].t1; + root_t = (a + b) * T(0.5); + + if (std::fabs(b - a) <= zero_tol) + return true; + + const int degree = static_cast(edge_coeffs.size()) - 1; + std::array xi = {root_t}; + std::array grad = {T(0)}; + + const T eval_tol = std::max(zero_tol, T(32) * std::numeric_limits::epsilon()); + for (int iter = 0; iter < 12; ++iter) + { + xi[0] = root_t; + const T value = bernstein::evaluate( + cell::type::interval, degree, + edge_coeffs, std::span(xi.data(), 1)); + if (std::fabs(value) <= eval_tol) + break; + + bernstein::gradient( + cell::type::interval, degree, + edge_coeffs, std::span(xi.data(), 1), + std::span(grad.data(), 1)); + if (std::fabs(grad[0]) <= eval_tol) + break; + + const T next = root_t - value / grad[0]; + if (next <= a || next >= b) + break; + root_t = next; + } + + root_t = std::clamp(root_t, a, b); + return true; +} + +// ===================================================================== +// classify_new_edges +// ===================================================================== + +template +void classify_new_edges(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol, + int max_depth) +{ + const int n_edges = adapt_cell.n_entities(1); + + // Ensure tag storage is large enough. + if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) + adapt_cell.resize_edge_root_tags(level_set_id + 1); + if (adapt_cell.edge_green_split_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + adapt_cell.resize_green_split_data(level_set_id + 1); + if (adapt_cell.edge_one_root_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + adapt_cell.resize_one_root_data(level_set_id + 1); + + std::vector edge_coeffs; + + for (int e = 0; e < n_edges; ++e) + { + // Skip already classified edges. + if (adapt_cell.get_edge_root_tag(level_set_id, e) + != EdgeRootTag::not_classified) + continue; + + // Extract edge Bernstein coefficients. + int parent_edge_id = -1; + if (edge_is_on_single_parent_edge(adapt_cell, e, parent_edge_id)) + { + extract_parent_edge_bernstein( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + parent_edge_id, edge_coeffs); + } + else + { + // Arbitrary adaptive edge: get endpoint reference coordinates. + auto verts = adapt_cell.entity_to_vertex[1][ + static_cast(e)]; + const int tdim = adapt_cell.tdim; + std::span xi_a( + adapt_cell.vertex_coords.data() + + static_cast(verts[0]) * static_cast(tdim), + static_cast(tdim)); + std::span xi_b( + adapt_cell.vertex_coords.data() + + static_cast(verts[1]) * static_cast(tdim), + static_cast(tdim)); + + restrict_edge_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + xi_a, xi_b, edge_coeffs); + } + + // Classify. + T green_split_t = T(0); + bool has_green = false; + EdgeRootTag tag = classify_edge_roots( + std::span(edge_coeffs), + zero_tol, sign_tol, max_depth, + green_split_t, has_green); + + adapt_cell.set_edge_root_tag(level_set_id, e, tag); + + const auto idx = static_cast(level_set_id * n_edges + e); + adapt_cell.edge_green_split_param[idx] = green_split_t; + adapt_cell.edge_green_split_has_value[idx] = has_green ? 1 : 0; + if (tag != EdgeRootTag::one_root) + { + adapt_cell.edge_one_root_param[idx] = T(0); + adapt_cell.edge_one_root_vertex_id[idx] = -1; + adapt_cell.edge_one_root_has_value[idx] = 0; + } + } +} + +// ===================================================================== +// Explicit template instantiations +// ===================================================================== + +template void subdivide_bernstein_1d(std::span, double, + std::vector&, std::vector&); +template void subdivide_bernstein_1d(std::span, float, + std::vector&, std::vector&); + +template bool bernstein_all_positive(std::span, double); +template bool bernstein_all_positive(std::span, float); +template bool bernstein_all_negative(std::span, double); +template bool bernstein_all_negative(std::span, float); +template bool bernstein_all_zero(std::span, double); +template bool bernstein_all_zero(std::span, float); + +template void find_root_intervals_1d(std::span, double, double, + double, double, int, int, + std::vector>&, bool&); +template void find_root_intervals_1d(std::span, float, float, + float, float, int, int, + std::vector>&, bool&); + +template void extract_parent_edge_bernstein(cell::type, int, + std::span, int, + std::vector&); +template void extract_parent_edge_bernstein(cell::type, int, + std::span, int, + std::vector&); + +template void restrict_edge_bernstein_exact(cell::type, int, + std::span, + std::span, + std::span, + std::vector&); +template void restrict_edge_bernstein_exact(cell::type, int, + std::span, + std::span, + std::span, + std::vector&); + +template bool edge_is_on_single_parent_edge(const AdaptCell&, int, int&); +template bool edge_is_on_single_parent_edge(const AdaptCell&, int, int&); + +template EdgeRootTag classify_edge_roots(std::span, double, double, + int, double&, bool&); +template EdgeRootTag classify_edge_roots(std::span, float, float, + int, float&, bool&); +template bool locate_one_root_parameter(std::span, double, double, + int, double&); +template bool locate_one_root_parameter(std::span, float, float, + int, float&); + +template void classify_new_edges(AdaptCell&, const LevelSetCell&, + int, double, double, int); +template void classify_new_edges(AdaptCell&, const LevelSetCell&, + int, float, float, int); +template void classify_new_edges(AdaptCell&, const LevelSetCell&, + int, double, double, int); +template void classify_new_edges(AdaptCell&, const LevelSetCell&, + int, float, float, int); + +} // namespace cutcells diff --git a/cpp/src/edge_certification.h b/cpp/src/edge_certification.h new file mode 100644 index 0000000..8994084 --- /dev/null +++ b/cpp/src/edge_certification.h @@ -0,0 +1,198 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +#include "adapt_cell.h" +#include "cell_types.h" +#include "level_set_cell.h" + +namespace cutcells +{ + +// ===================================================================== +// Root-interval helper +// ===================================================================== + +/// A parameter interval [t0, t1] containing a root of a 1D Bernstein polynomial. +template +struct EdgeRootInterval +{ + T t0 = 0; + T t1 = 0; +}; + +// ===================================================================== +// 1D Bernstein subdivision +// ===================================================================== + +/// Exact de Casteljau subdivision of a degree-p 1D Bernstein polynomial at t_split. +/// +/// Given coefficients c_0, ..., c_p, computes left[0..p] and right[0..p] +/// such that: +/// left is the Bernstein representation on [0, t_split] +/// right is the Bernstein representation on [t_split, 1] +/// +/// @param coeffs Bernstein coefficients of degree p (size p+1). +/// @param t_split Parameter in (0,1). +/// @param[out] left Left child coefficients (resized to p+1). +/// @param[out] right Right child coefficients (resized to p+1). +template +void subdivide_bernstein_1d(std::span coeffs, + T t_split, + std::vector& left, + std::vector& right); + +// ===================================================================== +// 1D sign-hull helpers +// ===================================================================== + +/// True if all coefficients are > tol. +template +bool bernstein_all_positive(std::span coeffs, T tol); + +/// True if all coefficients are < -tol. +template +bool bernstein_all_negative(std::span coeffs, T tol); + +/// True if |coeff| <= tol for every coefficient. +template +bool bernstein_all_zero(std::span coeffs, T tol); + +// ===================================================================== +// Recursive root-interval finder +// ===================================================================== + +/// Recursively locate parameter intervals containing roots of a 1D +/// Bernstein polynomial, using convex-hull exclusion and de Casteljau +/// subdivision. +/// +/// @param coeffs Bernstein coefficients on the sub-interval [t0, t1]. +/// @param t0, t1 Current parameter bounds (start with 0, 1). +/// @param zero_tol Tolerance for the all-zero test. +/// @param sign_tol Tolerance for the all-positive / all-negative test. +/// @param depth Current recursion depth. +/// @param max_depth Maximum recursion depth. +/// @param[out] intervals Collected root intervals. +/// @param[out] has_zero_segment Set to true if an identically-zero segment is found. +template +void find_root_intervals_1d(std::span coeffs, + T t0, T t1, + T zero_tol, T sign_tol, + int depth, int max_depth, + std::vector>& intervals, + bool& has_zero_segment); + +// ===================================================================== +// Exact edge Bernstein extraction +// ===================================================================== + +/// Extract the Bernstein coefficients of a parent polynomial restricted to +/// a parent edge (local edge id, canonical numbering). +/// +/// For simplex cells this is multi-index slicing. +/// For tensor-product cells this is row/column extraction. +/// +/// @param parent_cell_type Type of the parent cell. +/// @param degree Polynomial degree. +/// @param parent_coeffs Bernstein coefficients on the parent cell. +/// @param parent_local_edge_id Local edge index (cell_topology.h ordering). +/// @param[out] edge_coeffs Bernstein coefficients on the edge (degree+1 entries). +template +void extract_parent_edge_bernstein(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + int parent_local_edge_id, + std::vector& edge_coeffs); + +/// Exact Bernstein restriction to an arbitrary edge [xi_a, xi_b] inside +/// the parent reference cell. +/// +/// Computes the Bernstein coefficients of q(t) = phi((1-t)*xi_a + t*xi_b). +/// +/// @param parent_cell_type Type of the parent cell. +/// @param degree Polynomial degree. +/// @param parent_coeffs Bernstein coefficients on the parent cell. +/// @param xi_a, xi_b Endpoints in parent reference coordinates (size = tdim). +/// @param[out] edge_coeffs Bernstein coefficients on the edge (degree+1 entries). +template +void restrict_edge_bernstein_exact(cell::type parent_cell_type, + int degree, + std::span parent_coeffs, + std::span xi_a, + std::span xi_b, + std::vector& edge_coeffs); + +/// Test whether an adaptive edge lies entirely on a single parent edge. +/// +/// Uses vertex provenance: both endpoints must have parent_dim == 1 (or 0 +/// with the vertex on the same parent edge) and the same parent_edge_id. +/// +/// @param adapt_cell The AdaptCell. +/// @param edge_id Index of the edge in entity_to_vertex[1]. +/// @param[out] parent_edge_id If true, the parent edge id. +/// @return true if the edge lies on a single parent edge. +template +bool edge_is_on_single_parent_edge(const AdaptCell& adapt_cell, + int edge_id, + int& parent_edge_id); + +// ===================================================================== +// Edge classifier +// ===================================================================== + +/// Classify a 1D Bernstein polynomial for root structure. +/// +/// @param edge_coeffs Bernstein coefficients (degree+1 entries). +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +/// @param max_depth Maximum subdivision depth for root search. +/// @param[out] green_split_t Parameter between two distinct root intervals +/// (valid only if tag == multiple_roots). +/// @param[out] has_green_split_t True if green_split_t was computed. +/// @return EdgeRootTag. +template +EdgeRootTag classify_edge_roots(std::span edge_coeffs, + T zero_tol, T sign_tol, + int max_depth, + T& green_split_t, + bool& has_green_split_t); + +/// Localize the unique root on a 1D Bernstein polynomial already known to +/// have exactly one isolated root. +/// +/// Returns false if the polynomial does not represent a unique isolated root. +template +bool locate_one_root_parameter(std::span edge_coeffs, + T zero_tol, T sign_tol, + int max_depth, + T& root_t); + +/// Classify all not-yet-classified edges of an AdaptCell for one level set. +/// +/// For each edge with tag == not_classified: +/// 1. extract exact edge Bernstein from the LevelSetCell +/// 2. classify root structure +/// 3. store tag and optional green-split parameter +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set (bit position / name index). +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +/// @param max_depth Maximum subdivision depth for root search. +template +void classify_new_edges(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol, + int max_depth); + +} // namespace cutcells diff --git a/cpp/src/edge_root.h b/cpp/src/edge_root.h new file mode 100644 index 0000000..b6db9fd --- /dev/null +++ b/cpp/src/edge_root.h @@ -0,0 +1,1148 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cutcells::cell::edge_root +{ +enum class method +{ + linear = 0, + brent = 1, + itp = 2, + newton = 3 +}; + +template +struct RootSolveInfo +{ + T t = T(0.5); + T residual = std::numeric_limits::max(); + int iterations = 0; + int evaluations = 0; + bool converged = false; +}; + +/// Result of a ray root-finding solve: phi(x_s + t*d) = 0. +template +struct RayRootResult +{ + bool valid = false; + bool ambiguous_two_sided = false; + bool used_gradient_dir = false; + bool used_fallback_dir = false; + + T t = T(0); + T phi_at_root = std::numeric_limits::max(); + T dist = std::numeric_limits::max(); + T residual = std::numeric_limits::max(); + int iterations = 0; + int evaluations = 0; + bool converged = false; + bool domain_exit = false; + bool gradient_degenerate = false; + + std::array x_ref = {T(0), T(0), T(0)}; + + bool pos_valid = false; + bool neg_valid = false; + T pos_t = T(0); + T neg_t = T(0); +}; + +template +struct RaySearchOptions +{ + int max_iter = 64; + T xtol = T(1e-12); + T ftol = T(1e-12); + T domain_tol = T(1e-10); + std::array probe_scales = {T(1), T(2), T(4)}; + T max_probe_distance = std::numeric_limits::infinity(); +}; + +template +inline T linear_root_parameter(const T v0, const T v1, const T level = T(0)) +{ + const T denom = v1 - v0; + if (std::abs(denom) <= std::numeric_limits::epsilon()) + return T(0.5); + return (level - v0) / denom; +} + +template +inline void interpolate_point(const std::span p0, const std::span p1, + const T t, const std::span out) +{ + if (p0.size() != p1.size() || p0.size() != out.size()) + throw std::invalid_argument("interpolate_point: inconsistent point dimensions"); + + for (std::size_t i = 0; i < p0.size(); ++i) + out[i] = p0[i] + (p1[i] - p0[i]) * t; +} + +template +inline void linear_intersection_point(const std::span p0, const std::span p1, + const T v0, const T v1, const std::span out, + const T level = T(0)) +{ + const T t = linear_root_parameter(v0, v1, level); + interpolate_point(p0, p1, t, out); +} + +template +inline void linear_intersection_point(const std::span p0, const std::span p1, + const T v0, const T v1, std::vector& out, + const int offset = 0, const T level = T(0)) +{ + if (offset < 0) + throw std::invalid_argument("linear_intersection_point: negative offset"); + if (out.size() < static_cast(offset) + p0.size()) + throw std::invalid_argument("linear_intersection_point: output vector too small"); + + linear_intersection_point(p0, p1, v0, v1, + std::span(out.data() + offset, p0.size()), level); +} + +template +inline T evaluate_edge_function(Phi& phi, const std::span p0, const std::span p1, + const T t) +{ + std::array x = {T(0), T(0), T(0)}; + const std::size_t gdim = p0.size(); + if (gdim > x.size()) + throw std::invalid_argument("evaluate_edge_function: only gdim <= 3 supported"); + + for (std::size_t j = 0; j < gdim; ++j) + x[j] = p0[j] + (p1[j] - p0[j]) * t; + + return static_cast(std::invoke(phi, std::span(x.data(), gdim))); +} + +/// General-purpose Brent solver on an arbitrary real interval [a, b]. +/// +/// Unlike brent_parameter, this does NOT clamp the result to [0, 1]. +/// Use this when the parameter domain extends beyond the unit interval, +/// e.g. for ray-parameter root finding. +/// +/// @param eval callable T(T) — function whose root is sought +/// @param a left bracket +/// @param b right bracket +/// @param fa eval(a) +/// @param fb eval(b) — must satisfy fa * fb <= 0 +/// @param max_iter maximum iterations +/// @param xtol tolerance on bracket width +/// @param ftol tolerance on function value +template +inline T brent_solve(Eval&& eval, T a, T b, T fa, T fb, + int max_iter, T xtol, T ftol, + int* out_iterations = nullptr, + bool* out_converged = nullptr) +{ + if (out_iterations) + *out_iterations = 0; + if (out_converged) + *out_converged = false; + + if (std::abs(fa) <= ftol) + { + if (out_converged) *out_converged = true; + return a; + } + if (std::abs(fb) <= ftol) + { + if (out_converged) *out_converged = true; + return b; + } + if (fa * fb > T(0)) + return T(0.5) * (a + b); + + T c = a, fc = fa; + T d = b - a, e = d; + + for (int iter = 0; iter < max_iter; ++iter) + { + if ((fb > T(0) && fc > T(0)) || (fb < T(0) && fc < T(0))) + { + c = a; fc = fa; d = b - a; e = d; + } + if (std::abs(fc) < std::abs(fb)) + { + a = b; fa = fb; b = c; fb = fc; c = a; fc = fa; + } + + const T eps = std::numeric_limits::epsilon(); + const T tol1 = T(2) * eps * std::abs(b) + T(0.5) * xtol; + const T xm = T(0.5) * (c - b); + + if (std::abs(xm) <= tol1 || std::abs(fb) <= ftol) + { + if (out_iterations) *out_iterations = iter + 1; + if (out_converged) *out_converged = true; + return b; + } + + if (std::abs(e) >= tol1 && std::abs(fa) > std::abs(fb)) + { + T s = fb / fa; + T p, q; + if (a == c) + { + p = T(2) * xm * s; + q = T(1) - s; + } + else + { + T q0 = fa / fc, r = fb / fc; + p = s * (T(2) * xm * q0 * (q0 - r) - (b - a) * (r - T(1))); + q = (q0 - T(1)) * (r - T(1)) * (s - T(1)); + } + if (p > T(0)) q = -q; + p = std::abs(p); + + const T min1 = T(3) * xm * q - std::abs(tol1 * q); + const T min2 = std::abs(e * q); + if (T(2) * p < std::min(min1, min2)) + { e = d; d = p / q; } + else + { d = xm; e = d; } + } + else + { + d = xm; e = d; + } + + a = b; fa = fb; + if (std::abs(d) > tol1) b += d; + else b += (xm > T(0) ? tol1 : -tol1); + fb = eval(b); + } + + if (out_iterations) *out_iterations = max_iter; + return b; +} + +template +inline T brent_parameter(Eval&& eval, T a, T b, T fa, T fb, + const int max_iter, const T xtol, const T ftol, + int* out_iterations = nullptr, + bool* out_converged = nullptr) +{ + if (out_iterations) + *out_iterations = 0; + if (out_converged) + *out_converged = false; + + if (std::abs(fa) <= ftol) + { + if (out_converged) + *out_converged = true; + return a; + } + if (std::abs(fb) <= ftol) + { + if (out_converged) + *out_converged = true; + return b; + } + + if (fa * fb > T(0)) + throw std::domain_error("brent_parameter: no sign change in bracket [a, b]"); + + T c = a; + T fc = fa; + T d = b - a; + T e = d; + + for (int iter = 0; iter < max_iter; ++iter) + { + if ((fb > 0 && fc > 0) || (fb < 0 && fc < 0)) + { + c = a; + fc = fa; + d = b - a; + e = d; + } + + if (std::abs(fc) < std::abs(fb)) + { + // Three-way rotation: new b = c_old (better), new a = b_old, new c = b_old. + // Execute sequentially so that after a = b, 'a' already holds b_old. + a = b; + fa = fb; + b = c; + fb = fc; + c = a; // a is already b_old — maintains bracket [b, c] with f(b)*f(c) ≤ 0 + fc = fa; // fa is already fb_old + } + + const T eps = std::numeric_limits::epsilon(); + const T tol1 = T(2) * eps * std::abs(b) + T(0.5) * xtol; + const T xm = T(0.5) * (c - b); + + if (std::abs(xm) <= tol1 || std::abs(fb) <= ftol) + { + if (out_iterations) + *out_iterations = iter + 1; + if (out_converged) + *out_converged = true; + return b; + } + + if (std::abs(e) >= tol1 && std::abs(fa) > std::abs(fb)) + { + T s = fb / fa; + T p, q; + if (a == c) + { + p = T(2) * xm * s; + q = T(1) - s; + } + else + { + T r = fb / fc; + T q0 = fa / fc; + p = s * (T(2) * xm * q0 * (q0 - r) - (b - a) * (r - T(1))); + q = (q0 - T(1)) * (r - T(1)) * (s - T(1)); + } + + if (p > T(0)) + q = -q; + p = std::abs(p); + T min1 = T(3) * xm * q - std::abs(tol1 * q); + T min2 = std::abs(e * q); + if (T(2) * p < std::min(min1, min2)) + { + e = d; + d = p / q; + } + else + { + d = xm; + e = d; + } + } + else + { + d = xm; + e = d; + } + + a = b; + fa = fb; + if (std::abs(d) > tol1) + b += d; + else + b += (xm > 0 ? tol1 : -tol1); + fb = eval(b); + } + + if (out_iterations) + *out_iterations = max_iter; + return b; +} + +template +inline T itp_parameter(Eval&& eval, T a, T b, T fa, T fb, const T initial_guess, + const int max_iter, const T xtol, const T ftol, + int* out_iterations = nullptr, + bool* out_converged = nullptr) +{ + if (out_iterations) + *out_iterations = 0; + if (out_converged) + *out_converged = false; + + if (std::abs(fa) <= ftol) + { + if (out_converged) + *out_converged = true; + return a; + } + if (std::abs(fb) <= ftol) + { + if (out_converged) + *out_converged = true; + return b; + } + + if (fa * fb > T(0)) + throw std::domain_error("itp_parameter: no sign change in bracket [a, b]"); + + // Use the linear guess first to contract the bracket if possible. + const T t0 = std::clamp(initial_guess, a, b); + { + const T f0 = eval(t0); + if (std::abs(f0) <= ftol) + { + if (out_converged) + *out_converged = true; + return t0; + } + if (fa * f0 <= T(0)) + { + b = t0; + fb = f0; + } + else if (f0 * fb <= T(0)) + { + a = t0; + fa = f0; + } + } + + const T width0 = b - a; + const int n_max = std::max(0, static_cast(std::ceil(std::log2(width0 / xtol)))); + const T k1 = T(0.2) / std::max(width0, std::numeric_limits::epsilon()); + const T k2 = T(2); + const int iter_max = std::min(max_iter, n_max + 8); + + for (int iter = 0; iter < iter_max; ++iter) + { + const T width = b - a; + if (width <= xtol) + { + if (out_iterations) + *out_iterations = iter + 1; + if (out_converged) + *out_converged = true; + return T(0.5) * (a + b); + } + + const T m = T(0.5) * (a + b); + const T xf = (a * fb - b * fa) / (fb - fa); + const T sigma = (m >= xf) ? T(1) : T(-1); + + // Truncation: delta = min(k1 * width^k2, |m - xf|) + // Then xt = xf + sigma * delta (clamps between xf and m). + const T delta = std::min(k1 * std::pow(width, k2), std::abs(m - xf)); + const T xt = xf + sigma * delta; + + T r = xtol * std::ldexp(T(1), std::max(0, n_max - iter)) - T(0.5) * width; + r = std::max(T(0), r); + + T x = (std::abs(xt - m) <= r) ? xt : (m - sigma * r); + x = std::clamp(x, a, b); + const T fx = eval(x); + + if (std::abs(fx) <= ftol) + { + if (out_iterations) + *out_iterations = iter + 1; + if (out_converged) + *out_converged = true; + return x; + } + + if (fa * fx <= T(0)) + { + b = x; + fb = fx; + } + else + { + a = x; + fa = fx; + } + } + + if (out_iterations) + *out_iterations = iter_max; + return T(0.5) * (a + b); +} + +template +inline T newton_parameter(Eval&& eval, T a, T b, T fa, T fb, const T initial_guess, + const int max_iter, const T xtol, const T ftol, + int* out_iterations = nullptr, + bool* out_converged = nullptr) +{ + if (out_iterations) + *out_iterations = 0; + if (out_converged) + *out_converged = false; + + if (std::abs(fa) <= ftol) + { + if (out_converged) + *out_converged = true; + return a; + } + if (std::abs(fb) <= ftol) + { + if (out_converged) + *out_converged = true; + return b; + } + if (fa * fb > T(0)) + throw std::domain_error("newton_parameter: no sign change in bracket [a, b]"); + + T l = a; + T r = b; + T fl = fa; + T fr = fb; + T t = std::clamp(initial_guess, l, r); + const T eps = std::numeric_limits::epsilon(); + + for (int iter = 0; iter < max_iter; ++iter) + { + if ((r - l) <= xtol) + { + if (out_iterations) + *out_iterations = iter + 1; + if (out_converged) + *out_converged = true; + return T(0.5) * (l + r); + } + + const T f = eval(t); + if (std::abs(f) <= ftol) + { + if (out_iterations) + *out_iterations = iter + 1; + if (out_converged) + *out_converged = true; + return t; + } + + if (fl * f <= T(0)) + { + r = t; + fr = f; + } + else if (f * fr <= T(0)) + { + l = t; + fl = f; + } + + const T width = r - l; + T h = std::sqrt(eps) * std::max(T(1), std::abs(t)); + h = std::max(h, T(0.01) * width); + h = std::min(h, T(0.25) * width); + h = std::max(h, xtol); + + const T t_minus = std::max(l, t - h); + const T t_plus = std::min(r, t + h); + T t_candidate = T(0.5) * (l + r); + + if (t_plus > t_minus + xtol) + { + const T f_minus = eval(t_minus); + const T f_plus = eval(t_plus); + const T df = (f_plus - f_minus) / (t_plus - t_minus); + + if (std::isfinite(df) && std::abs(df) > T(10) * eps) + { + const T t_newton = t - f / df; + if (std::isfinite(t_newton) && t_newton > l && t_newton < r) + t_candidate = t_newton; + } + } + + if (!(t_candidate > l && t_candidate < r)) + t_candidate = T(0.5) * (l + r); + t = t_candidate; + } + + if (out_iterations) + *out_iterations = max_iter; + return t; +} + +template +inline RootSolveInfo find_root_parameter_info(const std::span p0, + const std::span p1, + Phi&& level_set, + const method root_method = method::linear, + const T level = T(0), + const int max_iter = 64, + const T xtol = T(1e-12), + const T ftol = T(1e-12)) +{ + if (p0.size() != p1.size()) + throw std::invalid_argument("find_root_parameter: inconsistent point dimensions"); + + RootSolveInfo info; + int eval_count = 0; + auto eval = [&](const T t) -> T + { + ++eval_count; + return evaluate_edge_function(level_set, p0, p1, t) - level; + }; + + T a = T(0); + T b = T(1); + T fa = eval(a); + T fb = eval(b); + const T a0 = a; + const T b0 = b; + const T fa0 = fa; + const T fb0 = fb; + + if (std::abs(fa) <= ftol) + { + info.t = T(0); + info.residual = std::abs(fa); + info.evaluations = eval_count; + info.converged = true; + return info; + } + if (std::abs(fb) <= ftol) + { + info.t = T(1); + info.residual = std::abs(fb); + info.evaluations = eval_count; + info.converged = true; + return info; + } + + const T t_linear = linear_root_parameter(fa, fb, T(0)); + if (root_method == method::linear) + { + const T f_linear = eval(t_linear); + info.t = t_linear; + info.residual = std::abs(f_linear); + info.evaluations = eval_count; + info.converged = (std::abs(f_linear) <= ftol); + return info; + } + + // Contract the bracket with the linear guess when possible. + bool have_f_linear = false; + T f_linear = T(0); + if (t_linear > T(0) && t_linear < T(1)) + { + f_linear = eval(t_linear); + have_f_linear = true; + if (std::abs(f_linear) <= ftol) + { + info.t = t_linear; + info.residual = std::abs(f_linear); + info.evaluations = eval_count; + info.converged = true; + return info; + } + if (fa * f_linear <= T(0)) + { + b = t_linear; + fb = f_linear; + } + else if (f_linear * fb <= T(0)) + { + a = t_linear; + fa = f_linear; + } + } + + if (fa * fb > T(0)) + { + if (!have_f_linear) + f_linear = eval(t_linear); + info.t = t_linear; + info.residual = std::abs(f_linear); + info.evaluations = eval_count; + return info; + } + + int iterations = 0; + bool converged = false; + T t = t_linear; + + if (root_method == method::brent) + { + t = brent_parameter(eval, a, b, fa, fb, max_iter, xtol, ftol, &iterations, &converged); + } + else if (root_method == method::itp) + { + t = itp_parameter(eval, a, b, fa, fb, t_linear, max_iter, xtol, ftol, &iterations, &converged); + } + else + { + t = newton_parameter(eval, a, b, fa, fb, t_linear, max_iter, xtol, ftol, &iterations, &converged); + } + + const T f = eval(t); + T f_abs = std::abs(f); + + // Safety fallback: if nonlinear solver returned a poor residual while we + // have a valid sign-changing bracket, finish with robust bisection. + auto bisect_on_bracket = [&](T left, T right, T f_left, T f_right) -> T + { + if (f_left * f_right > T(0)) + return t; + + T l = left; + T r = right; + T fl = f_left; + T fr = f_right; + T mid = T(0.5) * (l + r); + for (int k = 0; k < max_iter; ++k) + { + mid = T(0.5) * (l + r); + const T fm = eval(mid); + ++iterations; + if (std::abs(fm) <= ftol || std::abs(r - l) <= xtol) + return mid; + + if (fl * fm <= T(0)) + { + r = mid; + fr = fm; + } + else + { + l = mid; + fl = fm; + } + } + (void)fr; + return mid; + }; + + if (f_abs > ftol) + { + if (fa * fb <= T(0)) + { + t = bisect_on_bracket(a, b, fa, fb); + } + else if (fa0 * fb0 <= T(0)) + { + t = bisect_on_bracket(a0, b0, fa0, fb0); + } + f_abs = std::abs(eval(t)); + } + + info.t = t; + info.residual = f_abs; + info.iterations = iterations; + info.evaluations = eval_count; + info.converged = (f_abs <= ftol) || (converged && f_abs <= T(100) * ftol); + return info; +} + +template +inline T find_root_parameter(const std::span p0, const std::span p1, Phi&& level_set, + const method root_method = method::linear, + const T level = T(0), const int max_iter = 64, + const T xtol = T(1e-12), const T ftol = T(1e-12)) +{ + return find_root_parameter_info(p0, p1, std::forward(level_set), + root_method, level, max_iter, xtol, ftol).t; +} + +template +inline void find_root_point(const std::span p0, const std::span p1, + const std::span root_point, Phi&& level_set, + const method root_method = method::linear, + const T level = T(0), const int max_iter = 64, + const T xtol = T(1e-12), const T ftol = T(1e-12)) +{ + const T t = find_root_parameter(p0, p1, std::forward(level_set), + root_method, level, max_iter, xtol, ftol); + interpolate_point(p0, p1, t, root_point); +} + +template +inline void compute_case_intersections_from_edge_ids( + const std::span vertex_coordinates, const int gdim, + const std::span ls_values, + const int (*edges)[2], const int num_edges, + const std::span intersected_edge_ids, + std::vector& intersection_points, + EdgeMap& edge_ip_map) +{ + if (gdim <= 0 || gdim > 3) + throw std::invalid_argument("compute_case_intersections_from_edge_ids: gdim must be 1..3"); + + intersection_points.resize(intersected_edge_ids.size() * static_cast(gdim)); + + std::array p0 = {}; + std::array p1 = {}; + + for (std::size_t ip = 0; ip < intersected_edge_ids.size(); ++ip) + { + const int edge_id = intersected_edge_ids[ip]; + if (edge_id < 0 || edge_id >= num_edges) + throw std::invalid_argument("compute_case_intersections_from_edge_ids: invalid edge id"); + + const int v0 = edges[edge_id][0]; + const int v1 = edges[edge_id][1]; + + for (int j = 0; j < gdim; ++j) + { + p0[static_cast(j)] = vertex_coordinates[v0 * gdim + j]; + p1[static_cast(j)] = vertex_coordinates[v1 * gdim + j]; + } + + linear_intersection_point(std::span(p0.data(), static_cast(gdim)), + std::span(p1.data(), static_cast(gdim)), + ls_values[v0], ls_values[v1], + intersection_points, static_cast(ip * gdim), T(0)); + edge_ip_map[edge_id] = static_cast(ip); + } +} + +template +inline void compute_case_intersections_from_edge_mask( + const std::span vertex_coordinates, const int gdim, + const std::span ls_values, + const int (*edges)[2], const int num_edges, + const std::span intersected_edge_mask, + std::vector& intersection_points, + EdgeMap& edge_ip_map) +{ + if (gdim <= 0 || gdim > 3) + throw std::invalid_argument("compute_case_intersections_from_edge_mask: gdim must be 1..3"); + if (intersected_edge_mask.size() != static_cast(num_edges)) + throw std::invalid_argument("compute_case_intersections_from_edge_mask: invalid mask size"); + + intersection_points.clear(); + intersection_points.reserve(static_cast(num_edges) * static_cast(gdim)); + + std::array p0 = {}; + std::array p1 = {}; + + int ip = 0; + for (int edge_id = 0; edge_id < num_edges; ++edge_id) + { + if (intersected_edge_mask[static_cast(edge_id)] == 0) + continue; + + const int v0 = edges[edge_id][0]; + const int v1 = edges[edge_id][1]; + + for (int j = 0; j < gdim; ++j) + { + p0[static_cast(j)] = vertex_coordinates[v0 * gdim + j]; + p1[static_cast(j)] = vertex_coordinates[v1 * gdim + j]; + } + + const int offset = static_cast(intersection_points.size()); + intersection_points.resize(intersection_points.size() + static_cast(gdim)); + linear_intersection_point(std::span(p0.data(), static_cast(gdim)), + std::span(p1.data(), static_cast(gdim)), + ls_values[v0], ls_values[v1], intersection_points, offset, T(0)); + edge_ip_map[edge_id] = ip++; + } +} + +/// Check whether a reference point is inside the parent cell reference domain. +/// +/// @param x_ref reference coordinates, size = tdim +/// @param cell_type cell type defining the reference domain +/// @param tol tolerance for boundary proximity +template +inline bool is_inside_reference_domain( + std::span x_ref, + cutcells::cell::type cell_type, + T tol = T(1e-10)) +{ + using cutcells::cell::type; + switch (cell_type) + { + case type::interval: + { + return x_ref[0] >= -tol && x_ref[0] <= T(1) + tol; + } + case type::triangle: + { + const T x = x_ref[0]; + const T y = x_ref[1]; + return x >= -tol && y >= -tol && (x + y) <= T(1) + tol; + } + case type::tetrahedron: + { + const T x = x_ref[0]; + const T y = x_ref[1]; + const T z = x_ref[2]; + return x >= -tol && y >= -tol && z >= -tol && (x + y + z) <= T(1) + tol; + } + case type::quadrilateral: + { + return x_ref[0] >= -tol && x_ref[0] <= T(1) + tol + && x_ref[1] >= -tol && x_ref[1] <= T(1) + tol; + } + case type::hexahedron: + { + return x_ref[0] >= -tol && x_ref[0] <= T(1) + tol + && x_ref[1] >= -tol && x_ref[1] <= T(1) + tol + && x_ref[2] >= -tol && x_ref[2] <= T(1) + tol; + } + case type::prism: + { + // Prism: triangle in (x,y) x interval in z + const T x = x_ref[0]; + const T y = x_ref[1]; + const T z = x_ref[2]; + return x >= -tol && y >= -tol && (x + y) <= T(1) + tol + && z >= -tol && z <= T(1) + tol; + } + case type::pyramid: + { + // Pyramid: x,y in [0, 1-z], z in [0, 1] + const T x = x_ref[0]; + const T y = x_ref[1]; + const T z = x_ref[2]; + const T limit = T(1) - z; + return z >= -tol && z <= T(1) + tol + && x >= -tol && x <= limit + tol + && y >= -tol && y <= limit + tol; + } + default: + return false; + } +} + +/// Solve phi(x_s + t*d) = 0 for t using Newton → Brent → bisection fallback chain. +/// +/// @param eval_phi callable (const T* x_ref, int tdim) -> T +/// @param eval_grad callable (const T* x_ref, int tdim, T* grad_out) -> void (reference-space gradient) +/// @param x_s starting point in reference coordinates, size = tdim +/// @param d ray direction in reference coordinates, size = tdim +/// @param cell_type reference domain for the domain-exit check +/// @param tdim topological dimension +/// @param h_local local entity size for scale-aware bracket probing (segment length or face diameter) +/// @param tol convergence tolerance +template +inline RayRootResult find_ray_root_one_direction( + EvalPhi&& eval_phi, + EvalGrad&& eval_grad, + std::span x_s, + std::span d, + cutcells::cell::type cell_type, + int tdim, + T h_local, + int sign, + const RaySearchOptions& opts) +{ + RayRootResult result; + if (sign != 1 && sign != -1) + throw std::invalid_argument("find_ray_root_one_direction: sign must be +1 or -1"); + if (tdim <= 0 || tdim > 3) + throw std::invalid_argument("find_ray_root_one_direction: tdim must be in [1, 3]"); + + // Compute norm of direction + T d_norm = T(0); + for (int k = 0; k < tdim; ++k) + d_norm += d[k] * d[k]; + d_norm = std::sqrt(d_norm); + + if (d_norm < opts.ftol) + { + result.gradient_degenerate = true; + return result; + } + + // Normalized direction + std::vector d_hat(static_cast(tdim)); + for (int k = 0; k < tdim; ++k) + d_hat[static_cast(k)] = d[k] / d_norm; + + // Helper: evaluate phi at x_s + t * d_hat + std::vector x_tmp(static_cast(tdim)); + const auto phi_at_t = [&](T t_val) -> T + { + for (int k = 0; k < tdim; ++k) + x_tmp[static_cast(k)] = x_s[k] + t_val * d_hat[static_cast(k)]; + ++result.evaluations; + return eval_phi(x_tmp.data(), tdim); + }; + + T t_a = T(0); + T f_a = phi_at_t(T(0)); + + // Check if already at root + if (std::abs(f_a) <= opts.ftol) + { + result.t = T(0); + result.phi_at_root = f_a; + result.dist = T(0); + result.residual = std::abs(f_a); + result.converged = true; + result.valid = true; + for (int k = 0; k < tdim; ++k) + result.x_ref[static_cast(k)] = x_s[static_cast(k)]; + return result; + } + + // Find a bracket + T t_b = T(0); + T f_b = T(0); + bool have_bracket = false; + + for (const T scale : opts.probe_scales) + { + const T unclamped = scale * h_local; + const T max_probe = std::isfinite(opts.max_probe_distance) + ? std::min(unclamped, opts.max_probe_distance) + : unclamped; + const T t_probe = static_cast(sign) * max_probe; + // Check that probe is inside reference domain + for (int k = 0; k < tdim; ++k) + x_tmp[static_cast(k)] = x_s[k] + t_probe * d_hat[static_cast(k)]; + if (!is_inside_reference_domain( + std::span(x_tmp.data(), static_cast(tdim)), + cell_type, opts.domain_tol)) + continue; + + const T f_probe = phi_at_t(t_probe); + if (std::abs(f_probe) <= opts.ftol) + { + result.t = t_probe; + result.phi_at_root = f_probe; + result.dist = std::abs(t_probe); + result.residual = std::abs(f_probe); + result.converged = true; + result.valid = true; + for (int k = 0; k < tdim; ++k) + result.x_ref[static_cast(k)] + = x_s[static_cast(k)] + t_probe * d_hat[static_cast(k)]; + return result; + } + + if (f_a * f_probe <= T(0)) + { + t_b = t_probe; + f_b = f_probe; + have_bracket = true; + break; + } + + // Keep the smaller-magnitude endpoint as t_a + if (std::abs(f_probe) < std::abs(f_a)) + { + t_a = t_probe; + f_a = f_probe; + } + } + + if (!have_bracket) + { + // No sign change found; return best estimate + result.t = t_a; + result.phi_at_root = f_a; + result.dist = std::abs(t_a); + result.residual = std::abs(f_a); + result.converged = false; + return result; + } + + // Ensure t_a < t_b + if (t_a > t_b) + { + std::swap(t_a, t_b); + std::swap(f_a, f_b); + } + + // Newton iterations with bracket safety + const int max_iter = opts.max_iter; + T t = T(0.5) * (t_a + t_b); + // Try initial Newton step from t_a + { + std::vector grad(static_cast(tdim)); + for (int k = 0; k < tdim; ++k) + x_tmp[static_cast(k)] = x_s[k] + t_a * d_hat[static_cast(k)]; + eval_grad(x_tmp.data(), tdim, grad.data()); + T dphidt = T(0); + for (int k = 0; k < tdim; ++k) + dphidt += grad[static_cast(k)] * d_hat[static_cast(k)]; + if (std::abs(dphidt) > T(1e-14)) + { + const T t_newton = t_a - f_a / dphidt; + if (t_newton > t_a && t_newton < t_b) + t = t_newton; + } + } + + // Brent fallback (unclamped — ray parameter can be negative) + int brent_iters = 0; + bool brent_converged = false; + const T t_brent = brent_solve( + phi_at_t, t_a, t_b, f_a, f_b, + max_iter, opts.xtol, opts.ftol, &brent_iters, &brent_converged); + result.iterations += brent_iters; + t = t_brent; + + const T f_final = phi_at_t(t); + result.t = t; + result.phi_at_root = f_final; + result.dist = std::abs(t); + result.residual = std::abs(f_final); + result.converged = (result.residual <= opts.ftol) || brent_converged; + + // Check domain exit + for (int k = 0; k < tdim; ++k) + x_tmp[static_cast(k)] = x_s[k] + t * d_hat[static_cast(k)]; + result.domain_exit = !is_inside_reference_domain( + std::span(x_tmp.data(), static_cast(tdim)), + cell_type, opts.domain_tol); + result.valid = result.converged && !result.domain_exit; + for (int k = 0; k < tdim; ++k) + result.x_ref[static_cast(k)] = x_tmp[static_cast(k)]; + + return result; +} + +template +inline RayRootResult find_ray_root( + EvalPhi&& eval_phi, + EvalGrad&& eval_grad, + std::span x_s, + std::span d, + cutcells::cell::type cell_type, + int tdim, + T h_local, + const RaySearchOptions& opts) +{ + auto pos = find_ray_root_one_direction( + eval_phi, eval_grad, x_s, d, cell_type, tdim, h_local, +1, opts); + auto neg = find_ray_root_one_direction( + eval_phi, eval_grad, x_s, d, cell_type, tdim, h_local, -1, opts); + + RayRootResult out; + out.evaluations = pos.evaluations + neg.evaluations; + out.iterations = pos.iterations + neg.iterations; + out.gradient_degenerate = pos.gradient_degenerate && neg.gradient_degenerate; + + out.pos_valid = pos.valid; + out.neg_valid = neg.valid; + out.pos_t = pos.t; + out.neg_t = neg.t; + out.ambiguous_two_sided = pos.valid && neg.valid; + + if (pos.valid && neg.valid) + out = (std::abs(pos.t) <= std::abs(neg.t)) ? pos : neg; + else if (pos.valid) + out = pos; + else if (neg.valid) + out = neg; + else + out = (pos.residual <= neg.residual) ? pos : neg; + + out.pos_valid = pos.valid; + out.neg_valid = neg.valid; + out.pos_t = pos.t; + out.neg_t = neg.t; + out.ambiguous_two_sided = pos.valid && neg.valid; + out.evaluations = pos.evaluations + neg.evaluations; + out.iterations = pos.iterations + neg.iterations; + out.gradient_degenerate = pos.gradient_degenerate && neg.gradient_degenerate; + return out; +} + +} // namespace cutcells::cell::edge_root diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp new file mode 100644 index 0000000..b580007 --- /dev/null +++ b/cpp/src/ho_cut_mesh.cpp @@ -0,0 +1,424 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "ho_cut_mesh.h" +#include "cell_certification.h" + +#include +#include +#include + +namespace cutcells +{ +namespace +{ + +/// Gather vertex level-set values for the given cell. +template +void gather_vertex_ls_values(const MeshView& mesh, + const LevelSetFunction& ls, + I cell_id, int nv, + std::vector& out) +{ + out.resize(static_cast(nv)); + + if (ls.has_dof_values() && ls.has_mesh_data()) + { + auto dofs = ls.mesh_data->cell_dofs_span(cell_id); + for (int v = 0; v < nv; ++v) + out[static_cast(v)] = + ls.dof_values[static_cast( + dofs[static_cast(v)])]; + } + else if (ls.has_value()) + { + for (int v = 0; v < nv; ++v) + { + const I node_id = mesh.cell_node(cell_id, static_cast(v)); + out[static_cast(v)] = + ls.value(mesh.node(node_id), cell_id); + } + } + else + { + throw std::runtime_error( + "cut: LevelSetFunction has neither nodal_values, " + "dof_values, nor an analytical value function"); + } +} + +} // anonymous namespace + +// ===================================================================== +// cut() — single level set +// ===================================================================== + +template +std::pair, BackgroundMeshData> +cut(const MeshView& mesh, const LevelSetFunction& ls) +{ + if (!mesh.has_cell_types()) + throw std::runtime_error("cut: MeshView must have cell types"); + + const I ncells = mesh.num_cells(); + + // --- BackgroundMeshData --- + BackgroundMeshData bg; + bg.mesh = &mesh; + bg.level_set_names.push_back(ls.name); + bg.num_cells = static_cast(ncells); + bg.num_level_sets = 1; + bg.cell_domains.assign(static_cast(ncells), + cell::domain::unset); + bg.cell_to_cut_index.assign(static_cast(ncells), -1); + + // --- HOCutCells --- + HOCutCells hc; + hc.gdim = mesh.gdim; + hc.tdim = cell::get_tdim(mesh.cell_type(I(0))); + hc.ls_offsets.push_back(0); + + std::vector ls_vertex_vals; + + for (I ci = 0; ci < ncells; ++ci) + { + const cell::type ctype = mesh.cell_type(ci); + const int nv = cell::get_num_vertices(ctype); + + gather_vertex_ls_values(mesh, ls, ci, nv, ls_vertex_vals); + + const cell::domain dom = cell::classify_cell_domain( + std::span(ls_vertex_vals.data(), + static_cast(nv))); + bg.cell_domains[static_cast(ci)] = dom; + + if (dom != cell::domain::intersected) + continue; + + const int cut_idx = hc.num_cut_cells(); + bg.cell_to_cut_index[static_cast(ci)] = cut_idx; + + // LevelSetCell + hc.level_set_cells.push_back(make_cell_level_set(ls, ci)); + hc.ls_offsets.push_back( + static_cast(hc.level_set_cells.size())); + + // AdaptCell + AdaptCell ac = make_adapt_cell(mesh, ci); + build_edges(ac); + certify_refine_and_process_ready_cells( + ac, hc.level_set_cells.back(), /*level_set_id=*/0, + /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); + hc.adapt_cells.push_back(std::move(ac)); + + // Single LS: bit 0 is always set. + hc.active_level_set_mask.push_back(std::uint64_t(1)); + + hc.parent_cell_ids.push_back(ci); + } + + return {std::move(hc), std::move(bg)}; +} + +// ===================================================================== +// cut() — multiple level sets +// ===================================================================== + +template +std::pair, BackgroundMeshData> +cut(const MeshView& mesh, + const std::vector>& level_sets) +{ + if (!mesh.has_cell_types()) + throw std::runtime_error("cut: MeshView must have cell types"); + if (level_sets.empty()) + throw std::runtime_error("cut: no level sets provided"); + if (level_sets.size() > 64) + throw std::runtime_error("cut: more than 64 level sets not supported"); + + const I ncells = mesh.num_cells(); + const int nls = static_cast(level_sets.size()); + + // --- BackgroundMeshData --- + BackgroundMeshData bg; + bg.mesh = &mesh; + bg.num_cells = static_cast(ncells); + bg.num_level_sets = nls; + bg.cell_domains.assign( + static_cast(nls) * static_cast(ncells), + cell::domain::unset); + bg.cell_to_cut_index.assign(static_cast(ncells), -1); + for (const auto& ls : level_sets) + bg.level_set_names.push_back(ls.name); + + // --- HOCutCells --- + HOCutCells hc; + hc.gdim = mesh.gdim; + hc.tdim = cell::get_tdim(mesh.cell_type(I(0))); + hc.ls_offsets.push_back(0); + + std::vector ls_vertex_vals; + + for (I ci = 0; ci < ncells; ++ci) + { + const cell::type ctype = mesh.cell_type(ci); + const int nv = cell::get_num_vertices(ctype); + + // Classify each level set individually; track if any intersects. + bool any_intersected = false; + std::vector intersected_ls_indices; + std::vector intersected_vertex_vals; + intersected_ls_indices.reserve(static_cast(nls)); + intersected_vertex_vals.reserve( + static_cast(nls) * static_cast(nv)); + for (int li = 0; li < nls; ++li) + { + gather_vertex_ls_values(mesh, level_sets[static_cast(li)], + ci, nv, ls_vertex_vals); + + const cell::domain dom = cell::classify_cell_domain( + std::span(ls_vertex_vals.data(), + static_cast(nv))); + + bg.cell_domains[static_cast( + li * static_cast(ncells) + static_cast(ci))] = dom; + + if (dom == cell::domain::intersected) + { + any_intersected = true; + intersected_ls_indices.push_back(li); + intersected_vertex_vals.insert(intersected_vertex_vals.end(), + ls_vertex_vals.begin(), + ls_vertex_vals.end()); + } + } + + if (!any_intersected) + continue; + + const int cut_idx = hc.num_cut_cells(); + bg.cell_to_cut_index[static_cast(ci)] = cut_idx; + + // Build AdaptCell once per cell. + AdaptCell ac = make_adapt_cell(mesh, ci); + build_edges(ac); + + // Build LevelSetCell and fill vertex signs for each intersecting LS. + std::uint64_t cell_active_mask = 0; + for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) + { + const int li = intersected_ls_indices[k]; + const auto& ls_i = level_sets[static_cast(li)]; + const T* vals = intersected_vertex_vals.data() + + k * static_cast(nv); + + hc.level_set_cells.push_back(make_cell_level_set(ls_i, ci)); + certify_refine_and_process_ready_cells( + ac, hc.level_set_cells.back(), li, + /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); + + cell_active_mask |= std::uint64_t(1) << li; + } + hc.ls_offsets.push_back( + static_cast(hc.level_set_cells.size())); + hc.active_level_set_mask.push_back(cell_active_mask); + + hc.adapt_cells.push_back(std::move(ac)); + hc.parent_cell_ids.push_back(ci); + } + + return {std::move(hc), std::move(bg)}; +} + +// ===================================================================== +// select_part() +// ===================================================================== + +template +HOMeshPart select_part(const HOCutCells& cut_cells, + const BackgroundMeshData& bg, + std::string_view expr_str) +{ + HOMeshPart part; + part.cut_cells = &cut_cells; + part.bg = &bg; + + // Parse and compile the expression. + part.expr = parse_selection_expr(expr_str); + compile_selection_expr(part.expr, bg.level_set_names); + part.dim = infer_selection_dim(part.expr, cut_cells.tdim); + + const bool is_volume = (part.dim == cut_cells.tdim); + + // --- Uncut cells: scan cell_domains --- + const int ncells = bg.num_cells; + for (I ci = 0; ci < static_cast(ncells); ++ci) + { + if (bg.cell_to_cut_index[static_cast(ci)] >= 0) + continue; // cut cell, handled below + + // For an uncut cell, every vertex has the same sign for each LS. + // Check if the cell domain is compatible with the expression. + bool match = true; + for (const auto& clause : part.expr.clauses) + { + const int li = clause.level_set_index; + const cell::domain dom = bg.domain(li, ci); + + switch (clause.relation) + { + case Relation::LessThan: + if (dom != cell::domain::inside) match = false; + break; + case Relation::GreaterThan: + if (dom != cell::domain::outside) match = false; + break; + case Relation::EqualTo: + // An uncut (non-intersected) cell has no zero interface. + match = false; + break; + } + if (!match) break; + } + + if (match) + part.uncut_cell_ids.push_back(ci); + } + + // --- Cut cells: check vertex bitmasks for volume selections, + // or entity inventory for interface selections --- + const int ncut = cut_cells.num_cut_cells(); + for (int k = 0; k < ncut; ++k) + { + const auto& ac = cut_cells.adapt_cells[static_cast(k)]; + const int nv = ac.n_vertices(); + + if (is_volume) + { + // Volume selection: a cut cell contributes if at least one vertex + // satisfies all sign constraints. + // Actually, a cut cell always contributes to a volume part + // (it has sub-entities on the requested side), so we include it. + // More refined: check if the required bitmask pattern is present. + bool has_matching_vertex = false; + for (int v = 0; v < nv; ++v) + { + const auto zm = ac.zero_mask_per_vertex[static_cast(v)]; + const auto nm = ac.negative_mask_per_vertex[static_cast(v)]; + + // Positive bit: not zero, not negative. + const auto pm = ~zm & ~nm; + + bool vertex_ok = true; + + // Check if zero requirements are met (for EqualTo clauses). + if ((zm & part.expr.zero_required) != part.expr.zero_required) + vertex_ok = false; + + // Check if negative requirements are met. + if ((nm & part.expr.negative_required) != part.expr.negative_required) + vertex_ok = false; + + // Check if positive requirements are met. + if ((pm & part.expr.positive_required) != part.expr.positive_required) + vertex_ok = false; + + if (vertex_ok) + { + has_matching_vertex = true; + break; + } + } + + if (has_matching_vertex) + part.cut_cell_ids.push_back(static_cast(k)); + } + else + { + // Interface selection: for now include any cut cell that has + // the required zero level set(s) intersecting it. + // A cell is included if: + // - the zero_required LSs actually cut the cell + // - the sign constraints on the non-zero LSs are satisfiable + // (at least one vertex per non-zero constraint). + bool has_zero_ls = true; + for (int v = 0; v < nv && has_zero_ls; ++v) + { + // Check eventually; for now, if the LS is marked as + // intersected in cell_domains, the zero surface exists. + } + + // Check that the intersecting LS indices match zero_required. + bool zero_ok = true; + for (const auto& clause : part.expr.clauses) + { + if (clause.relation != Relation::EqualTo) continue; + const int li = clause.level_set_index; + const I bg_cell = cut_cells.parent_cell_ids[static_cast(k)]; + if (bg.domain(li, bg_cell) != cell::domain::intersected) + { + zero_ok = false; + break; + } + } + + if (zero_ok) + part.cut_cell_ids.push_back(static_cast(k)); + } + } + + part.cut_only = !is_volume; + + return part; +} + +// --------------------------------------------------------------------------- +// Explicit template instantiations +// --------------------------------------------------------------------------- + +// cut() single LS +template std::pair, BackgroundMeshData> +cut(const MeshView&, const LevelSetFunction&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const LevelSetFunction&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const LevelSetFunction&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const LevelSetFunction&); + +// cut() multi LS +template std::pair, BackgroundMeshData> +cut(const MeshView&, const std::vector>&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const std::vector>&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const std::vector>&); + +template std::pair, BackgroundMeshData> +cut(const MeshView&, const std::vector>&); + +// select_part() +template HOMeshPart +select_part(const HOCutCells&, const BackgroundMeshData&, + std::string_view); + +template HOMeshPart +select_part(const HOCutCells&, const BackgroundMeshData&, + std::string_view); + +template HOMeshPart +select_part(const HOCutCells&, const BackgroundMeshData&, + std::string_view); + +template HOMeshPart +select_part(const HOCutCells&, const BackgroundMeshData&, + std::string_view); + +} // namespace cutcells diff --git a/cpp/src/ho_cut_mesh.h b/cpp/src/ho_cut_mesh.h new file mode 100644 index 0000000..a466746 --- /dev/null +++ b/cpp/src/ho_cut_mesh.h @@ -0,0 +1,172 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + +#include "adapt_cell.h" +#include "cell_flags.h" +#include "level_set.h" +#include "level_set_cell.h" +#include "mesh_view.h" +#include "selection_expr.h" + +namespace cutcells +{ + +// ===================================================================== +// BackgroundMeshData — mesh-wide metadata produced during cutting +// ===================================================================== + +/// Mesh-wide metadata produced during cutting. +/// +/// Holds a non-owning reference to the background mesh plus: +/// - level-set name registry +/// - per-cell, per-level-set domain classification +/// - cell_to_cut_index lookup +template +struct BackgroundMeshData +{ + /// Non-owning reference to the background mesh. + const MeshView* mesh = nullptr; + + /// Level-set name registry. + /// Index in this vector = bit position in AdaptCell bitmasks. + std::vector level_set_names; + + /// Per-cell, per-level-set domain classification. + /// Flat storage: cell_domains[ls_index * num_cells + cell_id]. + std::vector cell_domains; + int num_cells = 0; ///< = mesh->num_cells() + int num_level_sets = 0; + + /// Lookup: background cell id → index into HOCutCells arrays. + /// -1 for uncut cells. Size = num_cells. + std::vector cell_to_cut_index; + + /// Access the domain of a specific (level_set, cell) pair. + cell::domain domain(int ls_index, I cell_id) const + { + return cell_domains[static_cast( + ls_index * num_cells + static_cast(cell_id))]; + } +}; + +// ===================================================================== +// HOCutCells — pure cut-cell storage (intersected cells only) +// ===================================================================== + +/// Pure storage of intersected cells. +/// No mesh reference, no uncut cells, no classification. +template +struct HOCutCells +{ + int gdim = 0; + int tdim = 0; + + /// Background-mesh cell ids for the cut cells. + /// parent_cell_ids[k] indexes the background mesh for the k-th entry. + std::vector parent_cell_ids; + + /// AdaptCell (reference-space topology + vertex signs) for each cut cell. + std::vector> adapt_cells; + + /// Per cut-cell level-set data, flat CSR layout: + /// level_set_cells[ls_offsets[k] .. ls_offsets[k+1]] for k-th cut cell. + std::vector> level_set_cells; + std::vector ls_offsets; ///< size = num_cut_cells + 1 + + /// Bitmask of active (intersecting) level sets for each cut cell. + /// Bit li set ↔ level set li has domain::intersected for the k-th cut cell. + /// Size = num_cut_cells. + std::vector active_level_set_mask; + + /// Number of intersected cells. + int num_cut_cells() const + { + return static_cast(parent_cell_ids.size()); + } +}; + +// ===================================================================== +// HOMeshPart — pure view/extraction layer over HOCutCells + BackgroundMeshData +// ===================================================================== + +/// View over HOCutCells + BackgroundMeshData, filtered by a SelectionExpr. +/// +/// Stores no data of its own beyond the selection results. +template +struct HOMeshPart +{ + /// Non-owning references. + const HOCutCells* cut_cells = nullptr; + const BackgroundMeshData* bg = nullptr; + + /// The selection expression (compiled). + SelectionExpr expr; + + /// Inferred entity dimension of the selection. + int dim = -1; + + /// Indices into cut_cells->adapt_cells for cells with matching sub-entities. + std::vector cut_cell_ids; + + /// Background cell ids of uncut cells matching the selection. + std::vector uncut_cell_ids; + + /// true: iterate only cut cells (quadrature). + /// false: iterate both cut + uncut (visualization). + bool cut_only = false; +}; + +// ===================================================================== +// Factory: cut() — produces both HOCutCells + BackgroundMeshData +// ===================================================================== + +/// Build HOCutCells and BackgroundMeshData from a mesh and a single level set. +/// +/// Iterates all background cells, classifies each by vertex-sign pattern, +/// and for intersected cells creates LevelSetCell + AdaptCell pairs. +/// +/// @param mesh Background mesh. Must have cell types. +/// @param ls Level-set function. +/// @return pair of (HOCutCells, BackgroundMeshData). +template +std::pair, BackgroundMeshData> +cut(const MeshView& mesh, + const LevelSetFunction& ls); + +/// Build HOCutCells and BackgroundMeshData from a mesh and multiple level sets. +/// +/// @param mesh Background mesh. +/// @param level_sets Vector of level-set functions. +/// @return pair of (HOCutCells, BackgroundMeshData). +template +std::pair, BackgroundMeshData> +cut(const MeshView& mesh, + const std::vector>& level_sets); + +// ===================================================================== +// select_part() — builds HOMeshPart +// ===================================================================== + +/// Build an HOMeshPart by filtering HOCutCells + BackgroundMeshData +/// with a selection expression string. +/// +/// @param cut_cells Intersected cell storage. +/// @param bg Mesh-wide metadata. +/// @param expr_str Selection expression, e.g. "phi < 0", "phi1 = 0 and phi2 < 0". +/// @return populated HOMeshPart. +template +HOMeshPart select_part(const HOCutCells& cut_cells, + const BackgroundMeshData& bg, + std::string_view expr_str); + +} // namespace cutcells diff --git a/cpp/src/level_set.cpp b/cpp/src/level_set.cpp index f48ad17..40472cc 100644 --- a/cpp/src/level_set.cpp +++ b/cpp/src/level_set.cpp @@ -14,7 +14,9 @@ #include #include +#include "cell_topology.h" #include "cell_types.h" +#include "reference_cell.h" namespace cutcells { @@ -123,8 +125,7 @@ cell::type infer_cell_type(const MeshView& mesh, I cell_id) { if (mesh.has_cell_types()) { - return cell::map_vtk_type_to_cell_type( - static_cast(mesh.cell_type(cell_id))); + return mesh.cell_type(cell_id); } const I n = mesh.cell_num_nodes(cell_id); @@ -155,32 +156,6 @@ cell::type infer_cell_type(const MeshView& mesh, I cell_id) throw std::runtime_error("create_level_set_mesh_data: unsupported cell type inference from MeshView"); } -inline std::vector vtk_to_basix_vertex_permutation(cell::type ctype, bool has_vtk_order) -{ - if (!has_vtk_order) - return {}; - - switch (ctype) - { - case cell::type::interval: - return {0, 1}; - case cell::type::triangle: - return {0, 1, 2}; - case cell::type::quadrilateral: - return {0, 1, 3, 2}; - case cell::type::tetrahedron: - return {0, 1, 2, 3}; - case cell::type::hexahedron: - return {0, 1, 3, 2, 4, 5, 7, 6}; - case cell::type::prism: - return {0, 1, 2, 3, 4, 5}; - case cell::type::pyramid: - return {0, 1, 2, 3, 4}; - default: - throw std::runtime_error("create_level_set_mesh_data: unsupported cell type"); - } -} - template std::vector cell_vertices_in_basix_order(const MeshView& mesh, I cell_id, cell::type ctype) @@ -193,15 +168,15 @@ std::vector cell_vertices_in_basix_order(const MeshView& mesh, I cell_i "create_level_set_mesh_data: MeshView cell connectivity must contain only corner vertices"); } - const std::vector perm = vtk_to_basix_vertex_permutation(ctype, mesh.has_cell_types()); std::vector verts; verts.reserve(static_cast(nverts)); - if (perm.empty()) + if (!mesh.vtk_vertex_order) { verts.insert(verts.end(), nodes.begin(), nodes.end()); } else { + const auto perm = cell::vtk_to_basix_vertex_permutation(ctype); for (int i = 0; i < nverts; ++i) verts.push_back(nodes[static_cast(perm[static_cast(i)])]); } @@ -230,40 +205,6 @@ struct FaceDef std::array verts = {-1, -1, -1, -1}; }; -inline std::span> basix_edges(cell::type ctype) -{ - static constexpr std::array, 1> interval = {{{0, 1}}}; - static constexpr std::array, 3> triangle = {{{1, 2}, {0, 2}, {0, 1}}}; - static constexpr std::array, 4> quadrilateral = {{{0, 1}, {0, 2}, {1, 3}, {2, 3}}}; - static constexpr std::array, 6> tetrahedron = {{{2, 3}, {1, 3}, {1, 2}, {0, 3}, {0, 2}, {0, 1}}}; - static constexpr std::array, 12> hexahedron = {{ - {0, 1}, {2, 3}, {0, 2}, {1, 3}, - {4, 5}, {6, 7}, {4, 6}, {5, 7}, - {0, 4}, {1, 5}, {2, 6}, {3, 7} - }}; - static constexpr std::array, 9> prism = {{ - {0, 1}, {0, 2}, {1, 2}, - {0, 3}, {1, 4}, {2, 5}, - {3, 4}, {3, 5}, {4, 5} - }}; - static constexpr std::array, 8> pyramid = {{ - {0, 1}, {0, 3}, {1, 2}, {2, 3}, - {0, 4}, {1, 4}, {2, 4}, {3, 4} - }}; - - switch (ctype) - { - case cell::type::interval: return std::span(interval); - case cell::type::triangle: return std::span(triangle); - case cell::type::quadrilateral: return std::span(quadrilateral); - case cell::type::tetrahedron: return std::span(tetrahedron); - case cell::type::hexahedron: return std::span(hexahedron); - case cell::type::prism: return std::span(prism); - case cell::type::pyramid: return std::span(pyramid); - default: return {}; - } -} - inline std::span> basix_faces(cell::type ctype) { static constexpr std::array, 4> tetrahedron = {{ @@ -847,7 +788,7 @@ void append_cell_dofs(const MeshView& mesh, LevelSetMeshData& out, return; } - const auto edges = basix_edges(ctype); + const auto edges = cell::edges(ctype); for (std::size_t e = 0; e < edges.size(); ++e) { const I a = verts[static_cast(edges[e][0])]; @@ -953,7 +894,7 @@ void validate_mesh_data_args(int gdim, int tdim, int degree, std::span dof_coordinates, std::span cell_dofs, std::span cell_offsets, - std::span cell_types) + std::span cell_types) { if (gdim <= 0) throw std::runtime_error("create_level_set_mesh_data: gdim must be positive"); @@ -1007,10 +948,7 @@ LevelSetMeshData create_level_set_mesh_data( append_cell_dofs(mesh, out, cell_id, ctype, verts, degree, edge_ids, edge_first_dof, face_ids, faces); - if (mesh.has_cell_types()) - out.cell_types.push_back(mesh.cell_type(cell_id)); - else - out.cell_types.push_back(static_cast(cell::map_cell_type_to_vtk(ctype))); + out.cell_types.push_back(ctype); out.cell_offsets.push_back(static_cast(out.cell_dofs.size())); } @@ -1025,7 +963,7 @@ LevelSetMeshData create_level_set_mesh_data( std::span dof_coordinates, std::span cell_dofs, std::span cell_offsets, - std::span cell_types) + std::span cell_types) { validate_mesh_data_args(gdim, tdim, degree, dof_coordinates, cell_dofs, cell_offsets, cell_types); @@ -1044,7 +982,8 @@ LevelSetMeshData create_level_set_mesh_data( template LevelSetFunction create_level_set_function( std::shared_ptr> mesh_data, - std::span dof_values) + std::span dof_values, + std::string name) { if (!mesh_data) throw std::runtime_error("create_level_set_function: mesh_data must not be null"); @@ -1061,6 +1000,7 @@ LevelSetFunction create_level_set_function( auto owned_values = std::make_shared>(dof_values.begin(), dof_values.end()); LevelSetFunction ls; + ls.name = std::move(name); ls.type = LevelSetType::Polynomial; ls.gdim = mesh_data->gdim; ls.mesh_data = std::move(mesh_data); @@ -1079,19 +1019,21 @@ template LevelSetMeshData create_level_set_mesh_data( std::span dof_coordinates, std::span cell_dofs, std::span cell_offsets, - std::span cell_types); + std::span cell_types); template LevelSetMeshData create_level_set_mesh_data( int gdim, int tdim, int degree, std::span dof_coordinates, std::span cell_dofs, std::span cell_offsets, - std::span cell_types); + std::span cell_types); template LevelSetFunction create_level_set_function( std::shared_ptr> mesh_data, - std::span dof_values); + std::span dof_values, + std::string name); template LevelSetFunction create_level_set_function( std::shared_ptr> mesh_data, - std::span dof_values); + std::span dof_values, + std::string name); } // namespace cutcells diff --git a/cpp/src/level_set.h b/cpp/src/level_set.h index 2626fa6..6e40c34 100644 --- a/cpp/src/level_set.h +++ b/cpp/src/level_set.h @@ -13,6 +13,7 @@ #include #include +#include "cell_types.h" #include "mesh_view.h" namespace cutcells @@ -41,7 +42,7 @@ struct LevelSetMeshData std::vector cell_offsets; // cell types, one per cell when available. - std::vector cell_types; + std::vector cell_types; // Provenance aligned with AdaptCell: // 0 = parent vertex, 1 = parent edge, 2 = parent face, 3 = parent cell interior. @@ -100,6 +101,8 @@ struct LevelSetFunction { std::string name = "phi"; + LevelSetType type = LevelSetType::Analytical; + int gdim = 0; // cell_id == -1 means "unknown / not provided" // The value and gradient of the level set function in physical coordinates (x) @@ -117,9 +120,6 @@ struct LevelSetFunction // Optional keep-alive anchor for Python / external memory. std::shared_ptr owner; - LevelSetType type = LevelSetType::Analytical; - int gdim = 0; - bool has_value() const { return static_cast(value_fn); @@ -179,11 +179,12 @@ LevelSetMeshData create_level_set_mesh_data( std::span dof_coordinates, std::span cell_dofs, std::span cell_offsets, - std::span cell_types = {}); + std::span cell_types = {}); template LevelSetFunction create_level_set_function( std::shared_ptr> mesh_data, - std::span dof_values); + std::span dof_values, + std::string name = "phi"); } // namespace cutcells diff --git a/cpp/src/level_set_cell.cpp b/cpp/src/level_set_cell.cpp new file mode 100644 index 0000000..a9c1e8e --- /dev/null +++ b/cpp/src/level_set_cell.cpp @@ -0,0 +1,219 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "level_set_cell.h" + +#include +#include + +#include "bernstein.h" +#include "mapping.h" + +namespace cutcells +{ +namespace +{ + +/// Determine the cell::type for a cell in the level-set mesh data. +/// Uses cell_types array if available, otherwise infers from tdim and +/// number of DOFs per cell (assuming equispaced Lagrange). +template +cell::type infer_ls_cell_type(const LevelSetMeshData& md, I cell_id) +{ + if (!md.cell_types.empty()) + { + return md.cell_types[static_cast(cell_id)]; + } + + // Infer from tdim and number of DOFs per cell + const I ndofs = md.cell_num_dofs(cell_id); + const int n = md.degree; + + switch (md.tdim) + { + case 1: + return cell::type::interval; + case 2: + { + // triangle: C(n+2,2) = (n+1)(n+2)/2 + // quad: (n+1)^2 + int tri_dofs = (n + 1) * (n + 2) / 2; + if (ndofs == static_cast(tri_dofs)) + return cell::type::triangle; + else + return cell::type::quadrilateral; + } + case 3: + { + // tet: C(n+3,3) = (n+1)(n+2)(n+3)/6 + // hex: (n+1)^3 + int tet_dofs = (n + 1) * (n + 2) * (n + 3) / 6; + int hex_dofs = (n + 1) * (n + 1) * (n + 1); + if (ndofs == static_cast(tet_dofs)) + return cell::type::tetrahedron; + else if (ndofs == static_cast(hex_dofs)) + return cell::type::hexahedron; + else + { + throw std::runtime_error( + "make_cell_level_set: cannot infer cell type from " + + std::to_string(ndofs) + " DOFs in 3D"); + } + } + default: + throw std::runtime_error( + "make_cell_level_set: unsupported tdim = " + + std::to_string(md.tdim)); + } +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// make_cell_level_set +// --------------------------------------------------------------------------- + +template +LevelSetCell +make_cell_level_set(const LevelSetFunction& global_ls, + I cell_id) +{ + LevelSetCell cell_ls; + cell_ls.global_level_set = &global_ls; + cell_ls.cell_id = cell_id; + + // --- Polynomial path: extract nodal values, convert to Bernstein --- + if (global_ls.type == LevelSetType::Polynomial + && global_ls.has_mesh_data() + && global_ls.has_dof_values()) + { + const auto& md = *global_ls.mesh_data; + const int degree = md.degree; + const int gdim = md.gdim; + + // Determine cell type + cell::type ctype = infer_ls_cell_type(md, cell_id); + cell_ls.cell_type = ctype; + cell_ls.tdim = cell::get_tdim(ctype); + + // Get the cell's DOF indices + auto cell_dofs = md.cell_dofs_span(cell_id); + const int ndofs = static_cast(cell_dofs.size()); + + // Extract nodal values from the global DOF array + cell_ls.nodal_values.resize(static_cast(ndofs)); + for (int i = 0; i < ndofs; ++i) + cell_ls.nodal_values[static_cast(i)] + = global_ls.dof_values[static_cast(cell_dofs[static_cast(i)])]; + cell_ls.nodal_order = degree; + + // Extract physical coordinates of all DOFs on this cell + std::vector dof_phys(static_cast(ndofs * gdim)); + for (int i = 0; i < ndofs; ++i) + { + const T* x = md.dof_coordinate(cell_dofs[static_cast(i)]); + for (int d = 0; d < gdim; ++d) + dof_phys[static_cast(i * gdim + d)] = x[d]; + } + + // Extract physical vertex coordinates (first n_vertices DOFs in Basix ordering) + const int nv = cell::get_num_vertices(ctype); + std::vector vertex_coords(static_cast(nv * gdim)); + for (int v = 0; v < nv; ++v) + for (int d = 0; d < gdim; ++d) + vertex_coords[static_cast(v * gdim + d)] + = dof_phys[static_cast(v * gdim + d)]; + + // Pull back all DOF coordinates from physical to reference space. + // Note: pull_back_affine is supported for volume cells (gdim == tdim). + assert(gdim == md.tdim && "pull_back_affine requires gdim == tdim"); + std::vector dof_ref(static_cast(ndofs * gdim)); + cell::pull_back_affine(ctype, vertex_coords, gdim, + std::span(dof_phys), + std::span(dof_ref)); + + // Convert Lagrange nodal values to Bernstein coefficients + cell_ls.bernstein_order = degree; + bernstein::lagrange_to_bernstein( + ctype, degree, + std::span(dof_ref), + std::span(cell_ls.nodal_values), + cell_ls.bernstein_coeffs); + } + + return cell_ls; +} + +// --------------------------------------------------------------------------- +// LevelSetCell::value +// --------------------------------------------------------------------------- + +template +T LevelSetCell::value(std::span xi) const +{ + // Polynomial backend: evaluate the Bernstein expansion + if (!bernstein_coeffs.empty()) + { + return bernstein::evaluate( + cell_type, bernstein_order, + std::span(bernstein_coeffs), xi); + } + + // Fallback: delegate to the global level set value function (in physical + // coordinates). This path requires an external mapping from reference to + // physical space, which is not yet wired up here. + throw std::runtime_error( + "LevelSetCell::value: no Bernstein coefficients available " + "and analytical fallback not yet implemented"); +} + +// --------------------------------------------------------------------------- +// LevelSetCell::grad +// --------------------------------------------------------------------------- + +template +void LevelSetCell::grad(std::span xi, std::span g) const +{ + // Polynomial backend: gradient of the Bernstein expansion + if (!bernstein_coeffs.empty()) + { + bernstein::gradient( + cell_type, bernstein_order, + std::span(bernstein_coeffs), xi, g); + return; + } + + throw std::runtime_error( + "LevelSetCell::grad: no Bernstein coefficients available " + "and analytical fallback not yet implemented"); +} + +// --------------------------------------------------------------------------- +// Explicit template instantiations +// --------------------------------------------------------------------------- + +template LevelSetCell +make_cell_level_set(const LevelSetFunction&, int); + +template LevelSetCell +make_cell_level_set(const LevelSetFunction&, int); + +template LevelSetCell +make_cell_level_set(const LevelSetFunction&, long); + +template LevelSetCell +make_cell_level_set(const LevelSetFunction&, long); + +template double LevelSetCell::value(std::span) const; +template float LevelSetCell::value(std::span) const; +template double LevelSetCell::value(std::span) const; +template float LevelSetCell::value(std::span) const; + +template void LevelSetCell::grad(std::span, std::span) const; +template void LevelSetCell::grad(std::span, std::span) const; +template void LevelSetCell::grad(std::span, std::span) const; +template void LevelSetCell::grad(std::span, std::span) const; + +} // namespace cutcells diff --git a/cpp/src/level_set_cell.h b/cpp/src/level_set_cell.h new file mode 100644 index 0000000..8731c6b --- /dev/null +++ b/cpp/src/level_set_cell.h @@ -0,0 +1,65 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "adapt_cell.h" +#include "cell_types.h" +#include "level_set.h" +#include "mesh_view.h" + +namespace cutcells +{ + +template +struct LevelSetCell +{ + const LevelSetFunction* global_level_set = nullptr; ///< non-owning view of the global level set function + + int level_set_id = 0 ; ///< identifier for the level set function on this cell (e.g., for multi-level-set scenarios) + + // --- Cell geometry --- + cell::type cell_type = cell::type::point; ///< cell type of the background cell + int tdim = 0; ///< topological dimension + I cell_id = static_cast(-1); ///< background cell index + + // --- Evaluation interface (uniform for both backends) --- + // Evaluate the level set at a point xi in reference coordinates. + T value(std::span xi) const; + + // Evaluate the gradient at xi in reference coordinates. + void grad(std::span xi, std::span g) const; + + // --- Polynomial backend storage --- + // Bernstein coefficients on this cell. + // For a polynomial level set, the global nodal values are extracted for + // this cell, then converted from the nodal (Lagrange) basis to the + // Bernstein basis on the reference cell. + std::vector bernstein_coeffs; + int bernstein_order = 0; + + // The original nodal values on this cell (before conversion), kept for + // reference and debugging. + std::vector nodal_values; + int nodal_order = 0; +}; + +/// Create a LevelSetCell and AdaptCell for a single background cell. +/// +/// @param global_ls The global level set function. +/// @param mesh The background mesh. +/// @param cell_id Index of the background cell to process. +/// +/// @return A pair of (LevelSetCell, AdaptCell) initialized for the given cell. +template +LevelSetCell +make_cell_level_set(const LevelSetFunction& global_ls, + I cell_id); + +} // namespace cutcells \ No newline at end of file diff --git a/cpp/src/mesh_view.h b/cpp/src/mesh_view.h index 51b9229..898f14c 100644 --- a/cpp/src/mesh_view.h +++ b/cpp/src/mesh_view.h @@ -12,6 +12,8 @@ #include #include +#include "cell_types.h" + namespace cutcells { @@ -28,8 +30,13 @@ struct MeshView std::span connectivity; std::span offsets; - // Cell types, one per cell - std::span cell_types; + // Cell types, one per cell in cutcells cell::type enum numbering (not VTK codes). + std::span cell_types; + + // True when the vertex connectivity of this mesh uses VTK vertex ordering. + // Set by the Python boundary when constructing from VTK data. + // Used internally to apply the VTK→Basix vertex permutation. + bool vtk_vertex_order = false; // Optional keep-alive anchor for Python / external memory std::shared_ptr owner; @@ -77,7 +84,7 @@ struct MeshView return connectivity[pos]; } - I cell_type(I cell_id) const + cell::type cell_type(I cell_id) const { if (cell_types.empty()) throw std::runtime_error("MeshView::cell_type not available"); diff --git a/cpp/src/reference_cell.h b/cpp/src/reference_cell.h new file mode 100644 index 0000000..13e32e6 --- /dev/null +++ b/cpp/src/reference_cell.h @@ -0,0 +1,270 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "cell_types.h" + +#include +#include +#include +#include +#include + +namespace cutcells::cell +{ + +template +inline std::vector reference_vertices(type cell_type) +{ + switch (cell_type) + { + case type::interval: + return {T(0), T(1)}; + case type::triangle: + return {T(0), T(0), T(1), T(0), T(0), T(1)}; + case type::quadrilateral: + return {T(0), T(0), T(1), T(0), T(0), T(1), T(1), T(1)}; + case type::tetrahedron: + return {T(0), T(0), T(0), + T(1), T(0), T(0), + T(0), T(1), T(0), + T(0), T(0), T(1)}; + case type::hexahedron: + return {T(0), T(0), T(0), + T(1), T(0), T(0), + T(0), T(1), T(0), + T(1), T(1), T(0), + T(0), T(0), T(1), + T(1), T(0), T(1), + T(0), T(1), T(1), + T(1), T(1), T(1)}; + case type::prism: + return {T(0), T(0), T(0), + T(1), T(0), T(0), + T(0), T(1), T(0), + T(0), T(0), T(1), + T(1), T(0), T(1), + T(0), T(1), T(1)}; + case type::pyramid: + return {T(0), T(0), T(0), + T(1), T(0), T(0), + T(0), T(1), T(0), + T(1), T(1), T(0), + T(0), T(0), T(1)}; + default: + throw std::invalid_argument("reference_vertices: unsupported cell type"); + } +} + +inline std::span vtk_to_basix_vertex_permutation(type cell_type) +{ + static constexpr std::array interval = {0, 1}; + static constexpr std::array triangle = {0, 1, 2}; + static constexpr std::array quadrilateral = {0, 1, 3, 2}; + static constexpr std::array tetrahedron = {0, 1, 2, 3}; + static constexpr std::array hexahedron = {0, 1, 3, 2, 4, 5, 7, 6}; + static constexpr std::array prism = {0, 1, 2, 3, 4, 5}; + static constexpr std::array pyramid = {0, 1, 3, 2, 4}; + + switch (cell_type) + { + case type::interval: return std::span(interval); + case type::triangle: return std::span(triangle); + case type::quadrilateral: return std::span(quadrilateral); + case type::tetrahedron: return std::span(tetrahedron); + case type::hexahedron: return std::span(hexahedron); + case type::prism: return std::span(prism); + case type::pyramid: return std::span(pyramid); + default: + throw std::invalid_argument("vtk_to_basix_vertex_permutation: unsupported cell type"); + } +} + +inline std::span basix_to_vtk_vertex_permutation(type cell_type) +{ + // For the supported first-order cells the inverse happens to equal the forward map. + return vtk_to_basix_vertex_permutation(cell_type); +} + +inline int vtk_to_basix_vertex(type cell_type, int vtk_vertex) +{ + const auto perm = vtk_to_basix_vertex_permutation(cell_type); + if (vtk_vertex < 0 || vtk_vertex >= static_cast(perm.size())) + throw std::invalid_argument("vtk_to_basix_vertex: vertex id out of bounds"); + + for (std::size_t i = 0; i < perm.size(); ++i) + { + if (perm[i] == vtk_vertex) + return static_cast(i); + } + + throw std::invalid_argument("vtk_to_basix_vertex: invalid vtk vertex id"); +} + +inline int basix_to_vtk_vertex(type cell_type, int basix_vertex) +{ + const auto perm = basix_to_vtk_vertex_permutation(cell_type); + if (basix_vertex < 0 || basix_vertex >= static_cast(perm.size())) + throw std::invalid_argument("basix_to_vtk_vertex: vertex id out of bounds"); + return perm[static_cast(basix_vertex)]; +} + +inline std::span vtk_to_basix_edge_permutation(type cell_type) +{ + static constexpr std::array interval = {0}; + static constexpr std::array triangle = {2, 0, 1}; + static constexpr std::array quadrilateral = {0, 2, 3, 1}; + static constexpr std::array tetrahedron = {5, 2, 4, 3, 1, 0}; + static constexpr std::array hexahedron = {0, 3, 1, 2, 4, 7, 5, 6, 8, 9, 11, 10}; + static constexpr std::array prism = {0, 2, 1, 6, 8, 7, 3, 4, 5}; + static constexpr std::array pyramid = {0, 2, 3, 1, 4, 5, 6, 7}; + + switch (cell_type) + { + case type::interval: return std::span(interval); + case type::triangle: return std::span(triangle); + case type::quadrilateral: return std::span(quadrilateral); + case type::tetrahedron: return std::span(tetrahedron); + case type::hexahedron: return std::span(hexahedron); + case type::prism: return std::span(prism); + case type::pyramid: return std::span(pyramid); + default: + throw std::invalid_argument("vtk_to_basix_edge_permutation: unsupported cell type"); + } +} + +inline int vtk_to_basix_edge(type cell_type, int vtk_edge) +{ + const auto perm = vtk_to_basix_edge_permutation(cell_type); + if (vtk_edge < 0 || vtk_edge >= static_cast(perm.size())) + throw std::invalid_argument("vtk_to_basix_edge: edge id out of bounds"); + return perm[static_cast(vtk_edge)]; +} + +inline int basix_to_vtk_edge(type cell_type, int basix_edge) +{ + const auto perm = vtk_to_basix_edge_permutation(cell_type); + if (basix_edge < 0 || basix_edge >= static_cast(perm.size())) + throw std::invalid_argument("basix_to_vtk_edge: edge id out of bounds"); + + for (std::size_t i = 0; i < perm.size(); ++i) + { + if (perm[i] == basix_edge) + return static_cast(i); + } + + throw std::invalid_argument("basix_to_vtk_edge: invalid basix edge id"); +} + +inline int vtk_parent_entity_token_to_basix(type cell_type, int token) +{ + if (token < 0) + return token; + if (token < 100) + return vtk_to_basix_edge(cell_type, token); + if (token < 200) + return 100 + vtk_to_basix_vertex(cell_type, token - 100); + return token; +} + +template +inline std::vector permute_vertex_data(std::span data, int ncomp, + std::span permutation) +{ + if (ncomp < 0) + throw std::invalid_argument("permute_vertex_data: negative component count"); + if (data.size() != static_cast(ncomp) * permutation.size()) + throw std::invalid_argument("permute_vertex_data: data size does not match permutation"); + + std::vector out(data.size()); + for (std::size_t i = 0; i < permutation.size(); ++i) + { + const int src = permutation[i]; + if (src < 0 || src >= static_cast(permutation.size())) + throw std::invalid_argument("permute_vertex_data: permutation entry out of bounds"); + + for (int c = 0; c < ncomp; ++c) + { + out[i * static_cast(ncomp) + static_cast(c)] + = data[static_cast(src) * static_cast(ncomp) + + static_cast(c)]; + } + } + return out; +} + +template +inline std::vector permute_vertex_ids(std::span ids, + std::span permutation) +{ + if (ids.size() != permutation.size()) + throw std::invalid_argument("permute_vertex_ids: size does not match permutation"); + + std::vector out(ids.size()); + for (std::size_t i = 0; i < permutation.size(); ++i) + { + const int src = permutation[i]; + if (src < 0 || src >= static_cast(permutation.size())) + throw std::invalid_argument("permute_vertex_ids: permutation entry out of bounds"); + out[i] = ids[static_cast(src)]; + } + return out; +} + +template +inline void reorder_subcell_vertices_from_vtk_to_basix(type cell_type, + std::array& vertices, + int nvertices) +{ + const auto perm = vtk_to_basix_vertex_permutation(cell_type); + if (nvertices != static_cast(perm.size())) + return; + + const auto reordered = permute_vertex_ids( + std::span(vertices.data(), static_cast(nvertices)), perm); + for (int i = 0; i < nvertices; ++i) + vertices[static_cast(i)] = reordered[static_cast(i)]; +} + +template +inline std::array remap_token_to_vertex_map_from_vtk_to_basix( + type cell_type, const std::array& vtk_token_to_vertex, + int n_edges, int n_vertices, int n_special = 0) +{ + std::array basix_token_to_vertex; + basix_token_to_vertex.fill(-1); + + for (int token = 0; token < n_edges; ++token) + { + if (vtk_token_to_vertex[static_cast(token)] >= 0) + { + basix_token_to_vertex[static_cast( + vtk_parent_entity_token_to_basix(cell_type, token))] + = vtk_token_to_vertex[static_cast(token)]; + } + } + + for (int token = 100; token < 100 + n_vertices; ++token) + { + if (vtk_token_to_vertex[static_cast(token)] >= 0) + { + basix_token_to_vertex[static_cast( + vtk_parent_entity_token_to_basix(cell_type, token))] + = vtk_token_to_vertex[static_cast(token)]; + } + } + + for (int token = 200; token < 200 + n_special; ++token) + { + if (vtk_token_to_vertex[static_cast(token)] >= 0) + basix_token_to_vertex[static_cast(token)] + = vtk_token_to_vertex[static_cast(token)]; + } + + return basix_token_to_vertex; +} + +} // namespace cutcells::cell diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp new file mode 100644 index 0000000..d28a03e --- /dev/null +++ b/cpp/src/refine_cell.cpp @@ -0,0 +1,773 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "refine_cell.h" +#include "cell_subdivision.h" +#include "cell_topology.h" + +#include +#include +#include +#include +#include + +namespace cutcells +{ +namespace +{ + +template +struct EdgeStateData +{ + std::array verts = {0, 0}; + std::vector tags; + std::vector split_params; + std::vector split_has_value; + std::vector root_params; + std::vector root_vertex_ids; + std::vector root_has_value; +}; + +template +struct CapturedEdgeState +{ + int num_level_sets = 0; + std::vector, EdgeStateData>> edges_in_old_order; + std::map, const EdgeStateData*> by_key; +}; + +template +CapturedEdgeState capture_edge_state(const AdaptCell& adapt_cell) +{ + CapturedEdgeState state; + const int n_edges = adapt_cell.n_entities(1); + state.num_level_sets = adapt_cell.edge_root_tag_num_level_sets; + state.edges_in_old_order.reserve(static_cast(n_edges)); + + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + const std::pair key = { + std::min(static_cast(ev[0]), static_cast(ev[1])), + std::max(static_cast(ev[0]), static_cast(ev[1]))}; + + EdgeStateData data; + data.verts = {ev[0], ev[1]}; + data.tags.resize(static_cast(state.num_level_sets), + EdgeRootTag::not_classified); + data.split_params.resize(static_cast(state.num_level_sets), T(0)); + data.split_has_value.resize(static_cast(state.num_level_sets), + std::uint8_t(0)); + data.root_params.resize(static_cast(state.num_level_sets), T(0)); + data.root_vertex_ids.resize(static_cast(state.num_level_sets), + std::int32_t(-1)); + data.root_has_value.resize(static_cast(state.num_level_sets), + std::uint8_t(0)); + + for (int ls = 0; ls < state.num_level_sets; ++ls) + { + data.tags[static_cast(ls)] = + adapt_cell.get_edge_root_tag(ls, e); + + const auto idx = static_cast(ls * n_edges + e); + if (idx < adapt_cell.edge_green_split_param.size()) + data.split_params[static_cast(ls)] = + adapt_cell.edge_green_split_param[idx]; + if (idx < adapt_cell.edge_green_split_has_value.size()) + data.split_has_value[static_cast(ls)] = + adapt_cell.edge_green_split_has_value[idx]; + if (idx < adapt_cell.edge_one_root_param.size()) + data.root_params[static_cast(ls)] = + adapt_cell.edge_one_root_param[idx]; + if (idx < adapt_cell.edge_one_root_vertex_id.size()) + data.root_vertex_ids[static_cast(ls)] = + adapt_cell.edge_one_root_vertex_id[idx]; + if (idx < adapt_cell.edge_one_root_has_value.size()) + data.root_has_value[static_cast(ls)] = + adapt_cell.edge_one_root_has_value[idx]; + } + + state.edges_in_old_order.emplace_back(key, std::move(data)); + } + + for (auto& [key, data] : state.edges_in_old_order) + state.by_key[key] = &data; + + return state; +} + +template +void clear_topology_caches(AdaptCell& adapt_cell) +{ + for (auto& row : adapt_cell.connectivity) + { + for (auto& conn : row) + { + conn.offsets.clear(); + conn.indices.clear(); + } + } + adapt_cell.has_connectivity = {}; + + adapt_cell.zero_entity_dim.clear(); + adapt_cell.zero_entity_id.clear(); + adapt_cell.zero_entity_zero_mask.clear(); + adapt_cell.zero_entity_is_owned.clear(); + adapt_cell.zero_entity_parent_dim.clear(); + adapt_cell.zero_entity_parent_id.clear(); + ++adapt_cell.zero_entity_version; +} + +template +int append_vertex(AdaptCell& adapt_cell, std::span coords) +{ + const int new_v = adapt_cell.n_vertices(); + adapt_cell.vertex_coords.insert(adapt_cell.vertex_coords.end(), + coords.begin(), coords.end()); + adapt_cell.vertex_parent_dim.push_back(static_cast(adapt_cell.tdim)); + adapt_cell.vertex_parent_id.push_back(-1); + const std::int32_t param_offset = adapt_cell.vertex_parent_param_offset.empty() + ? 0 + : adapt_cell.vertex_parent_param_offset.back(); + adapt_cell.vertex_parent_param_offset.push_back(param_offset); + adapt_cell.zero_mask_per_vertex.push_back(0); + adapt_cell.negative_mask_per_vertex.push_back(0); + adapt_cell.vertex_source_edge_id.push_back(-1); + return new_v; +} + +template +int append_interpolated_vertex(AdaptCell& adapt_cell, int v0, int v1, T t) +{ + std::vector x(static_cast(adapt_cell.tdim), T(0)); + for (int d = 0; d < adapt_cell.tdim; ++d) + { + const T x0 = adapt_cell.vertex_coords[static_cast(v0 * adapt_cell.tdim + d)]; + const T x1 = adapt_cell.vertex_coords[static_cast(v1 * adapt_cell.tdim + d)]; + x[static_cast(d)] = (T(1) - t) * x0 + t * x1; + } + return append_vertex(adapt_cell, std::span(x)); +} + +template +int append_cell_center_vertex(AdaptCell& adapt_cell, std::span verts) +{ + std::vector x(static_cast(adapt_cell.tdim), T(0)); + const T inv = T(1) / T(static_cast(verts.size())); + for (auto v : verts) + { + for (int d = 0; d < adapt_cell.tdim; ++d) + x[static_cast(d)] += + adapt_cell.vertex_coords[static_cast(v * adapt_cell.tdim + d)] * inv; + } + return append_vertex(adapt_cell, std::span(x)); +} + +template +std::map, int> build_edge_lookup(const AdaptCell& adapt_cell) +{ + std::map, int> edge_lookup; + const int n_edges = adapt_cell.n_entities(1); + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + const int a = std::min(static_cast(ev[0]), static_cast(ev[1])); + const int b = std::max(static_cast(ev[0]), static_cast(ev[1])); + edge_lookup[{a, b}] = e; + } + return edge_lookup; +} + +template +void rebuild_leaf_edges_preserve_certification( + AdaptCell& adapt_cell, + const CapturedEdgeState& old_edge_state) +{ + const int tdim = adapt_cell.tdim; + std::map, std::array> new_leaf_edges; + std::vector> new_edge_keys_in_encounter_order; + + const int n_cells = adapt_cell.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + const cell::type ctype = adapt_cell.entity_types[tdim][static_cast(c)]; + auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(c)]; + auto cell_edges = cell::edges(ctype); + + for (const auto& ce : cell_edges) + { + const std::int32_t lv0 = cell_verts[static_cast(ce[0])]; + const std::int32_t lv1 = cell_verts[static_cast(ce[1])]; + const std::pair key = { + std::min(static_cast(lv0), static_cast(lv1)), + std::max(static_cast(lv0), static_cast(lv1))}; + + if (!new_leaf_edges.contains(key)) + { + new_leaf_edges[key] = {lv0, lv1}; + new_edge_keys_in_encounter_order.push_back(key); + } + } + } + + std::vector> final_edges; + std::vector*> final_old_state; + final_edges.reserve(new_leaf_edges.size()); + final_old_state.reserve(new_leaf_edges.size()); + + for (const auto& [key, old_data] : old_edge_state.edges_in_old_order) + { + auto it = new_leaf_edges.find(key); + if (it == new_leaf_edges.end()) + continue; + + final_edges.push_back(old_data.verts); + final_old_state.push_back(&old_data); + new_leaf_edges.erase(it); + } + + for (const auto& key : new_edge_keys_in_encounter_order) + { + auto it = new_leaf_edges.find(key); + if (it == new_leaf_edges.end()) + continue; + + final_edges.push_back(it->second); + final_old_state.push_back(nullptr); + } + + adapt_cell.entity_types[1].assign(final_edges.size(), cell::type::interval); + adapt_cell.entity_to_vertex[1].indices.clear(); + adapt_cell.entity_to_vertex[1].offsets.clear(); + adapt_cell.entity_to_vertex[1].offsets.push_back(0); + for (const auto& edge : final_edges) + { + adapt_cell.entity_to_vertex[1].indices.push_back(edge[0]); + adapt_cell.entity_to_vertex[1].indices.push_back(edge[1]); + adapt_cell.entity_to_vertex[1].offsets.push_back( + static_cast(adapt_cell.entity_to_vertex[1].indices.size())); + } + + const int n_edges = static_cast(final_edges.size()); + const int nls = old_edge_state.num_level_sets; + adapt_cell.edge_root_tag_num_level_sets = nls; + adapt_cell.edge_root_tag.assign(static_cast(nls * n_edges), + EdgeRootTag::not_classified); + adapt_cell.edge_green_split_param.assign(static_cast(nls * n_edges), T(0)); + adapt_cell.edge_green_split_has_value.assign(static_cast(nls * n_edges), + std::uint8_t(0)); + adapt_cell.edge_one_root_param.assign(static_cast(nls * n_edges), T(0)); + adapt_cell.edge_one_root_vertex_id.assign(static_cast(nls * n_edges), + std::int32_t(-1)); + adapt_cell.edge_one_root_has_value.assign(static_cast(nls * n_edges), + std::uint8_t(0)); + + for (int e = 0; e < n_edges; ++e) + { + const auto* old_data = final_old_state[static_cast(e)]; + if (!old_data) + continue; + + for (int ls = 0; ls < nls; ++ls) + { + const auto idx = static_cast(ls * n_edges + e); + adapt_cell.edge_root_tag[idx] = old_data->tags[static_cast(ls)]; + adapt_cell.edge_green_split_param[idx] = + old_data->split_params[static_cast(ls)]; + adapt_cell.edge_green_split_has_value[idx] = + old_data->split_has_value[static_cast(ls)]; + adapt_cell.edge_one_root_param[idx] = + old_data->root_params[static_cast(ls)]; + adapt_cell.edge_one_root_vertex_id[idx] = + old_data->root_vertex_ids[static_cast(ls)]; + adapt_cell.edge_one_root_has_value[idx] = + old_data->root_has_value[static_cast(ls)]; + } + } +} + +template +void rebuild_leaf_cell_certification( + AdaptCell& adapt_cell, + std::span old_cell_tags, + int old_num_level_sets, + int old_num_cells, + std::span old_cell_ids_for_new_cells) +{ + const int new_num_cells = adapt_cell.n_entities(adapt_cell.tdim); + adapt_cell.cell_cert_tag_num_level_sets = old_num_level_sets; + adapt_cell.cell_cert_tag.assign( + static_cast(old_num_level_sets * new_num_cells), + CellCertTag::not_classified); + + for (int c = 0; c < new_num_cells; ++c) + { + const int old_c = old_cell_ids_for_new_cells[static_cast(c)]; + if (old_c < 0 || old_c >= old_num_cells) + continue; + + for (int ls = 0; ls < old_num_level_sets; ++ls) + { + adapt_cell.cell_cert_tag[static_cast(ls * new_num_cells + c)] = + old_cell_tags[static_cast(ls * old_num_cells + old_c)]; + } + } +} + +void append_top_cell(std::vector& types, + EntityAdjacency& adj, + cell::type ctype, + std::span verts) +{ + types.push_back(ctype); + for (int v : verts) + adj.indices.push_back(static_cast(v)); + adj.offsets.push_back(static_cast(adj.indices.size())); +} + +} // anonymous namespace + +template +void apply_topology_update_preserve_certification( + AdaptCell& adapt_cell, + std::vector&& new_types, + EntityAdjacency&& new_cells, + std::span old_cell_ids_for_new_cells) +{ + const int tdim = adapt_cell.tdim; + const CapturedEdgeState old_edge_state = capture_edge_state(adapt_cell); + const int old_num_level_sets = adapt_cell.cell_cert_tag_num_level_sets; + const int old_num_cells = adapt_cell.n_entities(tdim); + const std::vector old_cell_tags = adapt_cell.cell_cert_tag; + + adapt_cell.entity_types[tdim] = std::move(new_types); + adapt_cell.entity_to_vertex[tdim] = std::move(new_cells); + + rebuild_leaf_edges_preserve_certification(adapt_cell, old_edge_state); + rebuild_leaf_cell_certification(adapt_cell, + std::span(old_cell_tags), + old_num_level_sets, + old_num_cells, + old_cell_ids_for_new_cells); + clear_topology_caches(adapt_cell); +} + + +// ===================================================================== +// Invalidation helpers +// ===================================================================== + +template +void invalidate_edge_tags_for_new_edges(AdaptCell& adapt_cell, + std::span new_edge_ids) +{ + const int nls = adapt_cell.edge_root_tag_num_level_sets; + const int n_edges = adapt_cell.n_entities(1); + for (int ls = 0; ls < nls; ++ls) + { + for (int e : new_edge_ids) + { + if (e >= 0 && e < n_edges) + adapt_cell.set_edge_root_tag(ls, e, EdgeRootTag::not_classified); + } + } +} + +template +void invalidate_cell_tags_for_new_cells(AdaptCell& adapt_cell, + std::span new_cell_ids) +{ + const int nls = adapt_cell.cell_cert_tag_num_level_sets; + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + for (int ls = 0; ls < nls; ++ls) + { + for (int c : new_cell_ids) + { + if (c >= 0 && c < n_cells) + adapt_cell.set_cell_cert_tag(ls, c, CellCertTag::not_classified); + } + } +} + +// ===================================================================== +// Green refinement +// ===================================================================== + +template +bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, + int level_set_id) +{ + if (adapt_cell.tdim != 2 && adapt_cell.tdim != 3) + return false; + + const int n_edges = adapt_cell.n_entities(1); + int split_edge = -1; + T split_t = T(0); + for (int e = 0; e < n_edges; ++e) + { + if (adapt_cell.get_edge_root_tag(level_set_id, e) != EdgeRootTag::multiple_roots) + continue; + const auto idx = static_cast(level_set_id * n_edges + e); + if (!adapt_cell.edge_green_split_has_value[idx]) + continue; + split_edge = e; + split_t = adapt_cell.edge_green_split_param[idx]; + break; + } + + if (split_edge < 0) + return false; + + auto ev = adapt_cell.entity_to_vertex[1][static_cast(split_edge)]; + const int v0 = ev[0]; + const int v1 = ev[1]; + const int new_v = append_interpolated_vertex(adapt_cell, v0, v1, split_t); + + const int tdim = adapt_cell.tdim; + const std::vector old_types = adapt_cell.entity_types[tdim]; + const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; + + if (tdim == 2 + && std::any_of(old_types.begin(), old_types.end(), + [](cell::type ct) { return ct != cell::type::triangle; })) + { + return false; + } + if (tdim == 3 + && std::any_of(old_types.begin(), old_types.end(), + [](cell::type ct) { return ct != cell::type::tetrahedron; })) + { + return false; + } + + EntityAdjacency new_cells; + new_cells.offsets.push_back(0); + std::vector new_types; + std::vector old_cell_ids_for_new_cells; + + for (int c = 0; c < static_cast(old_types.size()); ++c) + { + auto verts = old_cells[static_cast(c)]; + + if (tdim == 2) + { + const int a = verts[0]; + const int b = verts[1]; + const int cvert = verts[2]; + bool split = false; + + if ((a == v0 && b == v1) || (a == v1 && b == v0)) + { + const std::array t0 = {a, new_v, cvert}; + const std::array t1 = {new_v, b, cvert}; + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); + old_cell_ids_for_new_cells.push_back(-1); + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); + old_cell_ids_for_new_cells.push_back(-1); + split = true; + } + else if ((b == v0 && cvert == v1) || (b == v1 && cvert == v0)) + { + const std::array t0 = {b, new_v, a}; + const std::array t1 = {new_v, cvert, a}; + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); + old_cell_ids_for_new_cells.push_back(-1); + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); + old_cell_ids_for_new_cells.push_back(-1); + split = true; + } + else if ((cvert == v0 && a == v1) || (cvert == v1 && a == v0)) + { + const std::array t0 = {cvert, new_v, b}; + const std::array t1 = {new_v, a, b}; + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); + old_cell_ids_for_new_cells.push_back(-1); + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); + old_cell_ids_for_new_cells.push_back(-1); + split = true; + } + + if (!split) + { + std::vector copy(verts.begin(), verts.end()); + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + } + } + else + { + const int a = verts[0]; + const int b = verts[1]; + const int cvert = verts[2]; + const int dvert = verts[3]; + bool split = false; + + const std::array, 6> edge_positions = {{ + {{0, 1, 2, 3}}, + {{0, 2, 1, 3}}, + {{0, 3, 1, 2}}, + {{1, 2, 0, 3}}, + {{1, 3, 0, 2}}, + {{2, 3, 0, 1}}, + }}; + + const std::array tet = {a, b, cvert, dvert}; + for (const auto& pos : edge_positions) + { + const int e0 = tet[static_cast(pos[0])]; + const int e1 = tet[static_cast(pos[1])]; + if (!((e0 == v0 && e1 == v1) || (e0 == v1 && e1 == v0))) + continue; + + // Preserve orientation: replace one endpoint at a time + // in the parent's vertex ordering. + std::array t0 = tet; + t0[static_cast(pos[1])] = new_v; + std::array t1 = tet; + t1[static_cast(pos[0])] = new_v; + append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(t0)); + old_cell_ids_for_new_cells.push_back(-1); + append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(t1)); + old_cell_ids_for_new_cells.push_back(-1); + split = true; + break; + } + + if (!split) + { + std::vector copy(verts.begin(), verts.end()); + append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + } + } + } + + apply_topology_update_preserve_certification( + adapt_cell, std::move(new_types), std::move(new_cells), + std::span(old_cell_ids_for_new_cells)); + return true; +} + +// ===================================================================== +// Red refinement +// ===================================================================== + +template +bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, + int level_set_id) +{ + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + + std::vector ambiguous_cells; + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) + == CellCertTag::ambiguous) + ambiguous_cells.push_back(c); + } + + if (ambiguous_cells.empty()) + return false; + + const std::vector old_types = adapt_cell.entity_types[tdim]; + const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; + const auto edge_lookup = build_edge_lookup(adapt_cell); + + std::map midpoint_vertex_by_edge; + auto get_midpoint_vertex = [&](int edge_id) -> int + { + auto it = midpoint_vertex_by_edge.find(edge_id); + if (it != midpoint_vertex_by_edge.end()) + return it->second; + + auto ev = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + const int mid = append_interpolated_vertex(adapt_cell, ev[0], ev[1], T(0.5)); + midpoint_vertex_by_edge[edge_id] = mid; + return mid; + }; + + EntityAdjacency new_cells; + new_cells.offsets.push_back(0); + std::vector new_types; + std::vector old_cell_ids_for_new_cells; + + for (int c = 0; c < n_cells; ++c) + { + auto verts = old_cells[static_cast(c)]; + const cell::type ctype = old_types[static_cast(c)]; + const bool is_ambiguous = + (adapt_cell.get_cell_cert_tag(level_set_id, c) == CellCertTag::ambiguous); + + if (!is_ambiguous) + { + std::vector copy(verts.begin(), verts.end()); + append_top_cell(new_types, new_cells, ctype, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + continue; + } + + switch (ctype) + { + case cell::type::interval: + { + const int a = verts[0]; + const int b = verts[1]; + const int e = edge_lookup.at({std::min(a, b), std::max(a, b)}); + const int m = get_midpoint_vertex(e); + const std::array, 2> children = {{{a, m}, {m, b}}}; + for (const auto& child : children) + { + append_top_cell(new_types, new_cells, cell::type::interval, std::span(child)); + old_cell_ids_for_new_cells.push_back(-1); + } + break; + } + case cell::type::triangle: + { + std::array local_to_global = { + static_cast(verts[0]), static_cast(verts[1]), static_cast(verts[2]), + -1, -1, -1}; + const auto ledges = cell::edges(cell::type::triangle); + for (int le = 0; le < 3; ++le) + { + const int a = verts[static_cast(ledges[static_cast(le)][0])]; + const int b = verts[static_cast(ledges[static_cast(le)][1])]; + local_to_global[static_cast(3 + le)] = + get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + } + + for (const auto& child : cell::triangle_subdivision_table) + { + const std::array g = { + local_to_global[static_cast(child[0])], + local_to_global[static_cast(child[1])], + local_to_global[static_cast(child[2])]}; + append_top_cell(new_types, new_cells, cell::type::triangle, std::span(g)); + old_cell_ids_for_new_cells.push_back(-1); + } + break; + } + case cell::type::quadrilateral: + { + std::array local_to_global = { + static_cast(verts[0]), static_cast(verts[1]), + static_cast(verts[2]), static_cast(verts[3]), + -1, -1, -1, -1, -1}; + const auto ledges = cell::edges(cell::type::quadrilateral); + for (int le = 0; le < 4; ++le) + { + const int a = verts[static_cast(ledges[static_cast(le)][0])]; + const int b = verts[static_cast(ledges[static_cast(le)][1])]; + local_to_global[static_cast(4 + le)] = + get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + } + local_to_global[8] = append_cell_center_vertex(adapt_cell, verts); + + for (const auto& child : cell::quadrilateral_subdivision_table) + { + const std::array g = { + local_to_global[static_cast(child[0])], + local_to_global[static_cast(child[1])], + local_to_global[static_cast(child[2])], + local_to_global[static_cast(child[3])]}; + append_top_cell(new_types, new_cells, cell::type::quadrilateral, std::span(g)); + old_cell_ids_for_new_cells.push_back(-1); + } + break; + } + case cell::type::tetrahedron: + { + std::array local_to_global = { + static_cast(verts[0]), static_cast(verts[1]), + static_cast(verts[2]), static_cast(verts[3]), + -1, -1, -1, -1, -1, -1}; + const auto ledges = cell::edges(cell::type::tetrahedron); + for (int le = 0; le < 6; ++le) + { + const int a = verts[static_cast(ledges[static_cast(le)][0])]; + const int b = verts[static_cast(ledges[static_cast(le)][1])]; + local_to_global[static_cast(4 + le)] = + get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + } + + for (const auto& child : cell::tetrahedron_subdivision_table) + { + const std::array g = { + local_to_global[static_cast(child[0])], + local_to_global[static_cast(child[1])], + local_to_global[static_cast(child[2])], + local_to_global[static_cast(child[3])]}; + append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(g)); + old_cell_ids_for_new_cells.push_back(-1); + } + break; + } + default: + throw std::runtime_error( + "refine_red_on_ambiguous_cells: unsupported cell type"); + } + } + + apply_topology_update_preserve_certification( + adapt_cell, std::move(new_types), std::move(new_cells), + std::span(old_cell_ids_for_new_cells)); + return true; +} + +// ===================================================================== +// Explicit template instantiations +// ===================================================================== + +template void invalidate_edge_tags_for_new_edges(AdaptCell&, + std::span); +template void invalidate_edge_tags_for_new_edges(AdaptCell&, + std::span); + +template void invalidate_cell_tags_for_new_cells(AdaptCell&, + std::span); +template void invalidate_cell_tags_for_new_cells(AdaptCell&, + std::span); + +template +void invalidate_face_tags_for_new_faces(AdaptCell& adapt_cell, + std::span new_face_ids) +{ + const int nls = adapt_cell.face_cert_tag_num_level_sets; + const int n_faces = adapt_cell.n_entities(2); + for (int ls = 0; ls < nls; ++ls) + { + for (int f : new_face_ids) + { + if (f >= 0 && f < n_faces) + adapt_cell.set_face_cert_tag(ls, f, FaceCertTag::not_classified); + } + } +} + +template void invalidate_face_tags_for_new_faces(AdaptCell&, + std::span); +template void invalidate_face_tags_for_new_faces(AdaptCell&, + std::span); + +template bool refine_green_on_multiple_root_edges(AdaptCell&, int); +template bool refine_green_on_multiple_root_edges(AdaptCell&, int); + +template bool refine_red_on_ambiguous_cells(AdaptCell&, int); +template bool refine_red_on_ambiguous_cells(AdaptCell&, int); + +template void apply_topology_update_preserve_certification( + AdaptCell&, + std::vector&&, + EntityAdjacency&&, + std::span); +template void apply_topology_update_preserve_certification( + AdaptCell&, + std::vector&&, + EntityAdjacency&&, + std::span); + +} // namespace cutcells diff --git a/cpp/src/refine_cell.h b/cpp/src/refine_cell.h new file mode 100644 index 0000000..7bb0986 --- /dev/null +++ b/cpp/src/refine_cell.h @@ -0,0 +1,88 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +#include "adapt_cell.h" + +namespace cutcells +{ + +// ===================================================================== +// Green refinement — split multiple-roots edges +// ===================================================================== + +/// Perform green refinement on cells incident to an edge tagged multiple_roots. +/// +/// Each call refines the first multiple_roots edge with a stored green_split_t. +/// Repeated calls, or certify_and_refine(...), therefore resolve multiple such +/// edges recursively. +/// +/// For the selected multiple_roots edge: +/// 1. Insert a new vertex on the edge at parameter t. +/// 2. Split all cells incident to that edge by connecting the new vertex. +/// 3. Replace the old edge with two child edges. +/// 4. New edges and new cells get tag not_classified. +/// 5. Unaffected edges retain their tags. +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param level_set_id Which level set. +/// @return true if any refinement was performed. +template +bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, + int level_set_id); + +// ===================================================================== +// Red refinement — subdivide ambiguous cells +// ===================================================================== + +/// Perform red (full) refinement on cells tagged ambiguous. +/// +/// Each ambiguous cell is subdivided uniformly: edge midpoints become +/// new vertices and the cell is replaced by 2^tdim children (or the +/// appropriate simplex subdivision). +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param level_set_id Which level set. +/// @return true if any refinement was performed. +template +bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, + int level_set_id); + +/// Replace the current leaf-cell pool and rebuild the leaf-edge pool while +/// preserving certification state on surviving leaf entities. New leaf edges +/// and cells are left not_classified. +template +void apply_topology_update_preserve_certification( + AdaptCell& adapt_cell, + std::vector&& new_types, + EntityAdjacency&& new_cells, + std::span old_cell_ids_for_new_cells); + +// ===================================================================== +// Invalidation helpers +// ===================================================================== + +/// Reset edge tags to not_classified for a set of edge ids. +template +void invalidate_edge_tags_for_new_edges(AdaptCell& adapt_cell, + std::span new_edge_ids); + +/// Reset cell tags to not_classified for a set of cell ids. +template +void invalidate_cell_tags_for_new_cells(AdaptCell& adapt_cell, + std::span new_cell_ids); + +/// Reset face tags to not_classified for a set of face ids (3D cells). +template +void invalidate_face_tags_for_new_faces(AdaptCell& adapt_cell, + std::span new_face_ids); + +} // namespace cutcells diff --git a/cpp/src/selection_expr.cpp b/cpp/src/selection_expr.cpp new file mode 100644 index 0000000..286de3b --- /dev/null +++ b/cpp/src/selection_expr.cpp @@ -0,0 +1,196 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "selection_expr.h" + +#include +#include +#include + +namespace cutcells +{ +namespace +{ + +/// Trim leading and trailing whitespace from a string_view. +std::string_view trim(std::string_view s) +{ + while (!s.empty() && std::isspace(static_cast(s.front()))) + s.remove_prefix(1); + while (!s.empty() && std::isspace(static_cast(s.back()))) + s.remove_suffix(1); + return s; +} + +/// Parse a single clause: "name < 0", "name > 0", or "name = 0". +Clause parse_clause(std::string_view text) +{ + text = trim(text); + if (text.empty()) + throw std::runtime_error("parse_selection_expr: empty clause"); + + // Find the relation operator: '<', '>', or '=' + auto pos_lt = text.find('<'); + auto pos_gt = text.find('>'); + auto pos_eq = text.find('='); + + std::size_t op_pos = std::string_view::npos; + Relation rel{}; + + if (pos_lt != std::string_view::npos + && (pos_lt < pos_gt || pos_gt == std::string_view::npos) + && (pos_lt < pos_eq || pos_eq == std::string_view::npos)) + { + op_pos = pos_lt; + rel = Relation::LessThan; + } + else if (pos_gt != std::string_view::npos + && (pos_gt < pos_eq || pos_eq == std::string_view::npos)) + { + op_pos = pos_gt; + rel = Relation::GreaterThan; + } + else if (pos_eq != std::string_view::npos) + { + op_pos = pos_eq; + rel = Relation::EqualTo; + } + else + { + throw std::runtime_error( + "parse_selection_expr: no operator (<, >, =) in clause: '" + + std::string(text) + "'"); + } + + std::string_view name_part = trim(text.substr(0, op_pos)); + std::string_view rhs_part = trim(text.substr(op_pos + 1)); + + if (name_part.empty()) + throw std::runtime_error( + "parse_selection_expr: missing level-set name in clause: '" + + std::string(text) + "'"); + + if (rhs_part != "0") + throw std::runtime_error( + "parse_selection_expr: right-hand side must be '0', got '" + + std::string(rhs_part) + "' in clause: '" + std::string(text) + "'"); + + Clause c; + c.name = std::string(name_part); + c.relation = rel; + return c; +} + +/// Case-insensitive check for " and " delimiter between clauses. +/// Returns the position of the 'a' in " and ", or npos. +std::size_t find_and_delimiter(std::string_view text, std::size_t start = 0) +{ + // Search for " and " (space-delimited, case-insensitive) + for (std::size_t i = start; i + 4 < text.size(); ++i) + { + if (std::isspace(static_cast(text[i])) + && (text[i + 1] == 'a' || text[i + 1] == 'A') + && (text[i + 2] == 'n' || text[i + 2] == 'N') + && (text[i + 3] == 'd' || text[i + 3] == 'D') + && std::isspace(static_cast(text[i + 4]))) + { + return i; + } + } + return std::string_view::npos; +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// parse_selection_expr +// --------------------------------------------------------------------------- + +SelectionExpr parse_selection_expr(std::string_view text) +{ + text = trim(text); + if (text.empty()) + throw std::runtime_error("parse_selection_expr: empty expression"); + + SelectionExpr expr; + + std::size_t pos = 0; + while (pos < text.size()) + { + std::size_t and_pos = find_and_delimiter(text, pos); + std::string_view clause_text; + if (and_pos == std::string_view::npos) + { + clause_text = text.substr(pos); + pos = text.size(); + } + else + { + clause_text = text.substr(pos, and_pos - pos); + pos = and_pos + 5; // skip " and " + } + + expr.clauses.push_back(parse_clause(clause_text)); + } + + if (expr.clauses.empty()) + throw std::runtime_error("parse_selection_expr: no clauses found"); + + return expr; +} + +// --------------------------------------------------------------------------- +// compile_selection_expr +// --------------------------------------------------------------------------- + +void compile_selection_expr(SelectionExpr& expr, + const std::vector& level_set_names) +{ + expr.zero_required = 0; + expr.negative_required = 0; + expr.positive_required = 0; + + for (auto& clause : expr.clauses) + { + auto it = std::find(level_set_names.begin(), level_set_names.end(), + clause.name); + if (it == level_set_names.end()) + { + throw std::runtime_error( + "compile_selection_expr: unknown level-set name '" + + clause.name + "'"); + } + + int idx = static_cast(std::distance(level_set_names.begin(), it)); + if (idx >= 64) + throw std::runtime_error( + "compile_selection_expr: level-set index >= 64 not supported"); + + clause.level_set_index = idx; + const std::uint64_t bit = std::uint64_t(1) << idx; + + switch (clause.relation) + { + case Relation::LessThan: expr.negative_required |= bit; break; + case Relation::GreaterThan: expr.positive_required |= bit; break; + case Relation::EqualTo: expr.zero_required |= bit; break; + } + } +} + +// --------------------------------------------------------------------------- +// infer_selection_dim +// --------------------------------------------------------------------------- + +int infer_selection_dim(const SelectionExpr& expr, int tdim) +{ + int n_eq = 0; + for (const auto& clause : expr.clauses) + if (clause.relation == Relation::EqualTo) + ++n_eq; + return tdim - n_eq; +} + +} // namespace cutcells diff --git a/cpp/src/selection_expr.h b/cpp/src/selection_expr.h new file mode 100644 index 0000000..ab4a06b --- /dev/null +++ b/cpp/src/selection_expr.h @@ -0,0 +1,77 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include + +namespace cutcells +{ + +/// Relation in a selection clause: φ < 0, φ > 0, or φ = 0. +enum class Relation +{ + LessThan, + GreaterThan, + EqualTo +}; + +/// One clause of a selection expression, e.g. "phi1 < 0". +struct Clause +{ + std::string name; ///< level-set name (e.g. "phi1") + int level_set_index = -1; ///< resolved after compile() + Relation relation; ///< the comparison operator +}; + +/// A compiled selection expression. +/// +/// The expression is a conjunction (AND) of clauses: +/// "phi1 < 0 and phi2 = 0" → two clauses. +/// +/// After compilation against a name registry, bitmasks are populated: +/// - bit i set in zero_required → φ_i = 0 +/// - bit i set in negative_required → φ_i < 0 +/// - bit i set in positive_required → φ_i > 0 +struct SelectionExpr +{ + std::vector clauses; ///< implicitly AND-connected + + // Compiled bitmasks (set by compile_selection_expr()) + std::uint64_t zero_required = 0; + std::uint64_t negative_required = 0; + std::uint64_t positive_required = 0; +}; + +/// Parse a selection expression string. +/// +/// Grammar: clause ('and' clause)* +/// clause: name ('<'|'>'|'=') '0' +/// +/// @throws std::runtime_error on syntax error. +SelectionExpr parse_selection_expr(std::string_view text); + +/// Compile a parsed expression against a level-set name registry. +/// +/// Resolves each clause's name to its index in @p level_set_names +/// and populates the bitmask fields. +/// +/// @throws std::runtime_error if a name is not found. +void compile_selection_expr(SelectionExpr& expr, + const std::vector& level_set_names); + +/// Infer the entity dimension selected by a compiled expression. +/// +/// If no clause is EqualTo → volume (tdim). +/// Each EqualTo clause reduces the dimension by one. +/// +/// @param tdim topological dimension of the background cells. +/// @return inferred entity dimension. +int infer_selection_dim(const SelectionExpr& expr, int tdim); + +} // namespace cutcells diff --git a/cpp/src/triangulation.h b/cpp/src/triangulation.h index 5d5821b..5dab1cd 100644 --- a/cpp/src/triangulation.h +++ b/cpp/src/triangulation.h @@ -75,23 +75,6 @@ namespace cutcells::cell } }; - /// @brief Canonical reference vertices for a cell type (VTK ordering, flat). - /// - /// Returns a flat vector of nv * tdim values representing the reference-element - /// vertex coordinates for the given cell type. Coordinates are in the - /// topological-dimension space (i.e. tdim values per vertex, NOT padded to gdim). - /// - /// VTK canonical positions used (these make the affine Jacobian = I for the - /// unit reference cell): - /// interval v0=(0) v1=(1) - /// triangle v0=(0,0) v1=(1,0) v2=(0,1) - /// quadrilateral v0=(0,0) v1=(1,0) v2=(1,1) v3=(0,1) - /// tetrahedron v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(0,0,1) - /// hexahedron v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) - /// v4=(0,0,1) v5=(1,0,1) v6=(1,1,1) v7=(0,1,1) - /// prism v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) - /// v3=(0,0,1) v4=(1,0,1) v5=(0,1,1) - /// pyramid v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) v4=(0,0,1) template inline std::vector canonical_vertices(type cell_type) { diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index aaa88d7..9b94e96 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -366,21 +366,19 @@ HOCellFamily infer_cell_family(const cutcells::LevelSetMeshData& me { if (!mesh_data.cell_types.empty()) { - const int vtk_type = mesh_data.cell_types[static_cast(cell_id)]; - if (vtk_type == static_cast(cutcells::cell::vtk_types::VTK_TRIANGLE) - || vtk_type == VTK_LAGRANGE_TRIANGLE) + const cutcells::cell::type ct = mesh_data.cell_types[static_cast(cell_id)]; + if (ct == cutcells::cell::type::triangle) { return HOCellFamily::triangle; } - if (vtk_type == static_cast(cutcells::cell::vtk_types::VTK_TETRA) - || vtk_type == VTK_LAGRANGE_TETRAHEDRON) + if (ct == cutcells::cell::type::tetrahedron) { return HOCellFamily::tetrahedron; } throw std::runtime_error( - "write_level_set_vtu: unsupported VTK cell type " + std::to_string(vtk_type) - + " (supported: triangle, tetrahedron)"); + "write_level_set_vtu: unsupported cell type " + + cutcells::cell::cell_type_to_str(ct)); } if (mesh_data.tdim == 2) diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index e801ef7..908bfbc 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -3,43 +3,112 @@ # This file is part of CutCells # # SPDX-License-Identifier: MIT -from ._cutcellscpp import ( - classify_cell_domain, - CellType, - CutCell_float32, - CutCell_float64, - CutCells_float32, - CutCells_float64, - CutMesh_float32, - CutMesh_float64, - QuadratureRules_float32, - QuadratureRules_float64, - MeshView, - MeshView_float32, - MeshView_float64, - LevelSetMeshData, - LevelSetMeshData_float32, - LevelSetMeshData_float64, - LevelSetFunction, - LevelSetFunction_float32, - LevelSetFunction_float64, - create_level_set_mesh_data, - create_level_set_function, - interpolate_level_set, - cut, - higher_order_cut, - create_cut_mesh, - locate_cells, - cut_vtk_mesh, - cut_mesh_view, - csr_to_vtk_cells, - compute_physical_cut_vertices, - complete_from_physical, - make_quadrature, - runtime_quadrature, - physical_points, - write_level_set_vtu, + +from importlib import util as _importlib_util +from pathlib import Path as _Path +import ctypes as _ctypes +import sys as _sys + + +def _load_cpp_module(): + build_dir = _Path(__file__).resolve().parents[1] / "build" + candidates = sorted(build_dir.glob("_cutcellscpp*.so")) + if candidates: + lib_candidates = [ + build_dir / "cutcells_cpp" / "src" / "libcutcells.dylib", + _Path(__file__).resolve().parents[2] + / "cpp" + / "build" + / "src" + / "libcutcells.dylib", + ] + for lib in lib_candidates: + if lib.exists(): + _ctypes.CDLL(str(lib), mode=_ctypes.RTLD_GLOBAL) + break + + spec = _importlib_util.spec_from_file_location( + "cutcells._cutcellscpp", candidates[0] + ) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load extension from {candidates[0]}") + + module = _importlib_util.module_from_spec(spec) + _sys.modules["cutcells._cutcellscpp"] = module + spec.loader.exec_module(module) + return module + + from . import _cutcellscpp as module + + return module + + +_cutcellscpp = _load_cpp_module() + +classify_cell_domain = _cutcellscpp.classify_cell_domain +CellType = _cutcellscpp.CellType +CutCell_float32 = _cutcellscpp.CutCell_float32 +CutCell_float64 = _cutcellscpp.CutCell_float64 +CutCells_float32 = _cutcellscpp.CutCells_float32 +CutCells_float64 = _cutcellscpp.CutCells_float64 +CutMesh_float32 = _cutcellscpp.CutMesh_float32 +CutMesh_float64 = _cutcellscpp.CutMesh_float64 +QuadratureRules_float32 = _cutcellscpp.QuadratureRules_float32 +QuadratureRules_float64 = _cutcellscpp.QuadratureRules_float64 +EdgeRootTag = _cutcellscpp.EdgeRootTag +CellCertTag = _cutcellscpp.CellCertTag +FaceCertTag = _cutcellscpp.FaceCertTag +MeshView = _cutcellscpp.MeshView +MeshView_float32 = _cutcellscpp.MeshView_float32 +MeshView_float64 = _cutcellscpp.MeshView_float64 +AdaptCell = _cutcellscpp.AdaptCell +AdaptCell_float32 = _cutcellscpp.AdaptCell_float32 +AdaptCell_float64 = _cutcellscpp.AdaptCell_float64 +LevelSetCell = _cutcellscpp.LevelSetCell +LevelSetCell_float32 = _cutcellscpp.LevelSetCell_float32 +LevelSetCell_float64 = _cutcellscpp.LevelSetCell_float64 +LevelSetMeshData = _cutcellscpp.LevelSetMeshData +LevelSetMeshData_float32 = _cutcellscpp.LevelSetMeshData_float32 +LevelSetMeshData_float64 = _cutcellscpp.LevelSetMeshData_float64 +LevelSetFunction = _cutcellscpp.LevelSetFunction +LevelSetFunction_float32 = _cutcellscpp.LevelSetFunction_float32 +LevelSetFunction_float64 = _cutcellscpp.LevelSetFunction_float64 +create_level_set_mesh_data = _cutcellscpp.create_level_set_mesh_data +create_level_set_function = _cutcellscpp.create_level_set_function +create_level_set = _cutcellscpp.create_level_set +make_adapt_cell = _cutcellscpp.make_adapt_cell +build_edges = _cutcellscpp.build_edges +make_cell_level_set = _cutcellscpp.make_cell_level_set +evaluate_bernstein = _cutcellscpp.evaluate_bernstein +extract_parent_edge_bernstein = _cutcellscpp.extract_parent_edge_bernstein +restrict_edge_bernstein_exact = _cutcellscpp.restrict_edge_bernstein_exact +classify_edge_roots = _cutcellscpp.classify_edge_roots +classify_new_edges = _cutcellscpp.classify_new_edges +fill_all_vertex_signs_from_level_set = _cutcellscpp.fill_all_vertex_signs_from_level_set +classify_leaf_cells = _cutcellscpp.classify_leaf_cells +process_ready_to_cut_cells = _cutcellscpp.process_ready_to_cut_cells +refine_green_on_multiple_root_edges = _cutcellscpp.refine_green_on_multiple_root_edges +refine_red_on_ambiguous_cells = _cutcellscpp.refine_red_on_ambiguous_cells +certify_and_refine = _cutcellscpp.certify_and_refine +certify_refine_and_process_ready_cells = ( + _cutcellscpp.certify_refine_and_process_ready_cells ) +cut = _cutcellscpp.cut +higher_order_cut = _cutcellscpp.higher_order_cut +create_cut_mesh = _cutcellscpp.create_cut_mesh +locate_cells = _cutcellscpp.locate_cells +cut_vtk_mesh = _cutcellscpp.cut_vtk_mesh +cut_mesh_view = _cutcellscpp.cut_mesh_view +csr_to_vtk_cells = _cutcellscpp.csr_to_vtk_cells +compute_physical_cut_vertices = _cutcellscpp.compute_physical_cut_vertices +complete_from_physical = _cutcellscpp.complete_from_physical +make_quadrature = _cutcellscpp.make_quadrature +runtime_quadrature = _cutcellscpp.runtime_quadrature +physical_points = _cutcellscpp.physical_points +write_level_set_vtu = _cutcellscpp.write_level_set_vtu +ho_cut = _cutcellscpp.ho_cut +HOCutResult = _cutcellscpp.HOCutResult +HOMeshPart = _cutcellscpp.HOMeshPart from .mesh_utils import ( mesh_from_pyvista, diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 358d4f4..7cadaec 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -22,10 +22,17 @@ #include "../../cpp/src/cut_cell.h" #include "../../cpp/src/cut_mesh.h" #include "../../cpp/src/write_vtk.h" +#include "../../cpp/src/bernstein.h" #include "../../cpp/src/mapping.h" #include "../../cpp/src/quadrature.h" #include "../../cpp/src/mesh_view.h" #include "../../cpp/src/level_set.h" +#include "../../cpp/src/adapt_cell.h" +#include "../../cpp/src/level_set_cell.h" +#include "../../cpp/src/edge_certification.h" +#include "../../cpp/src/cell_certification.h" +#include "../../cpp/src/refine_cell.h" +#include "../../cpp/src/ho_cut_mesh.h" namespace nb = nanobind; @@ -139,19 +146,35 @@ cutcells::MeshView make_mesh_view_from_numpy( mesh.offsets = std::span(offsets.data(), static_cast(offsets.size())); - nb::object coords_obj = nb::cast(coordinates); - nb::object conn_obj = nb::cast(connectivity); - nb::object offs_obj = nb::cast(offsets); - nb::object types_obj; + // Keep-alive bundle: holds numpy arrays and the converted cell-type vector. + struct MeshViewOwner + { + nb::object coords, conn, offs, types_numpy; + std::vector cell_types_converted; + }; + auto owner_data = std::make_shared(); + owner_data->coords = nb::cast(coordinates); + owner_data->conn = nb::cast(connectivity); + owner_data->offs = nb::cast(offsets); if (cell_types.has_value()) { - mesh.cell_types = std::span(cell_types->data(), - static_cast(cell_types->size())); - types_obj = nb::cast(*cell_types); + // Convert VTK integer codes to cutcells cell::type enum at the Python boundary. + const int* raw = cell_types->data(); + const std::size_t ncells = static_cast(cell_types->size()); + owner_data->types_numpy = nb::cast(*cell_types); + owner_data->cell_types_converted.reserve(ncells); + for (std::size_t i = 0; i < ncells; ++i) + owner_data->cell_types_converted.push_back( + cutcells::cell::map_vtk_type_to_cell_type( + static_cast(raw[i]))); + mesh.cell_types = std::span( + owner_data->cell_types_converted.data(), ncells); + // Connectivity from Python/VTK uses VTK vertex ordering. + mesh.vtk_vertex_order = true; } - mesh.owner = make_owner_from_objects(coords_obj, conn_obj, offs_obj, types_obj); + mesh.owner = owner_data; return mesh; } @@ -238,8 +261,10 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) { if (self.cell_types.empty()) return nb::ndarray(nullptr, {0}, nb::handle()); + // cell::type has explicit underlying type int; safe to expose as int array. + const int* data = reinterpret_cast(self.cell_types.data()); return nb::ndarray( - self.cell_types.data(), + data, {self.cell_types.size()}, nb::handle()); }, @@ -292,10 +317,17 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) "cell_types", [](const LevelSetMeshDataT& self) { - return nb::ndarray( - self.cell_types.data(), - {self.cell_types.size()}, - nb::cast(self, nb::rv_policy::reference)); + // Convert cell::type enum values to int for Python + auto* owner = new std::vector(); + owner->reserve(self.cell_types.size()); + for (auto ct : self.cell_types) + owner->push_back(static_cast(ct)); + return nb::ndarray( + owner->data(), + {owner->size()}, + nb::capsule(owner, [](void* p) noexcept { + delete static_cast*>(p); + })); }, nb::rv_policy::reference_internal) .def_prop_ro( @@ -359,17 +391,24 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) int tdim, nb::object cell_types_obj) { - std::optional> cell_types; + // Convert int cell types to cell::type enum + // Python passes VTK type integers; convert to cell::type. + std::vector cell_types_vec; if (!cell_types_obj.is_none()) - cell_types = nb::cast>(cell_types_obj); - - std::span cell_types_span; - if (cell_types.has_value()) { - cell_types_span = std::span( - cell_types->data(), static_cast(cell_types->size())); + auto arr = nb::cast>(cell_types_obj); + cell_types_vec.reserve(static_cast(arr.size())); + for (std::size_t i = 0; i < static_cast(arr.size()); ++i) + cell_types_vec.push_back( + cutcells::cell::map_vtk_type_to_cell_type( + static_cast(arr.data()[i]))); } + std::span cell_types_span; + if (!cell_types_vec.empty()) + cell_types_span = std::span( + cell_types_vec.data(), cell_types_vec.size()); + return cutcells::create_level_set_mesh_data( static_cast(dof_coordinates.shape(1)), tdim, @@ -541,22 +580,26 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) m.def( "create_level_set_function", - [](const LevelSetMeshDataT& mesh_data, const ndarray1& dof_values) + [](const LevelSetMeshDataT& mesh_data, const ndarray1& dof_values, + const std::string& name) { auto mesh_data_ptr = std::make_shared(mesh_data); return cutcells::create_level_set_function( std::move(mesh_data_ptr), std::span( dof_values.data(), - static_cast(dof_values.size()))); + static_cast(dof_values.size())), + name); }, nb::arg("mesh_data"), nb::arg("dof_values"), + nb::arg("name") = "phi", "Create a polynomial LevelSetFunction from mesh_data and global dof values."); m.def( - "interpolate_level_set", - [](const MeshViewT& mesh, nb::callable phi, int degree) + "create_level_set", + [](const MeshViewT& mesh, nb::callable phi, int degree, + const std::string& name) { LevelSetMeshDataT mesh_data; { @@ -566,10 +609,17 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) const std::size_t num_dofs = static_cast(mesh_data.num_dofs()); const std::size_t gdim = static_cast(mesh_data.gdim); + // Expose as (ndim, npoints) so x[0] gives all x-coords, etc. + // Underlying storage is row-major (npoints, gdim), so use transposed strides. + const std::size_t shape[2] = {gdim, num_dofs}; + // Strides are in element counts (nanobind/DLPack convention). + // data layout is (num_dofs, gdim) row-major, so to view as (gdim, num_dofs): + // stride[0] = 1 (adjacent coords of the same point) + // stride[1] = gdim (first coord of the next point) + const int64_t strides[2] = {1LL, static_cast(gdim)}; nb::ndarray x( mesh_data.dof_coordinates.data(), - {num_dofs, gdim}, - nb::handle()); + 2, shape, nb::handle(), strides); // Intentional single batched callback invocation. nb::object values_obj = phi(x); @@ -577,7 +627,7 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) if (static_cast(values.size()) != num_dofs) { throw std::runtime_error( - "interpolate_level_set: callback must return a 1D array with length num_dofs"); + "create_level_set: callback must return a 1D array with length num_dofs"); } auto mesh_data_ptr = std::make_shared(std::move(mesh_data)); @@ -585,11 +635,13 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) std::move(mesh_data_ptr), std::span( values.data(), - static_cast(values.size()))); + static_cast(values.size())), + name); }, nb::arg("mesh"), nb::arg("phi"), nb::arg("degree"), + nb::arg("name") = "phi", "Interpolate a batched callable phi(X) at higher-order level-set dof coordinates."); // ---- cut_mesh_view ---- @@ -628,13 +680,20 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) } // Cut mesh with GIL released + // Convert cell::type back to VTK int codes for the legacy cut_vtk_mesh API. + std::vector vtk_types_vec; + vtk_types_vec.reserve(mesh.cell_types.size()); + for (auto ct : mesh.cell_types) + vtk_types_vec.push_back( + static_cast(cutcells::cell::map_cell_type_to_vtk(ct))); + nb::gil_scoped_release release; return mesh::cut_vtk_mesh( std::span(ls_vals.data(), ls_vals.size()), mesh.coordinates, mesh.connectivity, mesh.offsets, - mesh.cell_types, + std::span(vtk_types_vec.data(), vtk_types_vec.size()), cut_type_str, triangulate); }, @@ -1108,6 +1167,521 @@ void declare_float(nb::module_& m, std::string type) "Returns a flat numpy array of shape (total_num_points * 3,)."); } +// ---- HO cut types (BackgroundMeshData, HOCutCells, HOMeshPart) ---- + +template +void declare_ho_cut(nb::module_& m, const std::string& type) +{ + using MeshViewT = cutcells::MeshView; + using LevelSetT = cutcells::LevelSetFunction; + using BGDataT = cutcells::BackgroundMeshData; + using HOCutT = cutcells::HOCutCells; + using PartT = cutcells::HOMeshPart; + + // --- Wrapper that owns both HOCutCells + BackgroundMeshData --- + struct HOCutResult + { + HOCutT cut_cells; + BGDataT bg; + }; + + std::string result_name = "HOCutResult_" + type; + auto py_result = nb::class_(m, result_name.c_str(), + "Result of ho cut(): holds HOCutCells and BackgroundMeshData."); + + py_result + .def_prop_ro("num_cut_cells", + [](const HOCutResult& self) { return self.cut_cells.num_cut_cells(); }) + .def_prop_ro("num_cells", + [](const HOCutResult& self) { return self.bg.num_cells; }) + .def_prop_ro("num_level_sets", + [](const HOCutResult& self) { return self.bg.num_level_sets; }) + .def_prop_ro("level_set_names", + [](const HOCutResult& self) { return self.bg.level_set_names; }) + .def_prop_ro("parent_cell_ids", + [](const HOCutResult& self) { + return nb::ndarray( + self.cut_cells.parent_cell_ids.data(), + {self.cut_cells.parent_cell_ids.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro("cell_domains", + [](const HOCutResult& self) { + const int* data = reinterpret_cast( + self.bg.cell_domains.data()); + return nb::ndarray( + data, + {static_cast(self.bg.num_level_sets), + static_cast(self.bg.num_cells)}, + nb::handle()); + }, + nb::rv_policy::reference_internal, + "Per-level-set domain classification, shape (num_level_sets, num_cells).") + .def("__getitem__", + [](const HOCutResult& self, std::string_view expr_str) { + return cutcells::select_part(self.cut_cells, self.bg, expr_str); + }, + nb::arg("expr"), + "Select a mesh part via expression, e.g. result[\"phi < 0\"]."); + + // --- HOMeshPart --- + std::string part_name = "HOMeshPart_" + type; + nb::class_(m, part_name.c_str(), "Mesh part selected by expression") + .def_prop_ro("dim", + [](const PartT& self) { return self.dim; }) + .def_prop_ro("cut_only", + [](const PartT& self) { return self.cut_only; }) + .def_prop_ro("num_cut_cells", + [](const PartT& self) { + return static_cast(self.cut_cell_ids.size()); + }) + .def_prop_ro("num_uncut_cells", + [](const PartT& self) { + return static_cast(self.uncut_cell_ids.size()); + }) + .def_prop_ro("cut_cell_ids", + [](const PartT& self) { + return nb::ndarray( + self.cut_cell_ids.data(), + {self.cut_cell_ids.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal) + .def_prop_ro("uncut_cell_ids", + [](const PartT& self) { + return nb::ndarray( + self.uncut_cell_ids.data(), + {self.uncut_cell_ids.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal); + + // --- ho_cut() factory --- + m.def("ho_cut", + [](const MeshViewT& mesh, const LevelSetT& ls) { + nb::gil_scoped_release release; + auto [hc, bg] = cutcells::cut(mesh, ls); + return HOCutResult{std::move(hc), std::move(bg)}; + }, + nb::arg("mesh"), nb::arg("level_set"), + "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" + "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); + + m.def("ho_cut", + [](const MeshViewT& mesh, const std::vector& level_sets) { + nb::gil_scoped_release release; + auto [hc, bg] = cutcells::cut(mesh, level_sets); + return HOCutResult{std::move(hc), std::move(bg)}; + }, + nb::arg("mesh"), nb::arg("level_sets"), + "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" + "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); + + // Simple aliases for Python + if constexpr (std::is_same_v) + { + m.attr("HOCutResult") = m.attr(result_name.c_str()); + m.attr("HOMeshPart") = m.attr(part_name.c_str()); + } +} + +template +void declare_certification(nb::module_& m, const std::string& suffix) +{ + using MeshViewT = cutcells::MeshView; + using LevelSetT = cutcells::LevelSetFunction; + using AdaptCellT = cutcells::AdaptCell; + using LevelSetCellT = cutcells::LevelSetCell; + + const std::string adapt_name = "AdaptCell_" + suffix; + nb::class_(m, adapt_name.c_str(), "Adaptive local cell topology") + .def(nb::init<>()) + .def_prop_ro("tdim", [](const AdaptCellT& self) { return self.tdim; }) + .def("num_vertices", &AdaptCellT::n_vertices) + .def("num_edges", [](const AdaptCellT& self) { return self.n_entities(1); }) + .def("num_cells", [](const AdaptCellT& self) { return self.n_entities(self.tdim); }) + .def_prop_ro( + "vertex_coords", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_coords.data(), + {static_cast(self.n_vertices()), + static_cast(self.tdim)}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "vertex_source_edge_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_source_edge_id.data(), + {self.vertex_source_edge_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "edge_connectivity", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[1].indices.data(), + {self.entity_to_vertex[1].indices.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "edge_offsets", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[1].offsets.data(), + {self.entity_to_vertex[1].offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_types", + [](const AdaptCellT& self) + { + std::vector types; + const int n_cells = self.n_entities(self.tdim); + types.reserve(static_cast(n_cells)); + for (int c = 0; c < n_cells; ++c) + types.push_back(static_cast(self.entity_types[self.tdim][static_cast(c)])); + return as_nbarray(std::move(types)); + }, + nb::rv_policy::move) + .def_prop_ro( + "cell_connectivity", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[self.tdim].indices.data(), + {self.entity_to_vertex[self.tdim].indices.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_offsets", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[self.tdim].offsets.data(), + {self.entity_to_vertex[self.tdim].offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def( + "edge_root_tags", + [](const AdaptCellT& self, int level_set_id) + { + std::vector tags; + const int n_edges = self.n_entities(1); + tags.reserve(static_cast(n_edges)); + for (int e = 0; e < n_edges; ++e) + tags.push_back(static_cast(self.get_edge_root_tag(level_set_id, e))); + return as_nbarray(std::move(tags)); + }, + nb::arg("level_set_id")) + .def( + "cell_cert_tags", + [](const AdaptCellT& self, int level_set_id) + { + std::vector tags; + const int n_cells = self.n_entities(self.tdim); + tags.reserve(static_cast(n_cells)); + for (int c = 0; c < n_cells; ++c) + tags.push_back(static_cast(self.get_cell_cert_tag(level_set_id, c))); + return as_nbarray(std::move(tags)); + }, + nb::arg("level_set_id")) + .def( + "edge_green_split_params", + [](const AdaptCellT& self, int level_set_id) + { + const int n_edges = self.n_entities(1); + std::vector params(static_cast(n_edges), T(0)); + for (int e = 0; e < n_edges; ++e) + { + const auto idx = static_cast(level_set_id * n_edges + e); + if (idx < self.edge_green_split_param.size()) + params[static_cast(e)] = self.edge_green_split_param[idx]; + } + return as_nbarray(std::move(params)); + }, + nb::arg("level_set_id")) + .def( + "edge_green_split_mask", + [](const AdaptCellT& self, int level_set_id) + { + const int n_edges = self.n_entities(1); + std::vector mask(static_cast(n_edges), 0); + for (int e = 0; e < n_edges; ++e) + { + const auto idx = static_cast(level_set_id * n_edges + e); + if (idx < self.edge_green_split_has_value.size()) + mask[static_cast(e)] = self.edge_green_split_has_value[idx]; + } + return as_nbarray(std::move(mask)); + }, + nb::arg("level_set_id")); + + const std::string lsc_name = "LevelSetCell_" + suffix; + nb::class_(m, lsc_name.c_str(), "Cell-local Bernstein level set") + .def(nb::init<>()) + .def_prop_ro("cell_id", [](const LevelSetCellT& self) { return self.cell_id; }) + .def_prop_ro("bernstein_order", [](const LevelSetCellT& self) { return self.bernstein_order; }) + .def_prop_ro("cell_type", [](const LevelSetCellT& self) { return self.cell_type; }) + .def_prop_ro( + "bernstein_coeffs", + [](const LevelSetCellT& self) + { + return nb::ndarray( + self.bernstein_coeffs.data(), + {self.bernstein_coeffs.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal); + + m.def( + "make_adapt_cell", + [](const MeshViewT& mesh, int cell_id) + { + nb::gil_scoped_release release; + return cutcells::make_adapt_cell(mesh, cell_id); + }, + nb::arg("mesh"), + nb::arg("cell_id")); + + m.def( + "build_edges", + [](AdaptCellT& adapt_cell) + { + nb::gil_scoped_release release; + cutcells::build_edges(adapt_cell); + }, + nb::arg("adapt_cell")); + + m.def( + "make_cell_level_set", + [](const LevelSetT& ls, int cell_id) + { + nb::gil_scoped_release release; + return cutcells::make_cell_level_set(ls, cell_id); + }, + nb::arg("level_set"), + nb::arg("cell_id")); + + m.def( + "evaluate_bernstein", + [](cell::type cell_type, int degree, const ndarray1& coeffs, const ndarray1& xi) + { + return cutcells::bernstein::evaluate( + cell_type, + degree, + std::span(coeffs.data(), static_cast(coeffs.size())), + std::span(xi.data(), static_cast(xi.size()))); + }, + nb::arg("cell_type"), + nb::arg("degree"), + nb::arg("coeffs"), + nb::arg("xi")); + + m.def( + "extract_parent_edge_bernstein", + [](cell::type parent_cell_type, int degree, + const ndarray1& parent_coeffs, int parent_local_edge_id) + { + std::vector out; + nb::gil_scoped_release release; + cutcells::extract_parent_edge_bernstein( + parent_cell_type, + degree, + std::span(parent_coeffs.data(), static_cast(parent_coeffs.size())), + parent_local_edge_id, + out); + return out; + }, + nb::arg("parent_cell_type"), + nb::arg("degree"), + nb::arg("parent_coeffs"), + nb::arg("parent_local_edge_id")); + + m.def( + "restrict_edge_bernstein_exact", + [](cell::type parent_cell_type, int degree, + const ndarray1& parent_coeffs, + const ndarray1& xi_a, + const ndarray1& xi_b) + { + std::vector out; + nb::gil_scoped_release release; + cutcells::restrict_edge_bernstein_exact( + parent_cell_type, + degree, + std::span(parent_coeffs.data(), static_cast(parent_coeffs.size())), + std::span(xi_a.data(), static_cast(xi_a.size())), + std::span(xi_b.data(), static_cast(xi_b.size())), + out); + return out; + }, + nb::arg("parent_cell_type"), + nb::arg("degree"), + nb::arg("parent_coeffs"), + nb::arg("xi_a"), + nb::arg("xi_b")); + + m.def( + "classify_edge_roots", + [](const ndarray1& edge_coeffs, + T zero_tol, + T sign_tol, + int max_depth) + { + T split_t = T(0); + bool has_split = false; + EdgeRootTag tag = cutcells::classify_edge_roots( + std::span(edge_coeffs.data(), static_cast(edge_coeffs.size())), + zero_tol, + sign_tol, + max_depth, + split_t, + has_split); + return nb::make_tuple(tag, has_split ? nb::cast(split_t) : nb::none()); + }, + nb::arg("edge_coeffs"), + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("max_depth") = 20); + + m.def( + "classify_new_edges", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + T zero_tol, T sign_tol, int max_depth) + { + nb::gil_scoped_release release; + cutcells::classify_new_edges(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, max_depth); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("max_depth") = 20); + + m.def( + "fill_all_vertex_signs_from_level_set", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + T zero_tol) + { + nb::gil_scoped_release release; + cutcells::fill_all_vertex_signs_from_level_set( + adapt_cell, ls_cell, level_set_id, zero_tol); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("zero_tol") = T(1e-12)); + + m.def( + "classify_leaf_cells", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + T zero_tol, T sign_tol) + { + nb::gil_scoped_release release; + cutcells::classify_leaf_cells(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12)); + + m.def( + "process_ready_to_cut_cells", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + T zero_tol, T sign_tol, int edge_max_depth) + { + nb::gil_scoped_release release; + cutcells::process_ready_to_cut_cells( + adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, edge_max_depth); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("edge_max_depth") = 20); + + m.def( + "refine_green_on_multiple_root_edges", + [](AdaptCellT& adapt_cell, int level_set_id) + { + nb::gil_scoped_release release; + return cutcells::refine_green_on_multiple_root_edges(adapt_cell, level_set_id); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_id")); + + m.def( + "refine_red_on_ambiguous_cells", + [](AdaptCellT& adapt_cell, int level_set_id) + { + nb::gil_scoped_release release; + return cutcells::refine_red_on_ambiguous_cells(adapt_cell, level_set_id); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_id")); + + m.def( + "certify_refine_and_process_ready_cells", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + int max_iterations, T zero_tol, T sign_tol, int edge_max_depth) + { + nb::gil_scoped_release release; + cutcells::certify_refine_and_process_ready_cells( + adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, edge_max_depth); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("max_iterations") = 8, + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("edge_max_depth") = 20); + + m.def( + "certify_and_refine", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + int max_iterations, T zero_tol, T sign_tol, int edge_max_depth) + { + nb::gil_scoped_release release; + cutcells::certify_and_refine(adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, + edge_max_depth); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("max_iterations") = 8, + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("edge_max_depth") = 20); + + if constexpr (std::is_same_v) + { + m.attr("AdaptCell") = m.attr(adapt_name.c_str()); + m.attr("LevelSetCell") = m.attr(lsc_name.c_str()); + } +} + } // namespace NB_MODULE(_cutcellscpp, m) @@ -1125,11 +1699,40 @@ NB_MODULE(_cutcellscpp, m) .value("prism", cell::type::prism) .value("pyramid", cell::type::pyramid); + nb::enum_(m, "EdgeRootTag") + .value("not_classified", cutcells::EdgeRootTag::not_classified) + .value("no_root", cutcells::EdgeRootTag::no_root) + .value("one_root", cutcells::EdgeRootTag::one_root) + .value("multiple_roots", cutcells::EdgeRootTag::multiple_roots) + .value("zero", cutcells::EdgeRootTag::zero); + + nb::enum_(m, "CellCertTag") + .value("not_classified", cutcells::CellCertTag::not_classified) + .value("positive", cutcells::CellCertTag::positive) + .value("negative", cutcells::CellCertTag::negative) + .value("cut", cutcells::CellCertTag::cut) + .value("zero", cutcells::CellCertTag::zero) + .value("ambiguous", cutcells::CellCertTag::ambiguous) + .value("ready_to_cut", cutcells::CellCertTag::ready_to_cut); + + nb::enum_(m, "FaceCertTag") + .value("not_classified", cutcells::FaceCertTag::not_classified) + .value("positive", cutcells::FaceCertTag::positive) + .value("negative", cutcells::FaceCertTag::negative) + .value("cut", cutcells::FaceCertTag::cut) + .value("zero", cutcells::FaceCertTag::zero) + .value("ambiguous", cutcells::FaceCertTag::ambiguous); + declare_float(m, "float32"); declare_float(m, "float64"); declare_meshview_and_levelset(m, "float32"); declare_meshview_and_levelset(m, "float64"); + declare_certification(m, "float32"); + declare_certification(m, "float64"); + + declare_ho_cut(m, "float32"); + declare_ho_cut(m, "float64"); m.def("csr_to_vtk_cells", [](const nb::ndarray, nb::c_contig>& connectivity, From 8599d4a11c2808b4a428d23234addcf84fd222bd Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Fri, 17 Apr 2026 15:47:38 +0200 Subject: [PATCH 08/23] first straight cut with one level set working in new ho pipeline with bernstein reconstruction --- cpp/src/CMakeLists.txt | 18 + cpp/src/adapt_cell.cpp | 160 ++-- cpp/src/adapt_cell.h | 12 + cpp/src/bernstein.cpp | 128 +++- cpp/src/cell_certification.cpp | 410 +++++++++- cpp/src/cell_certification.h | 44 ++ cpp/src/cell_topology.h | 46 ++ cpp/src/cut_mesh.h | 8 +- cpp/src/ho_cut_mesh.cpp | 115 ++- cpp/src/ho_cut_mesh.h | 5 +- cpp/src/ho_mesh_part_output.cpp | 706 ++++++++++++++++++ cpp/src/ho_mesh_part_output.h | 28 + cpp/src/level_set_cell.cpp | 386 +++++++++- cpp/src/mapping.cpp | 43 ++ cpp/src/mapping.h | 17 + cpp/src/refine_cell.cpp | 47 +- python/cutcells/__init__.py | 8 + python/cutcells/wrapper.cpp | 682 ++++++++++++++++- python/demo/bench_cut_pipeline.py | 520 ------------- python/demo/demo_level_set_ho_curved_vtk.py | 213 ------ python/demo/demo_level_set_ho_vtk.py | 8 +- python/demo/demo_meshview.py | 19 - python/demo/demo_meshview_ho_cut.py | 189 +++++ python/demo/meshview_levelset_demo.py | 325 -------- python/tests/test_certification_refinement.py | 499 +++++++++++++ python/tests/test_ho_part_straight_output.py | 69 ++ python/tests/test_level_set_mesh_data.py | 45 ++ python/tests/test_level_set_vtu.py | 21 + 28 files changed, 3547 insertions(+), 1224 deletions(-) create mode 100644 cpp/src/ho_mesh_part_output.cpp create mode 100644 cpp/src/ho_mesh_part_output.h delete mode 100644 python/demo/bench_cut_pipeline.py delete mode 100644 python/demo/demo_level_set_ho_curved_vtk.py create mode 100644 python/demo/demo_meshview_ho_cut.py delete mode 100644 python/demo/meshview_levelset_demo.py create mode 100644 python/tests/test_certification_refinement.py create mode 100644 python/tests/test_ho_part_straight_output.py diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index a12885a..214bcf9 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -26,8 +26,17 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.h ${CMAKE_CURRENT_SOURCE_DIR}/mapping.h + ${CMAKE_CURRENT_SOURCE_DIR}/bernstein.h ${CMAKE_CURRENT_SOURCE_DIR}/level_set.h + ${CMAKE_CURRENT_SOURCE_DIR}/level_set_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/mesh_view.h + ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.h + ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.h + ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.h + ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.h + ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.h + ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.h + ${CMAKE_CURRENT_SOURCE_DIR}/refine_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature_tables.h ${CMAKE_CURRENT_SOURCE_DIR}/span_math.h @@ -47,7 +56,16 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_mesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/bernstein.cpp ${CMAKE_CURRENT_SOURCE_DIR}/level_set.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/level_set_cell.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/refine_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.cpp ${CMAKE_CURRENT_SOURCE_DIR}/write_vtk.cpp ) diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp index a70bbf0..513c188 100644 --- a/cpp/src/adapt_cell.cpp +++ b/cpp/src/adapt_cell.cpp @@ -14,6 +14,107 @@ namespace cutcells { + + template +void build_faces(AdaptCell& ac) +{ + if (ac.tdim < 3) + return; + + // Clear existing 2D entity pool. + ac.entity_types[2].clear(); + ac.entity_to_vertex[2].offsets.clear(); + ac.entity_to_vertex[2].indices.clear(); + ac.entity_to_vertex[2].offsets.push_back(std::int32_t(0)); + + // Invalidate face cert tag storage: face count is about to change and + // faces may be reordered, so all old tags are stale. + ac.face_cert_tag.clear(); + ac.face_cert_tag_num_level_sets = 0; + + // Track unique faces by sorted vertex set → face_id. + std::map, int> face_map; + + const int n_cells = ac.n_entities(ac.tdim); + for (int c = 0; c < n_cells; ++c) + { + const cell::type ctype = ac.entity_types[ac.tdim][static_cast(c)]; + auto cell_verts = ac.entity_to_vertex[ac.tdim][static_cast(c)]; + const int nf = cell::num_faces(ctype); + + for (int fi = 0; fi < nf; ++fi) + { + auto local_fv = cell::face_vertices(ctype, fi); + const int fsize = static_cast(local_fv.size()); + + // Build global-vertex face and sorted key + std::vector global_fv(static_cast(fsize)); + std::vector sorted_fv(static_cast(fsize)); + for (int j = 0; j < fsize; ++j) + { + global_fv[static_cast(j)] = + cell_verts[static_cast(local_fv[static_cast(j)])]; + sorted_fv[static_cast(j)] = global_fv[static_cast(j)]; + } + std::sort(sorted_fv.begin(), sorted_fv.end()); + + if (face_map.find(sorted_fv) == face_map.end()) + { + face_map[sorted_fv] = ac.n_entities(2); + ac.entity_types[2].push_back(cell::face_type(ctype, fi)); + for (int j = 0; j < fsize; ++j) + ac.entity_to_vertex[2].indices.push_back(global_fv[static_cast(j)]); + ac.entity_to_vertex[2].offsets.push_back( + static_cast(ac.entity_to_vertex[2].indices.size())); + } + } + } +} + +// --------------------------------------------------------------------------- +// build_edges +// --------------------------------------------------------------------------- + +template +void build_edges(AdaptCell& ac) +{ + const int tdim = ac.tdim; + + // Clear existing 1D entity pool. + ac.entity_types[1].clear(); + ac.entity_to_vertex[1].offsets.clear(); + ac.entity_to_vertex[1].indices.clear(); + ac.entity_to_vertex[1].offsets.push_back(std::int32_t(0)); + + // Track unique edges as (min_v, max_v) → edge_id. + std::map, int> edge_map; + + const int n_cells = ac.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + const cell::type ctype = ac.entity_types[tdim][static_cast(c)]; + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + auto cell_edges = cell::edges(ctype); + + for (const auto& ce : cell_edges) + { + const std::int32_t lv0 = cell_verts[static_cast(ce[0])]; + const std::int32_t lv1 = cell_verts[static_cast(ce[1])]; + const auto key = std::make_pair(std::min(lv0, lv1), std::max(lv0, lv1)); + + if (edge_map.find(key) == edge_map.end()) + { + edge_map[key] = ac.n_entities(1); + ac.entity_types[1].push_back(cell::type::interval); + ac.entity_to_vertex[1].indices.push_back(lv0); + ac.entity_to_vertex[1].indices.push_back(lv1); + ac.entity_to_vertex[1].offsets.push_back( + static_cast(ac.entity_to_vertex[1].indices.size())); + } + } + } +} + // --------------------------------------------------------------------------- // make_adapt_cell // --------------------------------------------------------------------------- @@ -67,6 +168,12 @@ AdaptCell make_adapt_cell(const MeshView& mesh, I cell_id) for (int v = 0; v < nv; ++v) ac.entity_to_vertex[tdim].indices[static_cast(v)] = std::int32_t(v); + + // Create edges and faces (if tdim=3) in the entity pools + build_edges(ac); + + if (tdim == 3) + build_faces(ac); return ac; } @@ -102,6 +209,12 @@ void fill_vertex_signs(AdaptCell& ac, // Explicit template instantiations // --------------------------------------------------------------------------- +template void build_edges(AdaptCell&); +template void build_edges(AdaptCell&); + +template void build_faces(AdaptCell&); +template void build_faces(AdaptCell&); + template AdaptCell make_adapt_cell(const MeshView&, int); template AdaptCell make_adapt_cell(const MeshView&, int); template AdaptCell make_adapt_cell(const MeshView&, long); @@ -110,51 +223,4 @@ template AdaptCell make_adapt_cell(const MeshView&, long); template void fill_vertex_signs(AdaptCell&, std::span, int, double); template void fill_vertex_signs(AdaptCell&, std::span, int, float); -// --------------------------------------------------------------------------- -// build_edges -// --------------------------------------------------------------------------- - -template -void build_edges(AdaptCell& ac) -{ - const int tdim = ac.tdim; - - // Clear existing 1D entity pool. - ac.entity_types[1].clear(); - ac.entity_to_vertex[1].offsets.clear(); - ac.entity_to_vertex[1].indices.clear(); - ac.entity_to_vertex[1].offsets.push_back(std::int32_t(0)); - - // Track unique edges as (min_v, max_v) → edge_id. - std::map, int> edge_map; - - const int n_cells = ac.n_entities(tdim); - for (int c = 0; c < n_cells; ++c) - { - const cell::type ctype = ac.entity_types[tdim][static_cast(c)]; - auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; - auto cell_edges = cell::edges(ctype); - - for (const auto& ce : cell_edges) - { - const std::int32_t lv0 = cell_verts[static_cast(ce[0])]; - const std::int32_t lv1 = cell_verts[static_cast(ce[1])]; - const auto key = std::make_pair(std::min(lv0, lv1), std::max(lv0, lv1)); - - if (edge_map.find(key) == edge_map.end()) - { - edge_map[key] = ac.n_entities(1); - ac.entity_types[1].push_back(cell::type::interval); - ac.entity_to_vertex[1].indices.push_back(lv0); - ac.entity_to_vertex[1].indices.push_back(lv1); - ac.entity_to_vertex[1].offsets.push_back( - static_cast(ac.entity_to_vertex[1].indices.size())); - } - } - } -} - -template void build_edges(AdaptCell&); -template void build_edges(AdaptCell&); - } // namespace cutcells diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h index 06f7150..888805e 100644 --- a/cpp/src/adapt_cell.h +++ b/cpp/src/adapt_cell.h @@ -515,4 +515,16 @@ void fill_vertex_signs(AdaptCell& ac, template void build_edges(AdaptCell& ac); +/// Build (or rebuild) entity_to_vertex[2] from all top-dimensional leaf cells. +/// +/// Only meaningful for 3D cells (tdim == 3). Clears any existing 2D entity +/// pool, then re-derives faces by iterating every cell in entity_to_vertex[3] +/// and adding vertex-tuples from the cell type's face table +/// (cell_topology.h ordering). Duplicate faces are suppressed; each unique +/// sorted vertex set appears once. +/// +/// @param ac AdaptCell to update in place. +template +void build_faces(AdaptCell& ac); + } // namespace cutcells diff --git a/cpp/src/bernstein.cpp b/cpp/src/bernstein.cpp index 13b2fb9..ce4e5bd 100644 --- a/cpp/src/bernstein.cpp +++ b/cpp/src/bernstein.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace cutcells::bernstein @@ -207,6 +208,103 @@ void solve_dense(int n, std::vector& A, std::vector& b) } } +template +struct TransformKey +{ + cell::type ctype = cell::type::point; + int degree = 0; + std::vector ref_points; + + bool operator==(const TransformKey&) const = default; +}; + +template +struct TransformKeyHash +{ + std::size_t operator()(const TransformKey& key) const noexcept + { + std::size_t seed = std::hash{}(static_cast(key.ctype)); + seed ^= std::hash{}(key.degree) + 0x9e3779b97f4a7c15ULL + + (seed << 6) + (seed >> 2); + seed ^= std::hash{}(key.ref_points.size()) + 0x9e3779b97f4a7c15ULL + + (seed << 6) + (seed >> 2); + for (const T value : key.ref_points) + { + seed ^= std::hash{}(value) + 0x9e3779b97f4a7c15ULL + + (seed << 6) + (seed >> 2); + } + return seed; + } +}; + +template +std::vector build_lagrange_to_bernstein_transform(cell::type ctype, int degree, + std::span ref_points) +{ + const int N = num_polynomials(ctype, degree); + const int tdim = cell::get_tdim(ctype); + + std::vector V(static_cast(N) * static_cast(N)); + std::vector basis_row(static_cast(N)); + + for (int i = 0; i < N; ++i) + { + const T* pt = ref_points.data() + + static_cast(i) * static_cast(tdim); + evaluate_basis( + ctype, degree, + std::span(pt, static_cast(tdim)), + std::span(basis_row)); + + for (int j = 0; j < N; ++j) + { + V[static_cast(i) * static_cast(N) + + static_cast(j)] = basis_row[static_cast(j)]; + } + } + + // Build the inverse transform once so repeated conversions only need a + // dense matrix-vector multiply. + std::vector transform(static_cast(N) * static_cast(N), T(0)); + std::vector rhs(static_cast(N), T(0)); + for (int col = 0; col < N; ++col) + { + std::vector A = V; + std::fill(rhs.begin(), rhs.end(), T(0)); + rhs[static_cast(col)] = T(1); + solve_dense(N, A, rhs); + for (int row = 0; row < N; ++row) + { + transform[static_cast(row) * static_cast(N) + + static_cast(col)] = rhs[static_cast(row)]; + } + } + + return transform; +} + +template +const std::vector& cached_lagrange_to_bernstein_transform( + cell::type ctype, int degree, std::span ref_points) +{ + thread_local std::unordered_map, std::vector, TransformKeyHash> + cache; + + TransformKey key; + key.ctype = ctype; + key.degree = degree; + key.ref_points.assign(ref_points.begin(), ref_points.end()); + + auto it = cache.find(key); + if (it != cache.end()) + return it->second; + + auto transform = build_lagrange_to_bernstein_transform(ctype, degree, ref_points); + auto [inserted_it, inserted] = cache.emplace(std::move(key), std::move(transform)); + (void)inserted; + return inserted_it->second; +} + // --------------------------------------------------------------------------- // Simplex Bernstein evaluation // --------------------------------------------------------------------------- @@ -756,27 +854,21 @@ void lagrange_to_bernstein(cell::type ctype, int degree, if (static_cast(ref_points.size()) != ndofs * tdim) throw std::invalid_argument( "bernstein::lagrange_to_bernstein: ref_points size mismatch"); + const auto& transform = cached_lagrange_to_bernstein_transform( + ctype, degree, ref_points); - // Build the Bernstein evaluation matrix V: V[i][j] = B_j(ref_point_i) - std::vector V(static_cast(N) * static_cast(N)); - std::vector basis_row(static_cast(N)); - - for (int i = 0; i < N; ++i) + coeffs.assign(static_cast(N), T(0)); + for (int row = 0; row < N; ++row) { - const T* pt = ref_points.data() + static_cast(i) * static_cast(tdim); - std::span pt_span(pt, static_cast(tdim)); - std::span row_span(basis_row); - - evaluate_basis(ctype, degree, pt_span, row_span); - - for (int j = 0; j < N; ++j) - V[static_cast(i) * static_cast(N) - + static_cast(j)] = basis_row[static_cast(j)]; + T value = T(0); + for (int col = 0; col < N; ++col) + { + value += transform[static_cast(row) * static_cast(N) + + static_cast(col)] + * nodal_values[static_cast(col)]; + } + coeffs[static_cast(row)] = value; } - - // Solve V * coeffs = nodal_values - coeffs.assign(nodal_values.begin(), nodal_values.end()); - solve_dense(N, V, coeffs); } template void lagrange_to_bernstein(cell::type, int, diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index ea9f4d6..6e2372f 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -70,6 +70,159 @@ void append_top_cell_local(std::vector& types, adj.offsets.push_back(static_cast(adj.indices.size())); } +int basix_edge_id_for_vertices(cell::type cell_type, int a, int b) +{ + const auto ledges = cell::edges(cell_type); + for (std::size_t e = 0; e < ledges.size(); ++e) + { + const auto edge = ledges[e]; + if ((edge[0] == a && edge[1] == b) || (edge[0] == b && edge[1] == a)) + return static_cast(e); + } + + throw std::runtime_error("basix_edge_id_for_vertices: edge not found"); +} + +template +std::vector canonicalize_cut_part_cell_vertices( + const cell::CutCell& part, + int part_cell_id, + cell::type leaf_cell_type) +{ + auto part_vertices = cell::cell_vertices(part, part_cell_id); + std::vector canonical(part_vertices.begin(), part_vertices.end()); + + const cell::type part_cell_type = + part._types[static_cast(part_cell_id)]; + + if (leaf_cell_type == cell::type::triangle + && part_cell_type == cell::type::quadrilateral) + { + std::map token_to_part_vertex; + std::array present = {false, false, false}; + + for (int part_lv : part_vertices) + { + const int token = cell::vtk_parent_entity_token_to_basix( + leaf_cell_type, + part._vertex_parent_entity[static_cast(part_lv)]); + token_to_part_vertex[token] = part_lv; + if (token >= 100 && token < 103) + present[static_cast(token - 100)] = true; + } + + int omitted = -1; + for (int v = 0; v < 3; ++v) + { + if (!present[static_cast(v)]) + { + omitted = v; + break; + } + } + if (omitted < 0) + throw std::runtime_error("canonicalize_cut_part_cell_vertices: invalid triangle quad"); + + const auto kept_edge = cell::edges(cell::type::triangle)[static_cast(omitted)]; + const int a = kept_edge[0]; + const int b = kept_edge[1]; + const int e_a = basix_edge_id_for_vertices(cell::type::triangle, a, omitted); + const int e_b = basix_edge_id_for_vertices(cell::type::triangle, b, omitted); + + canonical = { + token_to_part_vertex.contains(100 + a) ? token_to_part_vertex.at(100 + a) + : throw std::runtime_error("canonicalize_cut_part_cell_vertices: missing triangle vertex token a"), + token_to_part_vertex.contains(100 + b) ? token_to_part_vertex.at(100 + b) + : throw std::runtime_error("canonicalize_cut_part_cell_vertices: missing triangle vertex token b"), + token_to_part_vertex.contains(e_a) ? token_to_part_vertex.at(e_a) + : throw std::runtime_error("canonicalize_cut_part_cell_vertices: missing triangle edge token ea"), + token_to_part_vertex.contains(e_b) ? token_to_part_vertex.at(e_b) + : throw std::runtime_error("canonicalize_cut_part_cell_vertices: missing triangle edge token eb")}; + } + else if (leaf_cell_type == cell::type::tetrahedron + && part_cell_type == cell::type::prism) + { + std::map token_to_part_vertex; + std::array present = {false, false, false, false}; + + for (int part_lv : part_vertices) + { + const int token = cell::vtk_parent_entity_token_to_basix( + leaf_cell_type, + part._vertex_parent_entity[static_cast(part_lv)]); + token_to_part_vertex[token] = part_lv; + if (token >= 100 && token < 104) + present[static_cast(token - 100)] = true; + } + + std::vector present_vertices; + std::vector missing_vertices; + for (int v = 0; v < 4; ++v) + { + if (present[static_cast(v)]) + present_vertices.push_back(v); + else + missing_vertices.push_back(v); + } + + if (present_vertices.size() == 3) + { + const int omitted = missing_vertices.front(); + const auto face = cell::tet_face(omitted); + canonical.resize(6); + for (int i = 0; i < 3; ++i) + { + const int fv = face[static_cast(i)]; + const int edge_id = basix_edge_id_for_vertices( + cell::type::tetrahedron, fv, omitted); + auto v_it = token_to_part_vertex.find(100 + fv); + auto e_it = token_to_part_vertex.find(edge_id); + if (v_it == token_to_part_vertex.end() || e_it == token_to_part_vertex.end()) + { + throw std::runtime_error( + "canonicalize_cut_part_cell_vertices: invalid 3-vertex prism token set"); + } + canonical[static_cast(i)] = v_it->second; + canonical[static_cast(3 + i)] = e_it->second; + } + } + else if (present_vertices.size() == 2) + { + const int p = present_vertices[0]; + const int q = present_vertices[1]; + const int r = missing_vertices[0]; + const int s = missing_vertices[1]; + + const int e_pr = basix_edge_id_for_vertices(cell::type::tetrahedron, p, r); + const int e_ps = basix_edge_id_for_vertices(cell::type::tetrahedron, p, s); + const int e_qr = basix_edge_id_for_vertices(cell::type::tetrahedron, q, r); + const int e_qs = basix_edge_id_for_vertices(cell::type::tetrahedron, q, s); + + const std::array prism_tokens = { + 100 + p, e_pr, e_ps, + 100 + q, e_qr, e_qs}; + canonical.resize(6); + for (int i = 0; i < 6; ++i) + { + auto it = token_to_part_vertex.find(prism_tokens[static_cast(i)]); + if (it == token_to_part_vertex.end()) + { + throw std::runtime_error( + "canonicalize_cut_part_cell_vertices: invalid 2-vertex prism token set"); + } + canonical[static_cast(i)] = it->second; + } + } + else + { + throw std::runtime_error( + "canonicalize_cut_part_cell_vertices: unsupported tetra prism token pattern"); + } + } + + return canonical; +} + template std::vector gather_leaf_cell_vertex_level_set_values( const AdaptCell& adapt_cell, @@ -391,9 +544,20 @@ void append_ready_cut_part_cells( adapt_cell, x, parent_dim, parent_id, std::span(), /*source_edge_id=*/-1); - const T value = ls_cell.value(x); - set_vertex_sign_for_level_set( - adapt_cell, vertex_id, level_set_id, value, zero_tol); + if (token >= 0 && token < 100) + { + // Straight cut vertices lie on the straight interface by construction. + // They should therefore participate in phi=0 face extraction even when + // the curved level set evaluated at this affine point is not exactly zero. + set_vertex_sign_for_level_set( + adapt_cell, vertex_id, level_set_id, T(0), zero_tol); + } + else + { + const T value = ls_cell.value(x); + set_vertex_sign_for_level_set( + adapt_cell, vertex_id, level_set_id, value, zero_tol); + } } token_to_vertex[token] = vertex_id; @@ -402,14 +566,25 @@ void append_ready_cut_part_cells( const int n_part_cells = cell::num_cells(part); for (int pc = 0; pc < n_part_cells; ++pc) { - auto part_vertices = cell::cell_vertices(part, pc); + const std::vector part_vertices = + canonicalize_cut_part_cell_vertices(part, pc, leaf_cell_type); std::vector mapped(part_vertices.size(), -1); for (std::size_t j = 0; j < part_vertices.size(); ++j) { const int part_lv = part_vertices[j]; const int raw_token = part._vertex_parent_entity[static_cast(part_lv)]; const int token = cell::vtk_parent_entity_token_to_basix(leaf_cell_type, raw_token); - mapped[j] = token_to_vertex.at(token); + auto token_it = token_to_vertex.find(token); + if (token_it == token_to_vertex.end()) + { + throw std::runtime_error( + "append_ready_cut_part_cells: missing mapped token " + + std::to_string(token) + " raw_token=" + std::to_string(raw_token) + + " leaf_type=" + cell_type_to_str(leaf_cell_type) + + " part_cell_type=" + + cell_type_to_str(part._types[static_cast(pc)])); + } + mapped[j] = token_it->second; } append_top_cell_local(new_types, new_cells, @@ -474,6 +649,37 @@ CellCertTag classify_ready_to_cut_topology(const AdaptCell& adapt_cell, return CellCertTag::not_classified; } +/// Check whether any edge incident to face_id has a root (one_root, multiple_roots, or zero). +template +bool face_has_any_edge_root(const AdaptCell& adapt_cell, + int level_set_id, int face_id) +{ + auto face_verts = adapt_cell.entity_to_vertex[2][static_cast(face_id)]; + const int n_edges = adapt_cell.n_entities(1); + + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + bool v0_in = false, v1_in = false; + for (auto fv : face_verts) + { + if (fv == ev[0]) v0_in = true; + if (fv == ev[1]) v1_in = true; + } + if (!v0_in || !v1_in) + continue; + + const EdgeRootTag etag = adapt_cell.get_edge_root_tag(level_set_id, e); + if (etag == EdgeRootTag::one_root + || etag == EdgeRootTag::multiple_roots + || etag == EdgeRootTag::zero) + { + return true; + } + } + return false; +} + } // anonymous namespace // ===================================================================== @@ -722,7 +928,30 @@ CellCertTag classify_leaf_cell(const AdaptCell& adapt_cell, std::span sc(subcell_coeffs); if (bernstein_all_zero(sc, zero_tol)) - return CellCertTag::zero; + { + // All Bernstein coefficients are zero within tolerance. + // Evaluate the level set at the cell centroid to disambiguate + // between a truly zero cell and a cell that is inside/outside + // but whose vertices all happen to lie on the zero set. + std::vector xi_centroid(static_cast(parent_tdim), T(0)); + for (int v = 0; v < n_cell_verts; ++v) + { + int gv = cell_verts[static_cast(v)]; + for (int d = 0; d < parent_tdim; ++d) + xi_centroid[static_cast(d)] += + adapt_cell.vertex_coords[static_cast(gv * parent_tdim + d)]; + } + for (int d = 0; d < parent_tdim; ++d) + xi_centroid[static_cast(d)] /= T(n_cell_verts); + + const T centroid_val = ls_cell.value( + std::span(xi_centroid.data(), + static_cast(parent_tdim))); + + if (std::fabs(centroid_val) <= zero_tol) + return CellCertTag::zero; + return (centroid_val > T(0)) ? CellCertTag::positive : CellCertTag::negative; + } if (bernstein_all_positive(sc, sign_tol)) return CellCertTag::positive; if (bernstein_all_negative(sc, sign_tol)) @@ -841,6 +1070,131 @@ void classify_leaf_cells(AdaptCell& adapt_cell, } } +// ===================================================================== +// classify_leaf_face +// ===================================================================== + +template +FaceCertTag classify_leaf_face(const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int face_id, + T zero_tol, T sign_tol) +{ + // Only meaningful for 3D cells. + if (adapt_cell.tdim != 3) + return FaceCertTag::not_classified; + + // A. Check incident-edge root topology. + const bool has_root = face_has_any_edge_root(adapt_cell, level_set_id, face_id); + if (has_root) + return FaceCertTag::cut; + + // B. Restrict the parent Bernstein to the face. + auto face_verts = adapt_cell.entity_to_vertex[2][static_cast(face_id)]; + const int parent_tdim = adapt_cell.tdim; + const int n_face_verts = static_cast(face_verts.size()); + std::vector face_vertex_coords( + static_cast(n_face_verts * parent_tdim)); + + for (int v = 0; v < n_face_verts; ++v) + { + int gv = face_verts[static_cast(v)]; + for (int d = 0; d < parent_tdim; ++d) + face_vertex_coords[static_cast(v * parent_tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * parent_tdim + d)]; + } + + cell::type face_type = adapt_cell.entity_types[2][static_cast(face_id)]; + + std::vector face_coeffs; + restrict_subcell_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + face_type, + std::span(face_vertex_coords), + face_coeffs); + + // Sign-hull classification. + std::span fc(face_coeffs); + + if (bernstein_all_zero(fc, zero_tol)) + { + // All Bernstein coefficients are zero — evaluate centroid. + std::vector xi_centroid(static_cast(parent_tdim), T(0)); + for (int v = 0; v < n_face_verts; ++v) + { + int gv = face_verts[static_cast(v)]; + for (int d = 0; d < parent_tdim; ++d) + xi_centroid[static_cast(d)] += + adapt_cell.vertex_coords[static_cast(gv * parent_tdim + d)]; + } + for (int d = 0; d < parent_tdim; ++d) + xi_centroid[static_cast(d)] /= T(n_face_verts); + + const T centroid_val = ls_cell.value( + std::span(xi_centroid.data(), + static_cast(parent_tdim))); + + if (std::fabs(centroid_val) <= zero_tol) + return FaceCertTag::zero; + return (centroid_val > T(0)) ? FaceCertTag::positive : FaceCertTag::negative; + } + if (bernstein_all_positive(fc, sign_tol)) + return FaceCertTag::positive; + if (bernstein_all_negative(fc, sign_tol)) + return FaceCertTag::negative; + + // Mixed Bernstein signs — apply monotonicity filter. + if (has_monotone_direction(face_type, ls_cell.bernstein_order, fc, sign_tol)) + { + // Evaluate phi at the first vertex of the face. + std::span xi_v0( + adapt_cell.vertex_coords.data() + + static_cast(face_verts[0]) * static_cast(parent_tdim), + static_cast(parent_tdim)); + const T val = bernstein::evaluate( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), xi_v0); + return (val >= T(0)) ? FaceCertTag::positive : FaceCertTag::negative; + } + + return FaceCertTag::ambiguous; +} + +// ===================================================================== +// classify_leaf_faces +// ===================================================================== + +template +void classify_leaf_faces(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol) +{ + if (adapt_cell.tdim != 3) + return; + + const int n_faces = adapt_cell.n_entities(2); + if (n_faces == 0) + return; + + // Ensure tag storage. + if (adapt_cell.face_cert_tag_num_level_sets <= level_set_id) + adapt_cell.resize_face_cert_tags(level_set_id + 1); + + for (int f = 0; f < n_faces; ++f) + { + if (adapt_cell.get_face_cert_tag(level_set_id, f) + != FaceCertTag::not_classified) + continue; + + FaceCertTag tag = classify_leaf_face( + adapt_cell, ls_cell, level_set_id, f, zero_tol, sign_tol); + adapt_cell.set_face_cert_tag(level_set_id, f, tag); + } +} + // ===================================================================== // fill_all_vertex_signs_from_level_set // ===================================================================== @@ -943,8 +1297,19 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, { const int a = old_cell_vertices[static_cast(ledges[le][0])]; const int b = old_cell_vertices[static_cast(ledges[le][1])]; - old_edge_ids_by_local_edge[le] = - edge_lookup.at({std::min(a, b), std::max(a, b)}); + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto edge_it = edge_lookup.find(key); + if (edge_it == edge_lookup.end()) + { + throw std::runtime_error( + "process_ready_to_cut_cells: missing leaf edge for ready-to-cut cell " + + std::to_string(c) + " type=" + + cell_type_to_str(leaf_cell_type) + " local_edge=" + + std::to_string(le) + " key=(" + + std::to_string(key.first) + "," + + std::to_string(key.second) + ")"); + } + old_edge_ids_by_local_edge[le] = edge_it->second; } std::map token_to_vertex; @@ -1057,6 +1422,14 @@ void certify_and_refine(AdaptCell& adapt_cell, classify_new_edges(adapt_cell, ls_cell, level_set_id, zero_tol, sign_tol, edge_max_depth); + // 1b. Build faces and classify them (3D only). + if (adapt_cell.tdim == 3) + { + build_faces(adapt_cell); + classify_leaf_faces(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol); + } + // 2. Classify cells. classify_leaf_cells(adapt_cell, ls_cell, level_set_id, zero_tol, sign_tol); @@ -1119,7 +1492,6 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, zero_tol, sign_tol, edge_max_depth); - fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); } // ===================================================================== @@ -1157,6 +1529,26 @@ template void classify_leaf_cells(AdaptCell&, const LevelSetCell&, int, double, double); +template FaceCertTag classify_leaf_face(const AdaptCell&, + const LevelSetCell&, + int, int, double, double); +template FaceCertTag classify_leaf_face(const AdaptCell&, + const LevelSetCell&, + int, int, float, float); +template FaceCertTag classify_leaf_face(const AdaptCell&, + const LevelSetCell&, + int, int, double, double); + +template void classify_leaf_faces(AdaptCell&, + const LevelSetCell&, + int, double, double); +template void classify_leaf_faces(AdaptCell&, + const LevelSetCell&, + int, float, float); +template void classify_leaf_faces(AdaptCell&, + const LevelSetCell&, + int, double, double); + template void fill_all_vertex_signs_from_level_set(AdaptCell&, const LevelSetCell&, int, double); diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index 4d07880..daac87d 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -88,6 +88,50 @@ void classify_leaf_cells(AdaptCell& adapt_cell, int level_set_id, T zero_tol, T sign_tol); +// ===================================================================== +// Face (facet) classifier (3D only) +// ===================================================================== + +/// Classify a single leaf face for one level set. +/// +/// Logic mirrors classify_leaf_cell but operates on a 2D face entity +/// (dimension 2) of a 3D AdaptCell: +/// 1. Check incident-edge root topology for a valid 2D cut pattern. +/// 2. Restrict the parent Bernstein to the face and test the sign hull. +/// 3. Apply monotonicity filter. +/// 4. If the centroid is needed (all-zero case), evaluate there. +/// 5. Otherwise → ambiguous. +/// +/// @param adapt_cell The AdaptCell. +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set. +/// @param face_id Index of the leaf face in entity_to_vertex[2]. +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +/// @return FaceCertTag. +template +FaceCertTag classify_leaf_face(const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int face_id, + T zero_tol, T sign_tol); + +/// Classify all not-yet-classified leaf faces for one level set. +/// +/// Face entities (entity_to_vertex[2]) must already be populated via +/// build_faces(). Only operates when tdim == 3. +/// +/// @param adapt_cell The AdaptCell (modified in place). +/// @param ls_cell LevelSetCell providing Bernstein coefficients. +/// @param level_set_id Which level set. +/// @param zero_tol Tolerance for all-zero. +/// @param sign_tol Tolerance for all-positive / all-negative. +template +void classify_leaf_faces(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, T sign_tol); + /// Evaluate the level set on every current AdaptCell vertex and update the /// sign/zero masks for that level set id. template diff --git a/cpp/src/cell_topology.h b/cpp/src/cell_topology.h index cbccbfa..8dcf41a 100644 --- a/cpp/src/cell_topology.h +++ b/cpp/src/cell_topology.h @@ -258,4 +258,50 @@ inline std::span tet_face(int face_id) return std::span(tetrahedron_faces[static_cast(face_id)]); } +/// Get the cell type of a specific face of a cell. +inline type face_type(type cell_type, int face_id) +{ + switch (cell_type) + { + case type::tetrahedron: + return type::triangle; + case type::hexahedron: + return type::quadrilateral; + case type::prism: + return (face_id < 2) ? type::triangle : type::quadrilateral; + case type::pyramid: + return (face_id == 0) ? type::quadrilateral : type::triangle; + default: + throw std::invalid_argument("Unknown cell type in face_type"); + } +} + +/// Get the vertex indices for face `face_id` of a 3D cell. +/// The returned span has `face_sizes(cell_type)[face_id]` entries. +inline std::span face_vertices(type cell_type, int face_id) +{ + const int fsize = face_sizes(cell_type)[static_cast(face_id)]; + switch (cell_type) + { + case type::tetrahedron: + return std::span( + tetrahedron_faces[static_cast(face_id)].data(), + static_cast(fsize)); + case type::hexahedron: + return std::span( + hexahedron_faces[static_cast(face_id)].data(), + static_cast(fsize)); + case type::prism: + return std::span( + prism_faces[static_cast(face_id)].data(), + static_cast(fsize)); + case type::pyramid: + return std::span( + pyramid_faces[static_cast(face_id)].data(), + static_cast(fsize)); + default: + throw std::invalid_argument("Unknown cell type in face_vertices"); + } +} + } // namespace cutcells::cell diff --git a/cpp/src/cut_mesh.h b/cpp/src/cut_mesh.h index 913fa1a..4fac90c 100644 --- a/cpp/src/cut_mesh.h +++ b/cpp/src/cut_mesh.h @@ -40,16 +40,16 @@ namespace cutcells::mesh struct CutMesh { /// Geometric Dimension of Cell - int _gdim; + int _gdim = 0; /// Topological Dimension of Cell - int _tdim; + int _tdim = 0; /// Number of cells - int _num_cells; + int _num_cells = 0; /// Number of vertices - int _num_vertices; + int _num_vertices = 0; /// Coordinates of vertices of cut cell std::vector _vertex_coords; diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index b580007..e356a28 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -5,8 +5,11 @@ #include "ho_cut_mesh.h" #include "cell_certification.h" +#include "edge_certification.h" #include +#include +#include #include #include @@ -49,6 +52,63 @@ void gather_vertex_ls_values(const MeshView& mesh, } } +template +T bernstein_cell_sign_tol(std::span coeffs) +{ + T max_abs = 0; + for (const T c : coeffs) + max_abs = std::max(max_abs, std::fabs(c)); + + return std::max(T(64) * std::numeric_limits::epsilon() + * std::max(T(1), max_abs), + T(1e-14)); +} + +template +cell::domain classify_cell_domain_from_bernstein(std::span coeffs) +{ + if (coeffs.empty()) + return cell::domain::unset; + + const T sign_tol = bernstein_cell_sign_tol(coeffs); + if (bernstein_all_positive(coeffs, sign_tol)) + return cell::domain::outside; + if (bernstein_all_negative(coeffs, sign_tol)) + return cell::domain::inside; + return cell::domain::intersected; +} + +template +cell::domain classify_cell_domain_fast(const MeshView& mesh, + const LevelSetFunction& ls, + I cell_id, int nv, + bool use_bernstein_classification, + std::vector& vertex_ls_values, + LevelSetCell* intersected_ls_cell) +{ + if (use_bernstein_classification) + { + LevelSetCell ls_cell = make_cell_level_set(ls, cell_id); + const cell::domain dom = classify_cell_domain_from_bernstein( + std::span(ls_cell.bernstein_coeffs.data(), + ls_cell.bernstein_coeffs.size())); + if (dom != cell::domain::unset) + { + if (dom == cell::domain::intersected + && intersected_ls_cell != nullptr) + { + *intersected_ls_cell = std::move(ls_cell); + } + return dom; + } + } + + gather_vertex_ls_values(mesh, ls, cell_id, nv, vertex_ls_values); + return cell::classify_cell_domain( + std::span(vertex_ls_values.data(), + static_cast(nv))); +} + } // anonymous namespace // ===================================================================== @@ -80,18 +140,20 @@ cut(const MeshView& mesh, const LevelSetFunction& ls) hc.tdim = cell::get_tdim(mesh.cell_type(I(0))); hc.ls_offsets.push_back(0); + const bool use_bernstein_classification = + ls.type == LevelSetType::Polynomial + && ls.has_mesh_data() + && ls.has_dof_values(); std::vector ls_vertex_vals; for (I ci = 0; ci < ncells; ++ci) { const cell::type ctype = mesh.cell_type(ci); const int nv = cell::get_num_vertices(ctype); - - gather_vertex_ls_values(mesh, ls, ci, nv, ls_vertex_vals); - - const cell::domain dom = cell::classify_cell_domain( - std::span(ls_vertex_vals.data(), - static_cast(nv))); + LevelSetCell ls_cell; + const cell::domain dom = classify_cell_domain_fast( + mesh, ls, ci, nv, use_bernstein_classification, ls_vertex_vals, + &ls_cell); bg.cell_domains[static_cast(ci)] = dom; if (dom != cell::domain::intersected) @@ -101,13 +163,13 @@ cut(const MeshView& mesh, const LevelSetFunction& ls) bg.cell_to_cut_index[static_cast(ci)] = cut_idx; // LevelSetCell - hc.level_set_cells.push_back(make_cell_level_set(ls, ci)); + hc.level_set_cells.push_back(std::move(ls_cell)); hc.ls_offsets.push_back( static_cast(hc.level_set_cells.size())); // AdaptCell AdaptCell ac = make_adapt_cell(mesh, ci); - build_edges(ac); + certify_refine_and_process_ready_cells( ac, hc.level_set_cells.back(), /*level_set_id=*/0, /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); @@ -159,6 +221,16 @@ cut(const MeshView& mesh, hc.tdim = cell::get_tdim(mesh.cell_type(I(0))); hc.ls_offsets.push_back(0); + std::vector use_bernstein_classification( + static_cast(nls), false); + for (int li = 0; li < nls; ++li) + { + const auto& ls = level_sets[static_cast(li)]; + use_bernstein_classification[static_cast(li)] = + ls.type == LevelSetType::Polynomial + && ls.has_mesh_data() + && ls.has_dof_values(); + } std::vector ls_vertex_vals; for (I ci = 0; ci < ncells; ++ci) @@ -169,18 +241,16 @@ cut(const MeshView& mesh, // Classify each level set individually; track if any intersects. bool any_intersected = false; std::vector intersected_ls_indices; - std::vector intersected_vertex_vals; + std::vector> intersected_ls_cells; intersected_ls_indices.reserve(static_cast(nls)); - intersected_vertex_vals.reserve( - static_cast(nls) * static_cast(nv)); + intersected_ls_cells.reserve(static_cast(nls)); for (int li = 0; li < nls; ++li) { - gather_vertex_ls_values(mesh, level_sets[static_cast(li)], - ci, nv, ls_vertex_vals); - - const cell::domain dom = cell::classify_cell_domain( - std::span(ls_vertex_vals.data(), - static_cast(nv))); + LevelSetCell ls_cell; + const cell::domain dom = classify_cell_domain_fast( + mesh, level_sets[static_cast(li)], ci, nv, + use_bernstein_classification[static_cast(li)], + ls_vertex_vals, &ls_cell); bg.cell_domains[static_cast( li * static_cast(ncells) + static_cast(ci))] = dom; @@ -189,9 +259,7 @@ cut(const MeshView& mesh, { any_intersected = true; intersected_ls_indices.push_back(li); - intersected_vertex_vals.insert(intersected_vertex_vals.end(), - ls_vertex_vals.begin(), - ls_vertex_vals.end()); + intersected_ls_cells.push_back(std::move(ls_cell)); } } @@ -203,18 +271,13 @@ cut(const MeshView& mesh, // Build AdaptCell once per cell. AdaptCell ac = make_adapt_cell(mesh, ci); - build_edges(ac); // Build LevelSetCell and fill vertex signs for each intersecting LS. std::uint64_t cell_active_mask = 0; for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) { const int li = intersected_ls_indices[k]; - const auto& ls_i = level_sets[static_cast(li)]; - const T* vals = intersected_vertex_vals.data() - + k * static_cast(nv); - - hc.level_set_cells.push_back(make_cell_level_set(ls_i, ci)); + hc.level_set_cells.push_back(std::move(intersected_ls_cells[k])); certify_refine_and_process_ready_cells( ac, hc.level_set_cells.back(), li, /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); diff --git a/cpp/src/ho_cut_mesh.h b/cpp/src/ho_cut_mesh.h index a466746..dfee0a9 100644 --- a/cpp/src/ho_cut_mesh.h +++ b/cpp/src/ho_cut_mesh.h @@ -132,8 +132,9 @@ struct HOMeshPart /// Build HOCutCells and BackgroundMeshData from a mesh and a single level set. /// -/// Iterates all background cells, classifies each by vertex-sign pattern, -/// and for intersected cells creates LevelSetCell + AdaptCell pairs. +/// Iterates all background cells, classifies each by a cheap Bernstein-cell +/// sign test when available (falling back to vertex signs otherwise), and +/// for intersected cells creates LevelSetCell + AdaptCell pairs. /// /// @param mesh Background mesh. Must have cell types. /// @param ls Level-set function. diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp new file mode 100644 index 0000000..2cc434f --- /dev/null +++ b/cpp/src/ho_mesh_part_output.cpp @@ -0,0 +1,706 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "ho_mesh_part_output.h" + +#include "mapping.h" +#include "quadrature_tables.h" +#include "reference_cell.h" +#include "cell_topology.h" +#include "triangulation.h" + +#include +#include +#include +#include +#include + +namespace cutcells::output +{ +namespace +{ + +struct SelectedEntity +{ + cell::type type = cell::type::point; + std::vector vertices; +}; + +inline bool is_simplex(cell::type cell_type) +{ + return cell_type == cell::type::interval + || cell_type == cell::type::triangle + || cell_type == cell::type::tetrahedron; +} + +inline cell::type simplex_type_for_dim(int dim) +{ + switch (dim) + { + case 1: + return cell::type::interval; + case 2: + return cell::type::triangle; + case 3: + return cell::type::tetrahedron; + default: + throw std::runtime_error("Unsupported simplex dimension"); + } +} + +template +std::vector parent_cell_vertex_coords_vtk(const MeshView& mesh, I cell_id) +{ + const auto ctype = mesh.cell_type(cell_id); + const int nv = cell::get_num_vertices(ctype); + std::vector coords(static_cast(nv * mesh.gdim), T(0)); + + for (int vtk_v = 0; vtk_v < nv; ++vtk_v) + { + const int local_v = mesh.vtk_vertex_order + ? vtk_v + : cell::vtk_to_basix_vertex(ctype, vtk_v); + const I node_id = mesh.cell_node(cell_id, static_cast(local_v)); + const T* x = mesh.node(node_id); + for (int d = 0; d < mesh.gdim; ++d) + coords[static_cast(vtk_v * mesh.gdim + d)] = x[d]; + } + + return coords; +} + +template +bool vertex_is_zero_for_level_set(const AdaptCell& adapt_cell, + int vertex_id, + int level_set_id) +{ + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + return (adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)] & bit) != 0; +} + +template +std::vector selected_entities(const HOMeshPart& part, + const AdaptCell& adapt_cell) +{ + if (part.expr.clauses.size() != 1 || part.expr.clauses.front().level_set_index != 0) + { + throw std::runtime_error( + "HOMeshPart output currently supports only one-clause single-level-set selections"); + } + + const auto relation = part.expr.clauses.front().relation; + std::vector entities; + + if (part.dim == adapt_cell.tdim) + { + if (relation == Relation::EqualTo) + throw std::runtime_error("HOMeshPart output: phi = 0 is not a volume selection"); + + if (adapt_cell.cell_cert_tag_num_level_sets <= 0) + throw std::runtime_error("HOMeshPart output: missing cell certification tags"); + + const auto target = (relation == Relation::LessThan) + ? CellCertTag::negative + : CellCertTag::positive; + + const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); + entities.reserve(static_cast(n_cells)); + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(/*level_set_id=*/0, c) != target) + continue; + + auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + SelectedEntity entity; + entity.type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; + entity.vertices.assign(verts.begin(), verts.end()); + entities.push_back(std::move(entity)); + } + return entities; + } + + if (relation != Relation::EqualTo) + { + throw std::runtime_error( + "HOMeshPart output: lower-dimensional direct export currently supports only phi = 0"); + } + + if (part.dim < 1 || part.dim >= adapt_cell.tdim) + throw std::runtime_error("HOMeshPart output: unsupported selection dimension"); + if (part.dim != adapt_cell.tdim - 1) + { + throw std::runtime_error( + "HOMeshPart output currently supports only codim-1 phi = 0 selections"); + } + + std::map, SelectedEntity> unique_entities; + const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); + for (int c = 0; c < n_cells; ++c) + { + const auto cell_type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; + auto cell_verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + + if (adapt_cell.tdim == 2) + { + for (const auto& edge : cell::edges(cell_type)) + { + SelectedEntity entity; + entity.type = cell::type::interval; + entity.vertices = { + static_cast(cell_verts[static_cast(edge[0])]), + static_cast(cell_verts[static_cast(edge[1])])}; + + bool all_zero = true; + for (int gv : entity.vertices) + { + if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) + { + all_zero = false; + break; + } + } + if (!all_zero) + continue; + + auto key = entity.vertices; + std::sort(key.begin(), key.end()); + unique_entities.try_emplace(std::move(key), std::move(entity)); + } + continue; + } + + const int n_faces = cell::num_faces(cell_type); + for (int fi = 0; fi < n_faces; ++fi) + { + auto local_face = cell::face_vertices(cell_type, fi); + SelectedEntity entity; + entity.type = cell::face_type(cell_type, fi); + entity.vertices.reserve(local_face.size()); + for (auto lv : local_face) + { + entity.vertices.push_back( + static_cast(cell_verts[static_cast(lv)])); + } + + bool all_zero = true; + for (int gv : entity.vertices) + { + if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) + { + all_zero = false; + break; + } + } + if (!all_zero) + continue; + + auto key = entity.vertices; + std::sort(key.begin(), key.end()); + unique_entities.try_emplace(std::move(key), std::move(entity)); + } + } + + entities.reserve(unique_entities.size()); + for (auto& [key, entity] : unique_entities) + entities.push_back(std::move(entity)); + return entities; +} + +template +std::vector entity_reference_coords(const AdaptCell& adapt_cell, + std::span entity_vertices) +{ + std::vector coords( + static_cast(entity_vertices.size() * adapt_cell.tdim), T(0)); + + for (std::size_t j = 0; j < entity_vertices.size(); ++j) + { + const int gv = entity_vertices[j]; + for (int d = 0; d < adapt_cell.tdim; ++d) + { + coords[static_cast(j * adapt_cell.tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * adapt_cell.tdim + d)]; + } + } + + return coords; +} + +template +void gather_subcell_vertices(std::span coords, + int coord_dim, + std::span vertex_ids, + std::vector& out) +{ + out.resize(static_cast(vertex_ids.size() * coord_dim)); + for (std::size_t j = 0; j < vertex_ids.size(); ++j) + { + const int local_v = vertex_ids[j]; + for (int d = 0; d < coord_dim; ++d) + { + out[static_cast(j * coord_dim + d)] = + coords[static_cast(local_v * coord_dim + d)]; + } + } +} + +template +void map_canonical_to_subcell_points(const T* canonical_points, + int num_points, + int simplex_dim, + const T* subcell_vertices, + int parent_tdim, + T* out_points) +{ + const T* v0 = subcell_vertices; + + for (int q = 0; q < num_points; ++q) + { + const T* X = canonical_points + q * simplex_dim; + T* x = out_points + q * parent_tdim; + + for (int d = 0; d < parent_tdim; ++d) + x[d] = v0[d]; + + for (int i = 1; i <= simplex_dim; ++i) + { + const T* vi = subcell_vertices + i * parent_tdim; + for (int d = 0; d < parent_tdim; ++d) + x[d] += X[i - 1] * (vi[d] - v0[d]); + } + } +} + +template +T simplex_physical_measure(const T* vertices, + int simplex_dim, + int gdim) +{ + T J[9] = {}; + const T* v0 = vertices; + + for (int col = 0; col < simplex_dim; ++col) + { + const T* vi = vertices + (col + 1) * gdim; + for (int row = 0; row < gdim; ++row) + J[col * gdim + row] = vi[row] - v0[row]; + } + + if (simplex_dim == gdim) + { + if (simplex_dim == 1) + return std::abs(J[0]); + if (simplex_dim == 2) + return std::abs(J[0] * J[3] - J[2] * J[1]); + + const T det = + J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + return std::abs(det); + } + + T G[9] = {}; + for (int i = 0; i < simplex_dim; ++i) + { + for (int j = 0; j < simplex_dim; ++j) + { + T sum = 0; + for (int k = 0; k < gdim; ++k) + sum += J[i * gdim + k] * J[j * gdim + k]; + G[i * simplex_dim + j] = sum; + } + } + + if (simplex_dim == 1) + return std::sqrt(G[0]); + if (simplex_dim == 2) + return std::sqrt(G[0] * G[3] - G[1] * G[2]); + + const T det = + G[0] * (G[4] * G[8] - G[7] * G[5]) + - G[3] * (G[1] * G[8] - G[7] * G[2]) + + G[6] * (G[1] * G[5] - G[4] * G[2]); + return std::sqrt(det); +} + +template +std::vector reorder_vertex_coords_to_vtk(cell::type cell_type, + std::span coords, + int coord_dim) +{ + const auto perm = cell::basix_to_vtk_vertex_permutation(cell_type); + return cell::permute_vertex_data(coords, coord_dim, perm); +} + +inline std::vector vtk_local_ids_from_basix(cell::type cell_type) +{ + const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); + return std::vector(perm.begin(), perm.end()); +} + +template +void append_mesh_entity(mesh::CutMesh& out, + std::span physical_coords, + int gdim, + cell::type cell_type, + int parent_cell_id, + bool triangulate, + bool input_is_basix) +{ + if (out._gdim == 0) + out._gdim = gdim; + if (out._tdim == 0) + out._tdim = cell::get_tdim(cell_type); + + std::vector vtk_coords; + std::span output_coords = physical_coords; + if (input_is_basix && !is_simplex(cell_type)) + { + vtk_coords = reorder_vertex_coords_to_vtk(cell_type, physical_coords, gdim); + output_coords = std::span(vtk_coords.data(), vtk_coords.size()); + } + + const int nv = static_cast(output_coords.size()) / gdim; + const int vertex_base = out._num_vertices; + out._vertex_coords.insert( + out._vertex_coords.end(), output_coords.begin(), output_coords.end()); + out._num_vertices += nv; + + if (triangulate && !is_simplex(cell_type)) + { + std::vector local_ids(static_cast(nv)); + std::iota(local_ids.begin(), local_ids.end(), 0); + + std::vector> simplices; + cell::triangulation(cell_type, local_ids.data(), simplices); + const auto simplex_type = simplex_type_for_dim(cell::get_tdim(cell_type)); + + for (const auto& simplex : simplices) + { + for (int lv : simplex) + out._connectivity.push_back(vertex_base + lv); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(simplex_type); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; + } + return; + } + + for (int lv = 0; lv < nv; ++lv) + out._connectivity.push_back(vertex_base + lv); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(cell_type); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; +} + +template +void append_simplex_quadrature(quadrature::QuadratureRules& rules, + cell::type simplex_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int order) +{ + const auto ref_rule = quadrature::get_reference_rule(simplex_type, order); + const int num_points = ref_rule._num_points; + const int simplex_dim = ref_rule._tdim; + + std::vector mapped_ref_points( + static_cast(num_points * parent_tdim), T(0)); + map_canonical_to_subcell_points( + ref_rule._points.data(), + num_points, + simplex_dim, + ref_vertices.data(), + parent_tdim, + mapped_ref_points.data()); + + rules._points.insert( + rules._points.end(), mapped_ref_points.begin(), mapped_ref_points.end()); + + const T measure = simplex_physical_measure( + physical_vertices.data(), simplex_dim, gdim); + for (int q = 0; q < num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); +} + +template +void append_entity_quadrature(quadrature::QuadratureRules& rules, + cell::type cell_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int parent_cell_id, + int order, + bool triangulate, + bool input_is_basix) +{ + if (rules._tdim == 0) + rules._tdim = parent_tdim; + if (rules._offset.empty()) + rules._offset.push_back(0); + + std::vector ref_vertices_vtk; + std::vector phys_vertices_vtk; + std::span ref_use = ref_vertices; + std::span phys_use = physical_vertices; + if (input_is_basix && !is_simplex(cell_type)) + { + ref_vertices_vtk = reorder_vertex_coords_to_vtk(cell_type, ref_vertices, parent_tdim); + phys_vertices_vtk = reorder_vertex_coords_to_vtk(cell_type, physical_vertices, gdim); + ref_use = std::span(ref_vertices_vtk.data(), ref_vertices_vtk.size()); + phys_use = std::span(phys_vertices_vtk.data(), phys_vertices_vtk.size()); + } + + const int entity_dim = cell::get_tdim(cell_type); + if (triangulate && !is_simplex(cell_type)) + { + const int nv = static_cast(ref_use.size()) / parent_tdim; + std::vector local_ids(static_cast(nv)); + std::iota(local_ids.begin(), local_ids.end(), 0); + + std::vector> simplices; + cell::triangulation(cell_type, local_ids.data(), simplices); + const auto simplex_type = simplex_type_for_dim(entity_dim); + + std::vector ref_simplex; + std::vector phys_simplex; + for (const auto& simplex : simplices) + { + gather_subcell_vertices( + ref_use, parent_tdim, + std::span(simplex.data(), simplex.size()), + ref_simplex); + gather_subcell_vertices( + phys_use, gdim, + std::span(simplex.data(), simplex.size()), + phys_simplex); + append_simplex_quadrature( + rules, + simplex_type, + std::span(ref_simplex.data(), ref_simplex.size()), + std::span(phys_simplex.data(), phys_simplex.size()), + parent_tdim, + gdim, + order); + } + } + else if (is_simplex(cell_type)) + { + append_simplex_quadrature( + rules, + cell_type, + ref_vertices, + physical_vertices, + parent_tdim, + gdim, + order); + } + else + { + const auto ref_rule = quadrature::get_reference_rule(cell_type, order); + const T measure = cell::affine_volume_factor( + cell_type, phys_use.data(), gdim); + rules._points.insert( + rules._points.end(), ref_rule._points.begin(), ref_rule._points.end()); + for (int q = 0; q < ref_rule._num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); + } + + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); +} + +template +void append_cut_entities(mesh::CutMesh& out, + quadrature::QuadratureRules* rules, + const HOMeshPart& part, + bool triangulate, + int quadrature_order) +{ + const auto& mesh = *part.bg->mesh; + for (std::int32_t cut_id : part.cut_cell_ids) + { + const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; + const auto entities = selected_entities(part, adapt_cell); + if (entities.empty()) + continue; + + const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); + + for (const auto& entity : entities) + { + const auto ref_coords = entity_reference_coords( + adapt_cell, std::span(entity.vertices.data(), entity.vertices.size())); + const auto phys_coords = cell::push_forward_affine_map( + adapt_cell.parent_cell_type, + parent_vertex_coords, + mesh.gdim, + std::span(ref_coords.data(), ref_coords.size())); + + // Volume entities (from adapt_cell) are in basix vertex ordering; + // face entities (from cell_topology.h face_vertices) are already + // in VTK cyclic ordering and must NOT be permuted again. + const bool entity_is_basix = + (cell::get_tdim(entity.type) == adapt_cell.tdim); + + append_mesh_entity( + out, + std::span(phys_coords.data(), phys_coords.size()), + mesh.gdim, + entity.type, + static_cast(parent_cell_id), + triangulate, + entity_is_basix); + + if (rules != nullptr) + { + append_entity_quadrature( + *rules, + entity.type, + std::span(ref_coords.data(), ref_coords.size()), + std::span(phys_coords.data(), phys_coords.size()), + mesh.tdim, + mesh.gdim, + static_cast(parent_cell_id), + quadrature_order, + triangulate || !is_simplex(entity.type), + entity_is_basix); + } + } + } +} + +template +void append_uncut_volume_cells(mesh::CutMesh& out, + quadrature::QuadratureRules* rules, + const HOMeshPart& part, + bool triangulate, + int quadrature_order) +{ + const auto& mesh = *part.bg->mesh; + if (part.dim != mesh.tdim) + return; + + for (I cell_id : part.uncut_cell_ids) + { + const auto ctype = mesh.cell_type(cell_id); + const auto phys_coords = parent_cell_vertex_coords_vtk(mesh, cell_id); + append_mesh_entity( + out, + std::span(phys_coords.data(), phys_coords.size()), + mesh.gdim, + ctype, + static_cast(cell_id), + triangulate, + /*input_is_basix=*/false); + + if (rules != nullptr) + { + const auto ref_coords = cell::canonical_vertices(ctype); + append_entity_quadrature( + *rules, + ctype, + std::span(ref_coords.data(), ref_coords.size()), + std::span(phys_coords.data(), phys_coords.size()), + mesh.tdim, + mesh.gdim, + static_cast(cell_id), + quadrature_order, + triangulate, + /*input_is_basix=*/false); + } + } +} + +} // namespace + +template +mesh::CutMesh visualization_mesh(const HOMeshPart& part, + bool include_uncut_cells, + bool triangulate) +{ + if (!part.cut_cells || !part.bg || !part.bg->mesh) + throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); + if (part.bg->num_level_sets != 1) + { + throw std::runtime_error( + "HOMeshPart output currently supports exactly one level set"); + } + + mesh::CutMesh out; + out._gdim = part.bg->mesh->gdim; + out._tdim = part.dim; + out._offset.push_back(0); + + append_cut_entities( + out, + static_cast*>(nullptr), + part, + triangulate, + /*quadrature_order=*/0); + if (include_uncut_cells) + { + append_uncut_volume_cells( + out, + static_cast*>(nullptr), + part, + triangulate, + /*quadrature_order=*/0); + } + + return out; +} + +template +quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, + int order, + bool include_uncut_cells, + bool triangulate) +{ + if (!part.cut_cells || !part.bg || !part.bg->mesh) + throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); + if (part.bg->num_level_sets != 1) + { + throw std::runtime_error( + "HOMeshPart output currently supports exactly one level set"); + } + + mesh::CutMesh unused_mesh; + quadrature::QuadratureRules rules; + rules._offset.push_back(0); + + append_cut_entities(unused_mesh, &rules, part, triangulate, order); + if (include_uncut_cells) + append_uncut_volume_cells(unused_mesh, &rules, part, triangulate, order); + + return rules; +} + +template mesh::CutMesh visualization_mesh( + const HOMeshPart&, bool, bool); +template mesh::CutMesh visualization_mesh( + const HOMeshPart&, bool, bool); +template mesh::CutMesh visualization_mesh( + const HOMeshPart&, bool, bool); +template mesh::CutMesh visualization_mesh( + const HOMeshPart&, bool, bool); + +template quadrature::QuadratureRules quadrature_rules( + const HOMeshPart&, int, bool, bool); +template quadrature::QuadratureRules quadrature_rules( + const HOMeshPart&, int, bool, bool); +template quadrature::QuadratureRules quadrature_rules( + const HOMeshPart&, int, bool, bool); +template quadrature::QuadratureRules quadrature_rules( + const HOMeshPart&, int, bool, bool); + +} // namespace cutcells::output diff --git a/cpp/src/ho_mesh_part_output.h b/cpp/src/ho_mesh_part_output.h new file mode 100644 index 0000000..3dc28ec --- /dev/null +++ b/cpp/src/ho_mesh_part_output.h @@ -0,0 +1,28 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "cut_mesh.h" +#include "ho_cut_mesh.h" +#include "quadrature.h" + +namespace cutcells::output +{ + +template +mesh::CutMesh visualization_mesh(const HOMeshPart& part, + bool include_uncut_cells, + bool triangulate); + +template +quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, + int order, + bool include_uncut_cells, + bool triangulate); + +} // namespace cutcells::output diff --git a/cpp/src/level_set_cell.cpp b/cpp/src/level_set_cell.cpp index a9c1e8e..807f889 100644 --- a/cpp/src/level_set_cell.cpp +++ b/cpp/src/level_set_cell.cpp @@ -7,15 +7,372 @@ #include #include +#include #include "bernstein.h" +#include "cell_topology.h" #include "mapping.h" +#include "reference_cell.h" namespace cutcells { namespace { +template +struct RefPointKey +{ + cell::type ctype = cell::type::point; + int degree = 0; + + bool operator==(const RefPointKey&) const = default; +}; + +template +struct RefPointKeyHash +{ + std::size_t operator()(const RefPointKey& key) const noexcept + { + std::size_t seed = std::hash{}(static_cast(key.ctype)); + seed ^= std::hash{}(key.degree) + 0x9e3779b97f4a7c15ULL + + (seed << 6) + (seed >> 2); + return seed; + } +}; + +template +struct FaceDef +{ + cell::type face_type = cell::type::point; + int nverts = 0; + std::array verts = {-1, -1, -1, -1}; +}; + +inline std::span> basix_faces(cell::type ctype) +{ + static constexpr std::array, 4> tetrahedron = {{ + {cell::type::triangle, 3, {1, 2, 3, -1}}, + {cell::type::triangle, 3, {0, 2, 3, -1}}, + {cell::type::triangle, 3, {0, 1, 3, -1}}, + {cell::type::triangle, 3, {0, 1, 2, -1}}, + }}; + static constexpr std::array, 6> hexahedron = {{ + {cell::type::quadrilateral, 4, {0, 1, 2, 3}}, + {cell::type::quadrilateral, 4, {4, 5, 6, 7}}, + {cell::type::quadrilateral, 4, {0, 1, 4, 5}}, + {cell::type::quadrilateral, 4, {0, 2, 4, 6}}, + {cell::type::quadrilateral, 4, {1, 3, 5, 7}}, + {cell::type::quadrilateral, 4, {2, 3, 6, 7}}, + }}; + static constexpr std::array, 5> prism = {{ + {cell::type::triangle, 3, {0, 1, 2, -1}}, + {cell::type::triangle, 3, {3, 4, 5, -1}}, + {cell::type::quadrilateral, 4, {0, 1, 3, 4}}, + {cell::type::quadrilateral, 4, {0, 2, 3, 5}}, + {cell::type::quadrilateral, 4, {1, 2, 4, 5}}, + }}; + static constexpr std::array, 5> pyramid = {{ + {cell::type::quadrilateral, 4, {0, 1, 2, 3}}, + {cell::type::triangle, 3, {0, 1, 4, -1}}, + {cell::type::triangle, 3, {1, 2, 4, -1}}, + {cell::type::triangle, 3, {2, 3, 4, -1}}, + {cell::type::triangle, 3, {0, 3, 4, -1}}, + }}; + + switch (ctype) + { + case cell::type::tetrahedron: return std::span(tetrahedron); + case cell::type::hexahedron: return std::span(hexahedron); + case cell::type::prism: return std::span(prism); + case cell::type::pyramid: return std::span(pyramid); + default: return {}; + } +} + +template +void append_point(std::vector& out, std::span x) +{ + out.insert(out.end(), x.begin(), x.end()); +} + +template +void append_affine_edge_point(std::vector& out, + std::span x0, + std::span x1, + T t) +{ + for (std::size_t d = 0; d < x0.size(); ++d) + out.push_back((T(1) - t) * x0[d] + t * x1[d]); +} + +template +void append_triangle_face_point(std::vector& out, + std::span x0, + std::span x1, + std::span x2, + T u, T v) +{ + const T w0 = T(1) - u - v; + for (std::size_t d = 0; d < x0.size(); ++d) + out.push_back(w0 * x0[d] + u * x1[d] + v * x2[d]); +} + +template +void append_quad_face_point(std::vector& out, + std::span x0, + std::span x1, + std::span x2, + std::span x3, + T u, T v) +{ + const T w0 = (T(1) - u) * (T(1) - v); + const T w1 = u * (T(1) - v); + const T w2 = (T(1) - u) * v; + const T w3 = u * v; + for (std::size_t d = 0; d < x0.size(); ++d) + out.push_back(w0 * x0[d] + w1 * x1[d] + w2 * x2[d] + w3 * x3[d]); +} + +template +void append_tetra_cell_point(std::vector& out, + std::span x0, + std::span x1, + std::span x2, + std::span x3, + T u, T v, T w) +{ + const T w0 = T(1) - u - v - w; + for (std::size_t d = 0; d < x0.size(); ++d) + out.push_back(w0 * x0[d] + u * x1[d] + v * x2[d] + w * x3[d]); +} + +template +void append_hex_cell_point(std::vector& out, + const std::vector& verts, int tdim, + T u, T v, T w) +{ + for (int d = 0; d < tdim; ++d) + { + out.push_back( + (T(1) - u) * (T(1) - v) * (T(1) - w) * verts[static_cast(d)] + + u * (T(1) - v) * (T(1) - w) * verts[static_cast(tdim + d)] + + (T(1) - u) * v * (T(1) - w) * verts[static_cast(2 * tdim + d)] + + u * v * (T(1) - w) * verts[static_cast(3 * tdim + d)] + + (T(1) - u) * (T(1) - v) * w * verts[static_cast(4 * tdim + d)] + + u * (T(1) - v) * w * verts[static_cast(5 * tdim + d)] + + (T(1) - u) * v * w * verts[static_cast(6 * tdim + d)] + + u * v * w * verts[static_cast(7 * tdim + d)]); + } +} + +template +std::vector build_reference_lagrange_points(cell::type ctype, int degree) +{ + const int tdim = cell::get_tdim(ctype); + const int nv = cell::get_num_vertices(ctype); + const std::vector vertices = cell::reference_vertices(ctype); + std::vector ref_points; + ref_points.reserve(static_cast( + bernstein::num_polynomials(ctype, degree) * tdim)); + + for (int v = 0; v < nv; ++v) + { + append_point( + ref_points, + std::span( + vertices.data() + static_cast(v * tdim), + static_cast(tdim))); + } + + if (degree == 1) + return ref_points; + + const auto edges = cell::edges(ctype); + for (const auto& edge : edges) + { + const T* x0 = vertices.data() + static_cast(edge[0] * tdim); + const T* x1 = vertices.data() + static_cast(edge[1] * tdim); + for (int k = 1; k < degree; ++k) + { + const T t = static_cast(k) / static_cast(degree); + append_affine_edge_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + t); + } + } + + if (tdim == 3) + { + const auto faces = basix_faces(ctype); + for (const auto& face : faces) + { + if (face.face_type == cell::type::triangle) + { + if (degree <= 2) + continue; + + const T* x0 = vertices.data() + static_cast(face.verts[0] * tdim); + const T* x1 = vertices.data() + static_cast(face.verts[1] * tdim); + const T* x2 = vertices.data() + static_cast(face.verts[2] * tdim); + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + append_triangle_face_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + std::span(x2, static_cast(tdim)), + u, v); + } + } + } + else if (face.face_type == cell::type::quadrilateral) + { + const T* x0 = vertices.data() + static_cast(face.verts[0] * tdim); + const T* x1 = vertices.data() + static_cast(face.verts[1] * tdim); + const T* x2 = vertices.data() + static_cast(face.verts[2] * tdim); + const T* x3 = vertices.data() + static_cast(face.verts[3] * tdim); + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + append_quad_face_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + std::span(x2, static_cast(tdim)), + std::span(x3, static_cast(tdim)), + u, v); + } + } + } + } + } + + switch (ctype) + { + case cell::type::triangle: + if (degree > 2) + { + const T* x0 = vertices.data(); + const T* x1 = vertices.data() + tdim; + const T* x2 = vertices.data() + 2 * tdim; + for (int j = 1; j <= degree - 2; ++j) + { + for (int i = 1; i <= degree - j - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + append_triangle_face_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + std::span(x2, static_cast(tdim)), + u, v); + } + } + } + break; + case cell::type::quadrilateral: + { + const T* x0 = vertices.data(); + const T* x1 = vertices.data() + tdim; + const T* x2 = vertices.data() + 2 * tdim; + const T* x3 = vertices.data() + 3 * tdim; + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + append_quad_face_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + std::span(x2, static_cast(tdim)), + std::span(x3, static_cast(tdim)), + u, v); + } + } + } + break; + case cell::type::tetrahedron: + if (degree > 3) + { + const T* x0 = vertices.data(); + const T* x1 = vertices.data() + tdim; + const T* x2 = vertices.data() + 2 * tdim; + const T* x3 = vertices.data() + 3 * tdim; + for (int k = 1; k <= degree - 3; ++k) + { + for (int j = 1; j <= degree - k - 2; ++j) + { + for (int i = 1; i <= degree - j - k - 1; ++i) + { + const T u = static_cast(i) / static_cast(degree); + const T v = static_cast(j) / static_cast(degree); + const T w = static_cast(k) / static_cast(degree); + append_tetra_cell_point( + ref_points, + std::span(x0, static_cast(tdim)), + std::span(x1, static_cast(tdim)), + std::span(x2, static_cast(tdim)), + std::span(x3, static_cast(tdim)), + u, v, w); + } + } + } + } + break; + case cell::type::hexahedron: + for (int k = 1; k < degree; ++k) + { + for (int j = 1; j < degree; ++j) + { + for (int i = 1; i < degree; ++i) + { + append_hex_cell_point( + ref_points, vertices, tdim, + static_cast(i) / static_cast(degree), + static_cast(j) / static_cast(degree), + static_cast(k) / static_cast(degree)); + } + } + } + break; + default: + break; + } + + return ref_points; +} + +template +const std::vector& cached_reference_lagrange_points(cell::type ctype, int degree) +{ + thread_local std::unordered_map, std::vector, RefPointKeyHash> cache; + + RefPointKey key; + key.ctype = ctype; + key.degree = degree; + + auto it = cache.find(key); + if (it != cache.end()) + return it->second; + + auto points = build_reference_lagrange_points(ctype, degree); + auto [inserted_it, inserted] = cache.emplace(key, std::move(points)); + (void)inserted; + return inserted_it->second; +} + /// Determine the cell::type for a cell in the level-set mesh data. /// Uses cell_types array if available, otherwise infers from tdim and /// number of DOFs per cell (assuming equispaced Lagrange). @@ -108,37 +465,18 @@ make_cell_level_set(const LevelSetFunction& global_ls, cell_ls.nodal_values[static_cast(i)] = global_ls.dof_values[static_cast(cell_dofs[static_cast(i)])]; cell_ls.nodal_order = degree; - - // Extract physical coordinates of all DOFs on this cell - std::vector dof_phys(static_cast(ndofs * gdim)); - for (int i = 0; i < ndofs; ++i) + const auto& dof_ref = cached_reference_lagrange_points(ctype, degree); + if (static_cast(dof_ref.size()) != ndofs * cell_ls.tdim) { - const T* x = md.dof_coordinate(cell_dofs[static_cast(i)]); - for (int d = 0; d < gdim; ++d) - dof_phys[static_cast(i * gdim + d)] = x[d]; + throw std::runtime_error( + "make_cell_level_set: cached reference lattice size mismatch"); } - // Extract physical vertex coordinates (first n_vertices DOFs in Basix ordering) - const int nv = cell::get_num_vertices(ctype); - std::vector vertex_coords(static_cast(nv * gdim)); - for (int v = 0; v < nv; ++v) - for (int d = 0; d < gdim; ++d) - vertex_coords[static_cast(v * gdim + d)] - = dof_phys[static_cast(v * gdim + d)]; - - // Pull back all DOF coordinates from physical to reference space. - // Note: pull_back_affine is supported for volume cells (gdim == tdim). - assert(gdim == md.tdim && "pull_back_affine requires gdim == tdim"); - std::vector dof_ref(static_cast(ndofs * gdim)); - cell::pull_back_affine(ctype, vertex_coords, gdim, - std::span(dof_phys), - std::span(dof_ref)); - // Convert Lagrange nodal values to Bernstein coefficients cell_ls.bernstein_order = degree; bernstein::lagrange_to_bernstein( ctype, degree, - std::span(dof_ref), + std::span(dof_ref.data(), dof_ref.size()), std::span(cell_ls.nodal_values), cell_ls.bernstein_coeffs); } diff --git a/cpp/src/mapping.cpp b/cpp/src/mapping.cpp index 531d6d1..80d6fe8 100644 --- a/cpp/src/mapping.cpp +++ b/cpp/src/mapping.cpp @@ -195,6 +195,45 @@ void push_forward_affine(type parent_type, } } +template +std::vector push_forward_affine_map(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span X_ref) +{ + const int tdim = get_tdim(parent_type); + if (tdim <= 0 || tdim > gdim) + throw std::invalid_argument("push_forward_affine_map: invalid tdim/gdim combination"); + if (X_ref.size() % static_cast(tdim) != 0) + { + throw std::invalid_argument( + "push_forward_affine_map: X_ref size must be a multiple of the parent tdim"); + } + + const int n = static_cast(X_ref.size()) / tdim; + std::vector x_phys(static_cast(n * gdim), T(0)); + const T* x0 = parent_vertex_coords.data(); + const auto cols = jacobian_col_indices(parent_type); + + for (int i = 0; i < n; ++i) + { + const T* X = X_ref.data() + i * tdim; + T* x = x_phys.data() + i * gdim; + + for (int d = 0; d < gdim; ++d) + x[d] = x0[d]; + + for (int col = 0; col < tdim; ++col) + { + const T* vi = parent_vertex_coords.data() + cols[col] * gdim; + for (int d = 0; d < gdim; ++d) + x[d] += X[col] * (vi[d] - x0[d]); + } + } + + return x_phys; +} + template void pull_back_affine(type parent_type, const std::vector& parent_vertex_coords, @@ -349,6 +388,10 @@ template void push_forward_affine(type, const std::vector&, int, std::span, std::span); template void push_forward_affine(type, const std::vector&, int, std::span, std::span); +template std::vector push_forward_affine_map( + type, const std::vector&, int, std::span); +template std::vector push_forward_affine_map( + type, const std::vector&, int, std::span); template void pull_back_affine(type, const std::vector&, int, std::span, std::span); diff --git a/cpp/src/mapping.h b/cpp/src/mapping.h index 466a720..d5f2d34 100644 --- a/cpp/src/mapping.h +++ b/cpp/src/mapping.h @@ -120,6 +120,23 @@ void push_forward_affine(type parent_type, std::span X_ref, std::span x_phys); +/// @brief Affine push-forward for parent-reference points when tdim <= gdim. +/// +/// Maps points in the parent reference space (dimension = get_tdim(parent_type)) +/// to physical space (dimension = gdim). This supports embedded cells, e.g. +/// triangles in 3D or intervals in 2D/3D. +/// +/// @param parent_type cell type of the parent element +/// @param parent_vertex_coords flat physical vertex coords in VTK ordering +/// @param gdim physical embedding dimension +/// @param X_ref flat input: n * tdim reference coordinates +/// @returns flat output: n * gdim physical coordinates +template +std::vector push_forward_affine_map(type parent_type, + const std::vector& parent_vertex_coords, + int gdim, + std::span X_ref); + /// @brief Affine pullback for a batch of n physical points. /// /// Maps x_phys (flat, n*gdim) from physical space to parent reference space diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp index d28a03e..cb03aaa 100644 --- a/cpp/src/refine_cell.cpp +++ b/cpp/src/refine_cell.cpp @@ -616,7 +616,16 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, { const int a = verts[0]; const int b = verts[1]; - const int e = edge_lookup.at({std::min(a, b), std::max(a, b)}); + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto it = edge_lookup.find(key); + if (it == edge_lookup.end()) + { + throw std::runtime_error( + "refine_red_on_ambiguous_cells: missing interval edge key=(" + + std::to_string(key.first) + "," + std::to_string(key.second) + + ") for cell " + std::to_string(c)); + } + const int e = it->second; const int m = get_midpoint_vertex(e); const std::array, 2> children = {{{a, m}, {m, b}}}; for (const auto& child : children) @@ -636,8 +645,18 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, { const int a = verts[static_cast(ledges[static_cast(le)][0])]; const int b = verts[static_cast(ledges[static_cast(le)][1])]; + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto it = edge_lookup.find(key); + if (it == edge_lookup.end()) + { + throw std::runtime_error( + "refine_red_on_ambiguous_cells: missing triangle edge key=(" + + std::to_string(key.first) + "," + std::to_string(key.second) + + ") for cell " + std::to_string(c) + " local_edge=" + + std::to_string(le)); + } local_to_global[static_cast(3 + le)] = - get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + get_midpoint_vertex(it->second); } for (const auto& child : cell::triangle_subdivision_table) @@ -662,8 +681,18 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, { const int a = verts[static_cast(ledges[static_cast(le)][0])]; const int b = verts[static_cast(ledges[static_cast(le)][1])]; + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto it = edge_lookup.find(key); + if (it == edge_lookup.end()) + { + throw std::runtime_error( + "refine_red_on_ambiguous_cells: missing quadrilateral edge key=(" + + std::to_string(key.first) + "," + std::to_string(key.second) + + ") for cell " + std::to_string(c) + " local_edge=" + + std::to_string(le)); + } local_to_global[static_cast(4 + le)] = - get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + get_midpoint_vertex(it->second); } local_to_global[8] = append_cell_center_vertex(adapt_cell, verts); @@ -690,8 +719,18 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, { const int a = verts[static_cast(ledges[static_cast(le)][0])]; const int b = verts[static_cast(ledges[static_cast(le)][1])]; + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto it = edge_lookup.find(key); + if (it == edge_lookup.end()) + { + throw std::runtime_error( + "refine_red_on_ambiguous_cells: missing tetrahedron edge key=(" + + std::to_string(key.first) + "," + std::to_string(key.second) + + ") for cell " + std::to_string(c) + " local_edge=" + + std::to_string(le)); + } local_to_global[static_cast(4 + le)] = - get_midpoint_vertex(edge_lookup.at({std::min(a, b), std::max(a, b)})); + get_midpoint_vertex(it->second); } for (const auto& child : cell::tetrahedron_subdivision_table) diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 908bfbc..873720f 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -8,12 +8,19 @@ from pathlib import Path as _Path import ctypes as _ctypes import sys as _sys +import sysconfig as _sysconfig def _load_cpp_module(): build_dir = _Path(__file__).resolve().parents[1] / "build" candidates = sorted(build_dir.glob("_cutcellscpp*.so")) if candidates: + ext_suffix = _sysconfig.get_config_var("EXT_SUFFIX") + if ext_suffix: + matching = [candidate for candidate in candidates if candidate.name.endswith(ext_suffix)] + if matching: + candidates = matching + lib_candidates = [ build_dir / "cutcells_cpp" / "src" / "libcutcells.dylib", _Path(__file__).resolve().parents[2] @@ -105,6 +112,7 @@ def _load_cpp_module(): make_quadrature = _cutcellscpp.make_quadrature runtime_quadrature = _cutcellscpp.runtime_quadrature physical_points = _cutcellscpp.physical_points +write_vtk = _cutcellscpp.write_vtk write_level_set_vtu = _cutcellscpp.write_level_set_vtu ho_cut = _cutcellscpp.ho_cut HOCutResult = _cutcellscpp.HOCutResult diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 7cadaec..85d567c 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include "../../cpp/src/cell_types.h" #include "../../cpp/src/cut_cell.h" @@ -25,14 +27,19 @@ #include "../../cpp/src/bernstein.h" #include "../../cpp/src/mapping.h" #include "../../cpp/src/quadrature.h" +#include "../../cpp/src/quadrature_tables.h" #include "../../cpp/src/mesh_view.h" #include "../../cpp/src/level_set.h" #include "../../cpp/src/adapt_cell.h" +#include "../../cpp/src/cell_topology.h" #include "../../cpp/src/level_set_cell.h" +#include "../../cpp/src/reference_cell.h" +#include "../../cpp/src/triangulation.h" #include "../../cpp/src/edge_certification.h" #include "../../cpp/src/cell_certification.h" #include "../../cpp/src/refine_cell.h" #include "../../cpp/src/ho_cut_mesh.h" +#include "../../cpp/src/ho_mesh_part_output.h" namespace nb = nanobind; @@ -178,6 +185,524 @@ cutcells::MeshView make_mesh_view_from_numpy( return mesh; } +template +std::vector parent_cell_vertex_coords_vtk( + const cutcells::MeshView& mesh, int cell_id) +{ + const auto ctype = mesh.cell_type(cell_id); + const int nv = cutcells::cell::get_num_vertices(ctype); + std::vector coords(static_cast(nv * mesh.gdim), T(0)); + + for (int vtk_v = 0; vtk_v < nv; ++vtk_v) + { + const int local_v = mesh.vtk_vertex_order + ? vtk_v + : cutcells::cell::vtk_to_basix_vertex(ctype, vtk_v); + const int node_id = mesh.cell_node(cell_id, local_v); + const T* x = mesh.node(node_id); + for (int d = 0; d < mesh.gdim; ++d) + coords[static_cast(vtk_v * mesh.gdim + d)] = x[d]; + } + return coords; +} + +template +bool vertex_is_zero_for_level_set(const cutcells::AdaptCell& adapt_cell, + int vertex_id, + int level_set_id) +{ + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + return (adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)] & bit) != 0; +} + +struct SelectedEntity +{ + cutcells::cell::type type = cutcells::cell::type::point; + std::vector vertices; +}; + +template +std::vector part_selected_entities( + const cutcells::HOMeshPart& part, + const cutcells::AdaptCell& adapt_cell) +{ + if (part.expr.clauses.size() != 1 || part.expr.clauses.front().level_set_index != 0) + { + throw std::runtime_error( + "HOMeshPart direct straight output currently supports only " + "one-clause single-level-set selections"); + } + + const auto relation = part.expr.clauses.front().relation; + std::vector entities; + + if (part.dim == adapt_cell.tdim) + { + if (relation == cutcells::Relation::EqualTo) + throw std::runtime_error("HOMeshPart: phi = 0 is not a volume selection"); + + if (adapt_cell.cell_cert_tag_num_level_sets <= 0) + throw std::runtime_error("HOMeshPart: missing cell certification tags"); + + const auto target = + (relation == cutcells::Relation::LessThan) + ? cutcells::CellCertTag::negative + : cutcells::CellCertTag::positive; + + const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); + entities.reserve(static_cast(n_cells)); + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(/*level_set_id=*/0, c) == target) + { + auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + SelectedEntity entity; + entity.type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; + entity.vertices.assign(verts.begin(), verts.end()); + entities.push_back(std::move(entity)); + } + } + return entities; + } + + if (relation != cutcells::Relation::EqualTo) + { + throw std::runtime_error( + "HOMeshPart: lower-dimensional direct export currently supports only phi = 0"); + } + + if (part.dim < 1 || part.dim >= adapt_cell.tdim) + { + throw std::runtime_error("HOMeshPart: unsupported selection dimension"); + } + + if (part.dim != adapt_cell.tdim - 1) + { + throw std::runtime_error( + "HOMeshPart direct straight output currently supports only codim-1 phi = 0 selections"); + } + + std::map, SelectedEntity> unique_entities; + const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); + for (int c = 0; c < n_cells; ++c) + { + const auto cell_type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; + auto cell_verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + + if (adapt_cell.tdim == 2) + { + for (const auto& edge : cutcells::cell::edges(cell_type)) + { + SelectedEntity entity; + entity.type = cutcells::cell::type::interval; + entity.vertices = { + static_cast(cell_verts[static_cast(edge[0])]), + static_cast(cell_verts[static_cast(edge[1])])}; + + bool all_zero = true; + for (int gv : entity.vertices) + { + if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) + { + all_zero = false; + break; + } + } + if (!all_zero) + continue; + + auto key = entity.vertices; + std::sort(key.begin(), key.end()); + unique_entities.try_emplace(std::move(key), std::move(entity)); + } + continue; + } + + const int n_faces = cutcells::cell::num_faces(cell_type); + for (int fi = 0; fi < n_faces; ++fi) + { + auto local_face = cutcells::cell::face_vertices(cell_type, fi); + SelectedEntity entity; + entity.type = cutcells::cell::face_type(cell_type, fi); + entity.vertices.reserve(local_face.size()); + for (auto lv : local_face) + { + entity.vertices.push_back( + static_cast(cell_verts[static_cast(lv)])); + } + + bool all_zero = true; + for (int gv : entity.vertices) + { + if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) + { + all_zero = false; + break; + } + } + if (!all_zero) + continue; + + auto key = entity.vertices; + std::sort(key.begin(), key.end()); + unique_entities.try_emplace(std::move(key), std::move(entity)); + } + } + + entities.reserve(unique_entities.size()); + for (auto& [key, entity] : unique_entities) + entities.push_back(std::move(entity)); + return entities; +} + +inline bool cell_type_is_simplex(cutcells::cell::type cell_type) +{ + using cutcells::cell::type; + return cell_type == type::interval + || cell_type == type::triangle + || cell_type == type::tetrahedron; +} + +inline cutcells::cell::type simplex_type_for_dim(int dim) +{ + using cutcells::cell::type; + switch (dim) + { + case 1: + return type::interval; + case 2: + return type::triangle; + case 3: + return type::tetrahedron; + default: + throw std::runtime_error("Unsupported simplex dimension"); + } +} + +template +std::vector entity_reference_coords(const cutcells::AdaptCell& adapt_cell, + std::span entity_vertices) +{ + std::vector coords( + static_cast(entity_vertices.size() * adapt_cell.tdim), T(0)); + + for (std::size_t j = 0; j < entity_vertices.size(); ++j) + { + const int gv = entity_vertices[j]; + for (int d = 0; d < adapt_cell.tdim; ++d) + { + coords[static_cast(j * adapt_cell.tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * adapt_cell.tdim + d)]; + } + } + + return coords; +} + +template +std::vector push_forward_parent_reference_coords( + cutcells::cell::type parent_cell_type, + const std::vector& parent_vertex_coords_vtk, + int parent_tdim, + std::span reference_coords) +{ + std::vector physical_coords(reference_coords.size(), T(0)); + cutcells::cell::push_forward_affine( + parent_cell_type, + parent_vertex_coords_vtk, + parent_tdim, + reference_coords, + std::span(physical_coords.data(), physical_coords.size())); + return physical_coords; +} + +template +void gather_subcell_vertices(std::span coords, + int coord_dim, + std::span vertex_ids, + std::vector& out) +{ + out.resize(static_cast(vertex_ids.size() * coord_dim)); + for (std::size_t j = 0; j < vertex_ids.size(); ++j) + { + const int local_v = vertex_ids[j]; + for (int d = 0; d < coord_dim; ++d) + { + out[static_cast(j * coord_dim + d)] = + coords[static_cast(local_v * coord_dim + d)]; + } + } +} + +template +void map_canonical_to_subcell_points(const T* canonical_points, + int num_points, + int simplex_dim, + const T* subcell_vertices, + int parent_tdim, + T* out_points) +{ + const T* v0 = subcell_vertices; + + for (int q = 0; q < num_points; ++q) + { + const T* X = canonical_points + q * simplex_dim; + T* x = out_points + q * parent_tdim; + + for (int d = 0; d < parent_tdim; ++d) + x[d] = v0[d]; + + for (int i = 1; i <= simplex_dim; ++i) + { + const T* vi = subcell_vertices + i * parent_tdim; + for (int d = 0; d < parent_tdim; ++d) + x[d] += X[i - 1] * (vi[d] - v0[d]); + } + } +} + +template +T simplex_physical_measure(const T* vertices, + int simplex_dim, + int gdim) +{ + T J[9] = {}; + const T* v0 = vertices; + + for (int col = 0; col < simplex_dim; ++col) + { + const T* vi = vertices + (col + 1) * gdim; + for (int row = 0; row < gdim; ++row) + J[col * gdim + row] = vi[row] - v0[row]; + } + + if (simplex_dim == gdim) + { + if (simplex_dim == 1) + return std::abs(J[0]); + if (simplex_dim == 2) + return std::abs(J[0] * J[3] - J[2] * J[1]); + + const T det = + J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + return std::abs(det); + } + + T G[9] = {}; + for (int i = 0; i < simplex_dim; ++i) + { + for (int j = 0; j < simplex_dim; ++j) + { + T sum = 0; + for (int k = 0; k < gdim; ++k) + sum += J[i * gdim + k] * J[j * gdim + k]; + G[i * simplex_dim + j] = sum; + } + } + + if (simplex_dim == 1) + return std::sqrt(G[0]); + if (simplex_dim == 2) + return std::sqrt(G[0] * G[3] - G[1] * G[2]); + + const T det = + G[0] * (G[4] * G[8] - G[7] * G[5]) + - G[3] * (G[1] * G[8] - G[7] * G[2]) + + G[6] * (G[1] * G[5] - G[4] * G[2]); + return std::sqrt(det); +} + +template +void append_mesh_entity(cutcells::mesh::CutMesh& out, + std::span physical_coords, + int gdim, + cutcells::cell::type cell_type, + int parent_cell_id, + bool triangulate) +{ + if (out._gdim == 0) + out._gdim = gdim; + if (out._tdim == 0) + out._tdim = cutcells::cell::get_tdim(cell_type); + + const int nv = static_cast(physical_coords.size()) / gdim; + const int vertex_base = out._num_vertices; + out._vertex_coords.insert( + out._vertex_coords.end(), physical_coords.begin(), physical_coords.end()); + out._num_vertices += nv; + + if (triangulate && !cell_type_is_simplex(cell_type)) + { + std::vector local_ids(static_cast(nv)); + std::iota(local_ids.begin(), local_ids.end(), 0); + + std::vector> simplices; + cutcells::cell::triangulation(cell_type, local_ids.data(), simplices); + const auto simplex_type = + simplex_type_for_dim(cutcells::cell::get_tdim(cell_type)); + + for (const auto& simplex : simplices) + { + for (int lv : simplex) + out._connectivity.push_back(vertex_base + lv); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(simplex_type); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; + } + return; + } + + for (int lv = 0; lv < nv; ++lv) + out._connectivity.push_back(vertex_base + lv); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(cell_type); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; +} + +template +void append_simplex_quadrature(cutcells::quadrature::QuadratureRules& rules, + cutcells::cell::type simplex_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int order) +{ + const auto ref_rule = cutcells::quadrature::get_reference_rule(simplex_type, order); + const int num_points = ref_rule._num_points; + const int simplex_dim = ref_rule._tdim; + + std::vector mapped_ref_points(static_cast(num_points * parent_tdim), T(0)); + map_canonical_to_subcell_points( + ref_rule._points.data(), + num_points, + simplex_dim, + ref_vertices.data(), + parent_tdim, + mapped_ref_points.data()); + + rules._points.insert( + rules._points.end(), mapped_ref_points.begin(), mapped_ref_points.end()); + + const T measure = simplex_physical_measure( + physical_vertices.data(), simplex_dim, gdim); + for (int q = 0; q < num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); +} + +template +void append_entity_quadrature(cutcells::quadrature::QuadratureRules& rules, + cutcells::cell::type cell_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int parent_cell_id, + int order, + bool triangulate) +{ + if (rules._tdim == 0) + rules._tdim = parent_tdim; + if (rules._offset.empty()) + rules._offset.push_back(0); + + const int cell_dim = cutcells::cell::get_tdim(cell_type); + if (triangulate && !cell_type_is_simplex(cell_type)) + { + const int nv = static_cast(ref_vertices.size()) / parent_tdim; + std::vector local_ids(static_cast(nv)); + std::iota(local_ids.begin(), local_ids.end(), 0); + + std::vector> simplices; + cutcells::cell::triangulation(cell_type, local_ids.data(), simplices); + const auto simplex_type = simplex_type_for_dim(cell_dim); + + std::vector ref_simplex; + std::vector phys_simplex; + for (const auto& simplex : simplices) + { + gather_subcell_vertices( + ref_vertices, + parent_tdim, + std::span(simplex.data(), simplex.size()), + ref_simplex); + gather_subcell_vertices( + physical_vertices, + gdim, + std::span(simplex.data(), simplex.size()), + phys_simplex); + append_simplex_quadrature( + rules, + simplex_type, + std::span(ref_simplex.data(), ref_simplex.size()), + std::span(phys_simplex.data(), phys_simplex.size()), + parent_tdim, + gdim, + order); + } + } + else if (cell_type_is_simplex(cell_type)) + { + append_simplex_quadrature( + rules, + cell_type, + ref_vertices, + physical_vertices, + parent_tdim, + gdim, + order); + } + else + { + const auto ref_rule = cutcells::quadrature::get_reference_rule(cell_type, order); + const T measure = cutcells::cell::affine_volume_factor( + cell_type, physical_vertices.data(), gdim); + rules._points.insert( + rules._points.end(), ref_rule._points.begin(), ref_rule._points.end()); + for (int q = 0; q < ref_rule._num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); + } + + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); +} + +inline bool part_mode_is_cut_only(std::string_view mode) +{ + if (mode == "cut_only") + return true; + if (mode == "full") + return false; + throw std::runtime_error("HOMeshPart mode must be 'cut_only' or 'full'"); +} + +template +cutcells::mesh::CutMesh part_visualization_mesh( + const cutcells::HOMeshPart& part, + std::string_view mode, + bool triangulate) +{ + const bool cut_only = part_mode_is_cut_only(mode); + return cutcells::output::visualization_mesh( + part, /*include_uncut_cells=*/!cut_only, triangulate); +} + +template +cutcells::quadrature::QuadratureRules part_quadrature( + const cutcells::HOMeshPart& part, + int order, + std::string_view mode, + bool triangulate) +{ + const bool cut_only = part_mode_is_cut_only(mode); + return cutcells::output::quadrature_rules( + part, order, /*include_uncut_cells=*/!cut_only, triangulate); +} + template void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) { @@ -1183,6 +1708,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) { HOCutT cut_cells; BGDataT bg; + std::shared_ptr level_set_owner; }; std::string result_name = "HOCutResult_" + type; @@ -1219,11 +1745,29 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::rv_policy::reference_internal, "Per-level-set domain classification, shape (num_level_sets, num_cells).") .def("__getitem__", - [](const HOCutResult& self, std::string_view expr_str) { - return cutcells::select_part(self.cut_cells, self.bg, expr_str); + [](const HOCutResult& self, const std::string& expr_str) { + return cutcells::select_part( + self.cut_cells, + self.bg, + std::string_view(expr_str)); }, nb::arg("expr"), - "Select a mesh part via expression, e.g. result[\"phi < 0\"]."); + nb::keep_alive<0, 1>(), + "Select a mesh part via expression, e.g. result[\"phi < 0\"].") + .def( + "adapt_cell", + [](const HOCutResult& self, int cut_cell_id) -> const cutcells::AdaptCell& + { + if (cut_cell_id < 0 + || cut_cell_id >= static_cast(self.cut_cells.adapt_cells.size())) + { + throw std::out_of_range("adapt_cell: cut_cell_id out of range"); + } + return self.cut_cells.adapt_cells[static_cast(cut_cell_id)]; + }, + nb::arg("cut_cell_id"), + nb::rv_policy::reference_internal, + "Return the AdaptCell for a cut-cell index."); // --- HOMeshPart --- std::string part_name = "HOMeshPart_" + type; @@ -1255,14 +1799,58 @@ void declare_ho_cut(nb::module_& m, const std::string& type) {self.uncut_cell_ids.size()}, nb::handle()); }, - nb::rv_policy::reference_internal); + nb::rv_policy::reference_internal) + .def( + "visualization_mesh", + [](const PartT& self, const std::string& mode, bool triangulate) { + nb::gil_scoped_release release; + return part_visualization_mesh(self, mode, triangulate); + }, + nb::arg("mode") = "full", + nb::arg("triangulate") = false, + "Return a straight visualization mesh for a one-level-set HOMeshPart.\n" + "This bridge currently supports only single-level-set selections.") + .def( + "quadrature", + [](const PartT& self, int order, const std::string& mode, bool triangulate) { + nb::gil_scoped_release release; + return part_quadrature(self, order, mode, triangulate); + }, + nb::arg("order") = 3, + nb::arg("mode") = "full", + nb::arg("triangulate") = false, + "Return straight quadrature rules for a one-level-set HOMeshPart.\n" + "This bridge currently supports only single-level-set selections.") + .def( + "write_vtu", + [](const PartT& self, + const std::string& filename, + const std::string& mode, + bool triangulate) { + nb::gil_scoped_release release; + auto vis = part_visualization_mesh(self, mode, triangulate); + std::vector coords( + vis._vertex_coords.begin(), vis._vertex_coords.end()); + io::write_vtk( + filename, + std::span(coords.data(), coords.size()), + std::span(vis._connectivity.data(), vis._connectivity.size()), + std::span(vis._offset.data(), vis._offset.size()), + std::span(vis._types.data(), vis._types.size()), + vis._gdim); + }, + nb::arg("filename"), + nb::arg("mode") = "full", + nb::arg("triangulate") = false, + "Write a straight visualization VTU file for a one-level-set HOMeshPart."); // --- ho_cut() factory --- m.def("ho_cut", [](const MeshViewT& mesh, const LevelSetT& ls) { nb::gil_scoped_release release; - auto [hc, bg] = cutcells::cut(mesh, ls); - return HOCutResult{std::move(hc), std::move(bg)}; + auto owned_ls = std::make_shared(ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_set"), "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" @@ -1271,8 +1859,31 @@ void declare_ho_cut(nb::module_& m, const std::string& type) m.def("ho_cut", [](const MeshViewT& mesh, const std::vector& level_sets) { nb::gil_scoped_release release; - auto [hc, bg] = cutcells::cut(mesh, level_sets); - return HOCutResult{std::move(hc), std::move(bg)}; + auto owned_ls = std::make_shared>(level_sets); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + return HOCutResult{std::move(hc), std::move(bg), owned_ls}; + }, + nb::arg("mesh"), nb::arg("level_sets"), + "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" + "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); + + m.def("cut", + [](const MeshViewT& mesh, const LevelSetT& ls) { + nb::gil_scoped_release release; + auto owned_ls = std::make_shared(ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + return HOCutResult{std::move(hc), std::move(bg), owned_ls}; + }, + nb::arg("mesh"), nb::arg("level_set"), + "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" + "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); + + m.def("cut", + [](const MeshViewT& mesh, const std::vector& level_sets) { + nb::gil_scoped_release release; + auto owned_ls = std::make_shared>(level_sets); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_sets"), "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" @@ -1465,6 +2076,15 @@ void declare_certification(nb::module_& m, const std::string& suffix) }, nb::arg("adapt_cell")); + m.def( + "build_faces", + [](AdaptCellT& adapt_cell) + { + nb::gil_scoped_release release; + cutcells::build_faces(adapt_cell); + }, + nb::arg("adapt_cell")); + m.def( "make_cell_level_set", [](const LevelSetT& ls, int cell_id) @@ -1602,6 +2222,21 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("zero_tol") = T(1e-12), nb::arg("sign_tol") = T(1e-12)); + m.def( + "classify_leaf_faces", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + T zero_tol, T sign_tol) + { + nb::gil_scoped_release release; + cutcells::classify_leaf_faces(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12)); + m.def( "process_ready_to_cut_cells", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, @@ -1745,4 +2380,35 @@ NB_MODULE(_cutcellscpp, m) nb::arg("connectivity"), nb::arg("offsets"), "Pack CSR connectivity/offsets to VTK cells layout [n0, v0..., n1, v1..., ...]."); + + m.def("write_vtk", + [](std::string filename, + const nb::ndarray, nb::c_contig>& vertex_coordinates, + const nb::ndarray, nb::c_contig>& connectivity, + const nb::ndarray, nb::c_contig>& offsets, + const nb::ndarray, nb::c_contig>& element_types, + int gdim) + { + std::vector types; + types.reserve(element_types.size()); + for (std::size_t i = 0; i < element_types.size(); ++i) + types.push_back(static_cast(element_types.data()[i])); + + nb::gil_scoped_release release; + io::write_vtk( + std::move(filename), + std::span(vertex_coordinates.data(), vertex_coordinates.size()), + std::span(connectivity.data(), connectivity.size()), + std::span(offsets.data(), offsets.size()), + std::span(types.data(), types.size()), + gdim); + }, + nb::arg("filename"), + nb::arg("vertex_coordinates"), + nb::arg("connectivity"), + nb::arg("offsets"), + nb::arg("element_types"), + nb::arg("gdim"), + "Write an unstructured VTK XML file using the existing C++ writer.\n" + "vertex_coordinates is a flat array of length num_points * gdim."); } diff --git a/python/demo/bench_cut_pipeline.py b/python/demo/bench_cut_pipeline.py deleted file mode 100644 index 72b4c97..0000000 --- a/python/demo/bench_cut_pipeline.py +++ /dev/null @@ -1,520 +0,0 @@ -""" -bench_cut_pipeline.py -===================== - -Pure-Python micro-benchmark for the CutCells cutting pipeline. - -Measures wall time for: - 1. cut_vtk_mesh – for triangle, quad, tetrahedron, hex meshes - 2. runtime_quadrature – phi<0 and phi=0 domains - 3. physical_points – mapping reference → physical space - -Run with: - conda run -n fenicsxv10 python bench_cut_pipeline.py [--n N] [--repeat R] [--order O] - -Columns: - kernel – what is being timed - mesh / domain – short description - n_cells – number of background cells - n_pts – number of quadrature points (quadrature kernels only) - total_ms – total wall time for repetitions [ms] - per_call_us – time per single call [µs] - Mcells/s – million cells per second (cut kernels) - Mpts/s – million quad pts per second (quadrature mapping) -""" - -import argparse -import time -from typing import Callable - -import numpy as np - -try: - import pyvista as pv - - HAS_PV = True -except ImportError: - HAS_PV = False - -import cutcells - -# --------------------------------------------------------------------------- -# Timing helper -# --------------------------------------------------------------------------- - - -def timeit(fn: Callable, repeat: int = 5) -> float: - """Return minimum wall time [seconds] over calls.""" - times = [] - for _ in range(repeat): - t0 = time.perf_counter() - fn() - times.append(time.perf_counter() - t0) - return min(times) - - -# --------------------------------------------------------------------------- -# Mesh creation helpers -# --------------------------------------------------------------------------- - - -def make_triangle_mesh_vtk(n: int): - """Return (points_flat, conn, offset, celltypes, ls_vals) for an n×n Delaunay - triangle mesh on [-1,1]^2, level-set = circle of radius 0.7.""" - if not HAS_PV: - raise RuntimeError("pyvista required for triangle/quad mesh generation") - x = np.linspace(-1.0, 1.0, n) - y = np.linspace(-1.0, 1.0, n) - xx, yy, zz = np.meshgrid(x, y, [0.0]) - pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] - grid = pv.UnstructuredGrid(pv.PolyData(pts).delaunay_2d()) - pts3 = np.asarray(grid.points, dtype=np.float64) - conn = np.asarray(grid.cell_connectivity, dtype=np.int32) - off = np.asarray(grid.offset, dtype=np.int32) - ctypes = np.asarray(grid.celltypes, dtype=np.int32) - ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2) - 0.7 - return pts3.ravel(), conn, off, ctypes, ls, grid.n_cells - - -def make_quad_mesh_vtk(n: int): - """Structured quad mesh on [-1,1]^2 (vectorised).""" - x = np.linspace(-1.0, 1.0, n) - y = np.linspace(-1.0, 1.0, n) - xx, yy = np.meshgrid(x, y) # both (n, n) - zz = np.zeros_like(xx) - pts3 = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1) # (n*n, 3) - - # Node index: node_id[i,j] = i*n + j - i = np.arange(n - 1) - j = np.arange(n - 1) - ii, jj = np.meshgrid(i, j, indexing="ij") # (n-1, n-1) - ii = ii.ravel() - jj = jj.ravel() - n0 = ii * n + jj - n1 = ii * n + (jj + 1) - n2 = (ii + 1) * n + (jj + 1) - n3 = (ii + 1) * n + jj - conn = np.stack([n0, n1, n2, n3], axis=1).astype(np.int32).ravel() - n_cells = (n - 1) * (n - 1) - off = np.arange(0, (n_cells + 1) * 4, 4, dtype=np.int32) - ct = np.full(n_cells, 9, dtype=np.int32) # VTK_QUAD = 9 - ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2) - 0.7 - return pts3.ravel(), conn, off, ct, ls, n_cells - - -def make_tet_mesh_vtk(n: int): - """Random tet mesh built via pyvista Delaunay 3D.""" - if not HAS_PV: - raise RuntimeError("pyvista required") - x = np.linspace(-1.0, 1.0, n) - y = np.linspace(-1.0, 1.0, n) - z = np.linspace(-1.0, 1.0, n) - xx, yy, zz = np.meshgrid(x, y, z) - pts = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] - grid = pv.UnstructuredGrid(pv.PolyData(pts).delaunay_3d()) - pts3 = np.asarray(grid.points, dtype=np.float64) - conn = np.asarray(grid.cell_connectivity, dtype=np.int32) - off = np.asarray(grid.offset, dtype=np.int32) - ctypes = np.asarray(grid.celltypes, dtype=np.int32) - ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2 + pts3[:, 2] ** 2) - 0.7 - return pts3.ravel(), conn, off, ctypes, ls, grid.n_cells - - -def make_hex_mesh_vtk(n: int): - """Structured hex mesh on [-1,1]^3 (vectorised).""" - x = np.linspace(-1.0, 1.0, n) - y = np.linspace(-1.0, 1.0, n) - z = np.linspace(-1.0, 1.0, n) - # node_id[i,j,k] = i*n*n + j*n + k (i=z-axis, j=y-axis, k=x-axis) - zz, yy, xx = np.meshgrid(z, y, x, indexing="ij") # each (n,n,n) - pts3 = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1) # (n^3, 3) - - i = np.arange(n - 1) - j = np.arange(n - 1) - k = np.arange(n - 1) - ii, jj, kk = np.meshgrid(i, j, k, indexing="ij") # each (n-1)^3 - ii = ii.ravel() - jj = jj.ravel() - kk = kk.ravel() - - def nid(a, b, c): - return (a * n + b) * n + c - - conn = ( - np.stack( - [ - nid(ii, jj, kk), - nid(ii, jj, kk + 1), - nid(ii, jj + 1, kk + 1), - nid(ii, jj + 1, kk), - nid(ii + 1, jj, kk), - nid(ii + 1, jj, kk + 1), - nid(ii + 1, jj + 1, kk + 1), - nid(ii + 1, jj + 1, kk), - ], - axis=1, - ) - .astype(np.int32) - .ravel() - ) - n_cells = (n - 1) ** 3 - off = np.arange(0, (n_cells + 1) * 8, 8, dtype=np.int32) - ct = np.full(n_cells, 12, dtype=np.int32) # VTK_HEXAHEDRON = 12 - ls = np.sqrt(pts3[:, 0] ** 2 + pts3[:, 1] ** 2 + pts3[:, 2] ** 2) - 0.7 - return pts3.ravel(), conn, off, ct, ls, n_cells - - -# --------------------------------------------------------------------------- -# Warm-up (avoids measuring JIT / module-load costs) -# --------------------------------------------------------------------------- - - -def warmup(): - pts = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], dtype=np.float64) - ls = np.array([-0.1, 0.1, 0.1], dtype=np.float64) - conn = np.array([0, 1, 2], dtype=np.int32) - off = np.array([0, 3], dtype=np.int32) - ct = np.array([5], dtype=np.int32) # VTK_TRIANGLE - for _ in range(10): - cutcells.cut_vtk_mesh(ls, pts, conn, off, ct, "phi<0", True) - - -# --------------------------------------------------------------------------- -# Benchmark runners -# --------------------------------------------------------------------------- - - -def bench_cut_vtk_mesh(pts_flat, conn, off, ct, ls, n_cells, label, repeat): - dt = timeit( - lambda: cutcells.cut_vtk_mesh(ls, pts_flat, conn, off, ct, "phi<0", True), - repeat=repeat, - ) - per_call_us = dt * 1e6 - mcells_s = n_cells / dt / 1e6 - return label, n_cells, None, per_call_us, mcells_s, None - - -def bench_runtime_quadrature( - pts_flat, conn, off, ct, ls, n_cells, domain, label, order, repeat -): - def _run(): - cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) - - # collect once to count points - q = cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) - n_pts = len(q.weights) - dt = timeit(_run, repeat=repeat) - per_call_us = dt * 1e6 - mcells_s = n_cells / dt / 1e6 - return label, n_cells, n_pts, per_call_us, mcells_s, None - - -def bench_physical_points( - pts_flat, conn, off, ct, ls, n_cells, domain, label, order, repeat -): - q = cutcells.runtime_quadrature(ls, pts_flat, conn, off, ct, domain, True, order) - n_pts = len(q.weights) - - def _run(): - cutcells.physical_points(q, pts_flat, conn, off, ct) - - dt = timeit(_run, repeat=repeat) - per_call_us = dt * 1e6 - mpts_s = n_pts / dt / 1e6 if n_pts > 0 else 0.0 - return label, n_cells, n_pts, per_call_us, None, mpts_s - - -# --------------------------------------------------------------------------- -# Reporting -# --------------------------------------------------------------------------- - -HDR = f"{'kernel':<36} {'n_cells':>8} {'n_pts':>8} {'us/call':>10} {'Mcells/s':>10} {'Mpts/s':>8}" -SEP = "-" * len(HDR) - - -def print_row(kernel, n_cells, n_pts, per_call_us, mcells_s, mpts_s): - npts_s = f"{n_pts:>8}" if n_pts is not None else f"{'':>8}" - mc_s = f"{mcells_s:>10.2f}" if mcells_s is not None else f"{'':>10}" - mp_s = f"{mpts_s:>8.2f}" if mpts_s is not None else f"{'':>8}" - print(f"{kernel:<36} {n_cells:>8} {npts_s} {per_call_us:>10.1f} {mc_s} {mp_s}") - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - - -def main(): - parser = argparse.ArgumentParser(description="CutCells pipeline benchmark") - parser.add_argument( - "--n2d", - type=int, - default=40, - help="Grid points per axis for 2-D meshes (default: 40)", - ) - parser.add_argument( - "--n3d", - type=int, - default=12, - help="Grid points per axis for 3-D meshes (default: 12)", - ) - parser.add_argument( - "--repeat", - type=int, - default=7, - help="Repetitions per kernel (min is reported, default: 7)", - ) - parser.add_argument( - "--order", type=int, default=3, help="Quadrature order (default: 3)" - ) - args = parser.parse_args() - - print( - f"\nCutCells pipeline benchmark (repeat={args.repeat}, quad order={args.order})" - ) - print( - f" 2-D mesh resolution: {args.n2d} pts/axis (~{2 * (args.n2d - 1) ** 2} tri cells, {(args.n2d - 1) ** 2} quad cells)" - ) - print( - f" 3-D mesh resolution: {args.n3d} pts/axis (~{(args.n3d - 1) ** 3} hex cells)" - ) - - print("\nBuilding meshes …") - t0 = time.perf_counter() - tri_data = make_triangle_mesh_vtk(args.n2d) - quad_data = make_quad_mesh_vtk(args.n2d) - hex_data = make_hex_mesh_vtk(args.n3d) - tet_data = make_tet_mesh_vtk(args.n3d) if HAS_PV else None - print(f" mesh generation: {(time.perf_counter() - t0) * 1e3:.1f} ms") - if tet_data is not None: - print( - f" tri {tri_data[5]:>6} cells | quad {quad_data[5]:>6} cells | " - f"tet {tet_data[5]:>6} cells | hex {hex_data[5]:>6} cells" - ) - else: - print( - f" tri {tri_data[5]:>6} cells | quad {quad_data[5]:>6} cells | " - f"hex {hex_data[5]:>6} cells" - ) - - print("\nWarming up …") - warmup() - - print() - print(HDR) - print(SEP) - - # ------------------------------------------------------------------ - # 2-D Triangle mesh - # ------------------------------------------------------------------ - pts, conn, off, ct, ls, nc = tri_data - for row in [ - bench_cut_vtk_mesh( - pts, conn, off, ct, ls, nc, "cut_vtk_mesh tri phi<0", args.repeat - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "runtime_quad tri phi<0", - args.order, - args.repeat, - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi=0", - "runtime_quad tri phi=0", - args.order, - args.repeat, - ), - bench_physical_points( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "physical_pts tri phi<0", - args.order, - args.repeat, - ), - bench_physical_points( - pts, - conn, - off, - ct, - ls, - nc, - "phi=0", - "physical_pts tri phi=0", - args.order, - args.repeat, - ), - ]: - print_row(*row) - - print() - - # ------------------------------------------------------------------ - # 2-D Quad mesh - # ------------------------------------------------------------------ - pts, conn, off, ct, ls, nc = quad_data - for row in [ - bench_cut_vtk_mesh( - pts, conn, off, ct, ls, nc, "cut_vtk_mesh quad phi<0", args.repeat - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "runtime_quad quad phi<0", - args.order, - args.repeat, - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi=0", - "runtime_quad quad phi=0", - args.order, - args.repeat, - ), - bench_physical_points( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "physical_pts quad phi<0", - args.order, - args.repeat, - ), - ]: - print_row(*row) - - print() - - # ------------------------------------------------------------------ - # 3-D Tetrahedron mesh - # ------------------------------------------------------------------ - if HAS_PV and tet_data is not None: - pts, conn, off, ct, ls, nc = tet_data - for row in [ - bench_cut_vtk_mesh( - pts, conn, off, ct, ls, nc, "cut_vtk_mesh tet phi<0", args.repeat - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "runtime_quad tet phi<0", - args.order, - args.repeat, - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi=0", - "runtime_quad tet phi=0", - args.order, - args.repeat, - ), - bench_physical_points( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "physical_pts tet phi<0", - args.order, - args.repeat, - ), - ]: - print_row(*row) - print() - - # ------------------------------------------------------------------ - # 3-D Hex mesh - # ------------------------------------------------------------------ - pts, conn, off, ct, ls, nc = hex_data - for row in [ - bench_cut_vtk_mesh( - pts, conn, off, ct, ls, nc, "cut_vtk_mesh hex phi<0", args.repeat - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "runtime_quad hex phi<0", - args.order, - args.repeat, - ), - bench_runtime_quadrature( - pts, - conn, - off, - ct, - ls, - nc, - "phi=0", - "runtime_quad hex phi=0", - args.order, - args.repeat, - ), - bench_physical_points( - pts, - conn, - off, - ct, - ls, - nc, - "phi<0", - "physical_pts hex phi<0", - args.order, - args.repeat, - ), - ]: - print_row(*row) - - print(SEP) - print("Times are minimum over all repetitions (best-of-N).") - print("Mcells/s = background cells processed per second.") - print("Mpts/s = quadrature points mapped per second.") - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_level_set_ho_curved_vtk.py b/python/demo/demo_level_set_ho_curved_vtk.py deleted file mode 100644 index 5b16540..0000000 --- a/python/demo/demo_level_set_ho_curved_vtk.py +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT - -import argparse -from pathlib import Path - -import numpy as np - -import cutcells - - -def _parse_degrees(arg: str) -> list[int]: - if arg == "all": - return [2, 3, 4] - return [int(arg)] - - -def _triangle_basix_nodes(degree: int) -> np.ndarray: - points: list[tuple[float, float, float]] = [ - (0.0, 0.0, 0.0), - (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - ] - - for k in range(1, degree): - t = k / degree - points.append((1.0 - t, t, 0.0)) - for k in range(1, degree): - t = k / degree - points.append((0.0, t, 0.0)) - for k in range(1, degree): - t = k / degree - points.append((t, 0.0, 0.0)) - - for j in range(1, degree - 1): - for i in range(1, degree - j): - points.append((i / degree, j / degree, 0.0)) - - return np.asarray(points, dtype=np.float64) - - -def _tetra_basix_nodes(degree: int) -> np.ndarray: - points: list[tuple[float, float, float]] = [ - (0.0, 0.0, 0.0), - (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (0.0, 0.0, 1.0), - ] - - for k in range(1, degree): - t = k / degree - points.append((0.0, 1.0 - t, t)) - for k in range(1, degree): - t = k / degree - points.append((1.0 - t, 0.0, t)) - for k in range(1, degree): - t = k / degree - points.append((1.0 - t, t, 0.0)) - for k in range(1, degree): - t = k / degree - points.append((0.0, 0.0, t)) - for k in range(1, degree): - t = k / degree - points.append((0.0, t, 0.0)) - for k in range(1, degree): - t = k / degree - points.append((t, 0.0, 0.0)) - - faces = ( - ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), - ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)), - ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)), - ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)), - ) - for x0, x1, x2 in faces: - for j in range(1, degree - 1): - for i in range(1, degree - j): - u = i / degree - v = j / degree - w0 = 1.0 - u - v - points.append( - ( - w0 * x0[0] + u * x1[0] + v * x2[0], - w0 * x0[1] + u * x1[1] + v * x2[1], - w0 * x0[2] + u * x1[2] + v * x2[2], - ) - ) - - for k in range(1, degree - 2): - for j in range(1, degree - k - 1): - for i in range(1, degree - j - k): - points.append((i / degree, j / degree, k / degree)) - - return np.asarray(points, dtype=np.float64) - - -def _curved_triangle_nodes(degree: int) -> np.ndarray: - X = _triangle_basix_nodes(degree).copy() - x = X[:, 0] - y = X[:, 1] - l0 = 1.0 - x - y - l1 = x - l2 = y - X[:, 0] = x + 0.18 * l0 * l2 - 0.10 * l1 * l2 - X[:, 1] = y + 0.24 * l0 * l1 + 0.08 * l1 * l2 - return X - - -def _curved_tetra_nodes(degree: int) -> np.ndarray: - X = _tetra_basix_nodes(degree).copy() - x = X[:, 0] - y = X[:, 1] - z = X[:, 2] - l0 = 1.0 - x - y - z - l1 = x - l2 = y - l3 = z - X[:, 0] = x + 0.10 * l0 * l2 - 0.05 * l1 * l3 - X[:, 1] = y + 0.10 * l0 * l1 + 0.05 * l2 * l3 - X[:, 2] = z + 0.18 * l0 * l1 + 0.10 * l0 * l2 - 0.08 * l1 * l2 - return X - - -def _phi(X: np.ndarray) -> np.ndarray: - return np.sqrt((X[:, 0] - 0.35) ** 2 + (X[:, 1] - 0.30) ** 2 + (X[:, 2] - 0.20) ** 2) - 0.28 - - -def _make_level_set(cell_family: str, degree: int) -> cutcells.LevelSetFunction: - if cell_family == "triangle": - dof_coordinates = _curved_triangle_nodes(degree) - vtk_type = np.array([5], dtype=np.int32) - tdim = 2 - expected_local_dofs = (degree + 1) * (degree + 2) // 2 - elif cell_family == "tetra": - dof_coordinates = _curved_tetra_nodes(degree) - vtk_type = np.array([10], dtype=np.int32) - tdim = 3 - expected_local_dofs = (degree + 1) * (degree + 2) * (degree + 3) // 6 - else: - raise ValueError(f"Unsupported cell_family={cell_family}") - - if dof_coordinates.shape[0] != expected_local_dofs: - raise RuntimeError( - f"{cell_family} p{degree}: generated {dof_coordinates.shape[0]} dofs, " - f"expected {expected_local_dofs}" - ) - - cell_dofs = np.arange(expected_local_dofs, dtype=np.int32) - cell_offsets = np.array([0, expected_local_dofs], dtype=np.int32) - mesh_data = cutcells.create_level_set_mesh_data( - dof_coordinates=dof_coordinates, - cell_dofs=cell_dofs, - cell_offsets=cell_offsets, - degree=degree, - tdim=tdim, - cell_types=vtk_type, - ) - dof_values = _phi(dof_coordinates) - return cutcells.create_level_set_function(mesh_data, dof_values) - - -def _run_case(cell_family: str, degree: int, output_dir: Path) -> None: - ls = _make_level_set(cell_family, degree) - output_path = output_dir / f"curved_{cell_family}_p{degree}.vtu" - cutcells.write_level_set_vtu(str(output_path), ls, field_name="phi") - - print(f"[{cell_family} p{degree}] cell_dofs={ls.mesh_data.cell_num_dofs(0)}") - print(f"[{cell_family} p{degree}] num_dofs={ls.mesh_data.num_dofs()}") - print(f"[{cell_family} p{degree}] wrote {output_path}") - print( - f"[{cell_family} p{degree}] ParaView: set 'Nonlinear Subdivision Level' > 0 " - "to visualize the curved high-order cell." - ) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Write single curved high-order Lagrange cells for ParaView inspection." - ) - parser.add_argument( - "--cell-family", - choices=["triangle", "tetra", "both"], - default="both", - help="Cell family to write.", - ) - parser.add_argument( - "--degree", - choices=["2", "3", "4", "all"], - default="all", - help="Polynomial degree.", - ) - parser.add_argument( - "--output-dir", - type=Path, - default=Path("demo_level_set_ho_curved_vtu"), - help="Output directory for VTU files.", - ) - args = parser.parse_args() - - args.output_dir.mkdir(parents=True, exist_ok=True) - degrees = _parse_degrees(args.degree) - families = ["triangle", "tetra"] if args.cell_family == "both" else [args.cell_family] - - for family in families: - for degree in degrees: - _run_case(family, degree, args.output_dir) - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_level_set_ho_vtk.py b/python/demo/demo_level_set_ho_vtk.py index 52baf38..5c0a942 100644 --- a/python/demo/demo_level_set_ho_vtk.py +++ b/python/demo/demo_level_set_ho_vtk.py @@ -20,10 +20,8 @@ def _parse_degrees(arg: str) -> list[int]: def _phi(X: np.ndarray) -> np.ndarray: - x = X[:, 0] - y = X[:, 1] - z = X[:, 2] if X.shape[1] > 2 else 0.0 - return np.sqrt((x - 0.15) ** 2 + (y + 0.1) ** 2 + (z - 0.05) ** 2) - 0.55 + z = X[2] if X.shape[1] > 2 else 0.0 + return np.sqrt((X[0] - 0.15) ** 2 + (X[1] + 0.1) ** 2 + (z - 0.05) ** 2) - 0.55 def _run_case(mesh, family: str, degree: int, output_dir: Path) -> None: @@ -33,7 +31,7 @@ def phi(X: np.ndarray) -> np.ndarray: callback_info["shape"] = tuple(X.shape) return _phi(X) - ls = cutcells.interpolate_level_set(mesh, phi, degree=degree) + ls = cutcells.create_level_set(mesh, phi, degree=degree) output_path = output_dir / f"{family}_p{degree}.vtu" cutcells.write_level_set_vtu(str(output_path), ls, field_name="phi") diff --git a/python/demo/demo_meshview.py b/python/demo/demo_meshview.py index 806b9e9..85c766e 100644 --- a/python/demo/demo_meshview.py +++ b/python/demo/demo_meshview.py @@ -57,25 +57,6 @@ def circle_phi(x: np.ndarray) -> float: return float(np.sqrt(np.sum((x - CENTER) ** 2)) - RADIUS) -# --------------------------------------------------------------------------- -# Helper: build MeshView from a pyvista UnstructuredGrid -# --------------------------------------------------------------------------- -# -# NOTE: Using mesh_from_pyvista from cutcells.mesh_utils module - - -# --------------------------------------------------------------------------- -# Mesh generation -# --------------------------------------------------------------------------- -# -# NOTE: Using rectangle_triangle_mesh from cutcells.mesh_utils module - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - - def main(): parser = argparse.ArgumentParser( description="Demo: cut_mesh_view with MeshView and LevelSetFunction" diff --git a/python/demo/demo_meshview_ho_cut.py b/python/demo/demo_meshview_ho_cut.py new file mode 100644 index 0000000..d4c6773 --- /dev/null +++ b/python/demo/demo_meshview_ho_cut.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: MeshView + create_level_set + cut(mesh, ls) straight output bridge. + +This is the first user-facing step for the HO pipeline: +1. build a pyvista tetrahedral mesh, +2. convert it to a MeshView, +3. build one polynomial level set with create_level_set(...), +4. call cut(mesh, ls), +5. select HOMeshPart objects, +6. obtain straight hybrid visualization meshes and straight quadrature from HOMeshPart, +7. write VTU files. + +Curving is intentionally out of scope here. This demo only exercises the +straight decomposition / straight output bridge. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np + +try: + import pyvista as pv +except ImportError as exc: + raise SystemExit( + "pyvista is required for this demo. " + "Install with: python -m pip install pyvista\n" + f"Import error: {exc}" + ) + +import cutcells +from cutcells import box_tetrahedron_mesh, mesh_from_pyvista + + +CENTER = np.array([0.1, -0.05, 0.0], dtype=np.float64) +RADIUS = 0.55 + + +def phi_batch(X: np.ndarray) -> np.ndarray: + return np.sqrt( + (X[0] - CENTER[0]) ** 2 + + (X[1] - CENTER[1]) ** 2 + + (X[2] - CENTER[2]) ** 2 + ) - RADIUS + + +def cutmesh_to_pyvista(cut_mesh) -> pv.UnstructuredGrid: + return pv.UnstructuredGrid( + np.asarray(cut_mesh.cells, dtype=np.int64), + np.asarray(cut_mesh.vtk_types, dtype=np.uint8), + np.asarray(cut_mesh.vertex_coords, dtype=np.float64), + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Demo: use cut(mesh, ls) on a MeshView from a pyvista tetra mesh." + ) + parser.add_argument( + "--n", + type=int, + default=8, + help="Structured tetrahedral mesh resolution parameter.", + ) + parser.add_argument( + "--degree", + type=int, + default=2, + help="Polynomial degree for create_level_set(...).", + ) + parser.add_argument( + "--quadrature-order", + type=int, + default=4, + help="Quadrature order for HOMeshPart.quadrature(...).", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("demo_meshview_ho_cut_output"), + help="Directory for VTU files.", + ) + parser.add_argument( + "--no-plot", + action="store_true", + help="Skip interactive pyvista plotting.", + ) + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Creating tetrahedral pyvista mesh with n={args.n} ...") + grid = box_tetrahedron_mesh( + -1.0, -1.0, -1.0, + 1.0, 1.0, 1.0, + args.n, args.n, args.n, + ) + mesh = mesh_from_pyvista(grid, tdim=3) + print(f" pyvista cells={grid.n_cells}, points={grid.n_points}") + print(f" MeshView gdim={mesh.gdim}, tdim={mesh.tdim}") + + print(f"Building polynomial level set with degree={args.degree} ...") + ls = cutcells.create_level_set(mesh, phi_batch, degree=args.degree, name="phi") + print(f" level-set dofs={ls.mesh_data.num_dofs()}") + + print("Running cut(mesh, ls) ...") + result = cutcells.cut(mesh, ls) + negative = result["phi < 0"] + interface = result["phi = 0"] + positive = result["phi > 0"] + + print(f" num_cut_cells={result.num_cut_cells}") + print(f" phi < 0 : cut={negative.num_cut_cells}, uncut={negative.num_uncut_cells}") + print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") + print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") + + print("Building straight hybrid visualization meshes from HOMeshPart ...") + inside_full = negative.visualization_mesh(mode="full", triangulate=False) + inside_cut = negative.visualization_mesh(mode="cut_only", triangulate=False) + interface_mesh = interface.visualization_mesh(mode="cut_only", triangulate=False) + + print(f" inside full cells={len(np.asarray(inside_full.types))}") + print(f" inside cut-only cells={len(np.asarray(inside_cut.types))}") + print(f" interface cells={len(np.asarray(interface_mesh.types))}") + + print("Computing straight quadrature from HOMeshPart ...") + q_inside_full = negative.quadrature( + order=args.quadrature_order, + mode="full", + triangulate=False, + ) + q_inside_cut = negative.quadrature( + order=args.quadrature_order, + mode="cut_only", + triangulate=False, + ) + q_interface = interface.quadrature( + order=args.quadrature_order, + mode="cut_only", + triangulate=False, + ) + + print(f" inside full quadrature sum = {q_inside_full.weights.sum():.12g}") + print(f" inside cut-only quadrature sum= {q_inside_cut.weights.sum():.12g}") + print(f" interface quadrature sum = {q_interface.weights.sum():.12g}") + + inside_full_path = args.output_dir / "phi_negative_full_straight.vtu" + inside_cut_path = args.output_dir / "phi_negative_cut_only_straight.vtu" + interface_path = args.output_dir / "phi_interface_straight.vtu" + + negative.write_vtu(str(inside_full_path), mode="full", triangulate=False) + negative.write_vtu(str(inside_cut_path), mode="cut_only", triangulate=False) + interface.write_vtu(str(interface_path), mode="cut_only", triangulate=False) + + print(f" wrote {inside_full_path}") + print(f" wrote {inside_cut_path}") + print(f" wrote {interface_path}") + + if args.no_plot: + return + + pv_inside_full = cutmesh_to_pyvista(inside_full) + pv_interface = cutmesh_to_pyvista(interface_mesh) + + plotter = pv.Plotter(shape=(1, 2), title="CutCells HO straight-output demo") + plotter.subplot(0, 0) + plotter.add_title("phi < 0, mode='full'") + plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.25) + plotter.add_mesh(pv_inside_full, color="steelblue", opacity=0.75, show_edges=True) + + plotter.subplot(0, 1) + plotter.add_title("phi = 0") + plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.15) + plotter.add_mesh(pv_interface, color="crimson", show_edges=True, line_width=2) + + plotter.link_views() + plotter.show() + + +if __name__ == "__main__": + main() diff --git a/python/demo/meshview_levelset_demo.py b/python/demo/meshview_levelset_demo.py deleted file mode 100644 index 398a1f2..0000000 --- a/python/demo/meshview_levelset_demo.py +++ /dev/null @@ -1,325 +0,0 @@ -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT -""" -Demo: MeshView + LevelSetFunction Python wrappers -================================================== - -Demonstrates: - * Building a ``cutcells.MeshView`` from a pyvista ``UnstructuredGrid``. - * Building a ``cutcells.LevelSetFunction`` from a plain Python callable. - * Calling ``cutcells.cut_mesh_view`` to obtain cut meshes for - – the interface (phi = 0) - – the inside (phi < 0) - * Visualising the results with pyvista. - -The background mesh is a 2-D Delaunay triangulation of a square [-1, 1]^2. -The level-set is a circle of radius 0.7 centred at the origin: - phi(x) = sqrt(x0^2 + x1^2) - 0.7 -""" - -import argparse - -import numpy as np - -try: - import pyvista as pv -except ImportError as exc: - raise SystemExit( - "pyvista is required for this demo. " - "Install with: python -m pip install pyvista\n" - f"Import error: {exc}" - ) - -import cutcells - - -# --------------------------------------------------------------------------- -# Level-set definition -# --------------------------------------------------------------------------- - -RADIUS = 0.7 -CENTER = np.array([0.0, 0.0, 0.0]) - - -def circle_phi(x: np.ndarray) -> float: - """Signed-distance to a circle of radius RADIUS centred at CENTER. - - ``x`` is a 1-D NumPy array of length 3 (pyvista always stores 3-D coords). - Returns a float: negative inside the circle, positive outside. - """ - return float(np.sqrt(np.sum((x - CENTER) ** 2)) - RADIUS) - - -# --------------------------------------------------------------------------- -# Helper: build MeshView from a pyvista UnstructuredGrid -# --------------------------------------------------------------------------- - - -def mesh_view_from_pyvista(grid: pv.UnstructuredGrid, tdim: int) -> cutcells.MeshView: - """Wrap a pyvista UnstructuredGrid as a cutcells MeshView. - - Parameters - ---------- - grid : pv.UnstructuredGrid - Source grid. Points must already by stored as (N, 3) float64. - tdim : int - Topological dimension of the cells (e.g. 2 for triangles/quads). - - Returns - ------- - cutcells.MeshView (MeshView_float64 alias) - Zero-copy view backed by the pyvista arrays; the pyvista grid must - remain alive as long as the MeshView is used. - """ - coordinates = np.asarray(grid.points, dtype=np.float64) # (N, 3) - connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) # flat CSR - offsets = np.asarray(grid.offset, dtype=np.int32) # (ncells+1,) - cell_types_np = np.asarray(grid.celltypes, dtype=np.int32) # VTK type IDs - - return cutcells.MeshView( - coordinates=coordinates, - connectivity=connectivity, - offsets=offsets, - cell_types=cell_types_np, - tdim=tdim, - ) - - -# --------------------------------------------------------------------------- -# Mesh generation -# --------------------------------------------------------------------------- - - -def create_triangle_mesh(x0: float, y0: float, x1: float, y1: float, N: int): - """Create a 2-D Delaunay triangle mesh on [x0,x1]×[y0,y1]. - - Returns a pyvista UnstructuredGrid with triangular cells. - """ - x = np.linspace(x0, x1, num=N) - y = np.linspace(y0, y1, num=N) - xx, yy, zz = np.meshgrid(x, y, [0.0]) - points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()] - poly = pv.PolyData(points).delaunay_2d() - return pv.UnstructuredGrid(poly) - - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - - -def main(): - parser = argparse.ArgumentParser( - description="Demo: cut_mesh_view with MeshView and LevelSetFunction" - ) - parser.add_argument( - "--n", - type=int, - default=30, - help="Grid points per axis (default: 30)", - ) - parser.add_argument( - "--no-plot", - action="store_true", - help="Skip interactive visualisation (useful for CI / off-screen runs)", - ) - args = parser.parse_args() - - N = int(args.n) - - # ------------------------------------------------------------------ - # 1. Build the background pyvista grid and wrap it in a MeshView - # ------------------------------------------------------------------ - print(f"Creating {N}×{N} Delaunay triangle mesh …") - grid = create_triangle_mesh(-1.0, -1.0, 1.0, 1.0, N) - print(f" nodes: {grid.n_points}, cells: {grid.n_cells}") - - mesh_view = mesh_view_from_pyvista(grid, tdim=2) - print( - f" MeshView gdim={mesh_view.gdim}, tdim={mesh_view.tdim}, " - f"num_nodes={mesh_view.num_nodes()}, num_cells={mesh_view.num_cells()}" - ) - - # ------------------------------------------------------------------ - # 2. Build a LevelSetFunction from a Python callable - # ------------------------------------------------------------------ - # The callable receives a length-3 NumPy array (x) and may optionally - # accept a second argument cell_id (ignored here). - level_set = cutcells.LevelSetFunction( - value=circle_phi, - gdim=mesh_view.gdim, # 3 (pyvista always stores 3-D coords) - ) - print( - f"\nLevelSetFunction has_value={level_set.has_value()}, " - f"has_nodal_values={level_set.has_nodal_values()}" - ) - - # ------------------------------------------------------------------ - # 3. Cut the mesh – interface (phi = 0) - # ------------------------------------------------------------------ - print("\nCutting mesh for phi=0 (interface) …") - cut_interface = cutcells.cut_mesh_view( - mesh_view, level_set, "phi=0", triangulate=True - ) - print( - f" Interface cut mesh: {len(cut_interface.vtk_types)} cells, " - f"{len(cut_interface.vertex_coords)} vertices" - ) - - # ------------------------------------------------------------------ - # 4. Cut the mesh – interior (phi < 0) - # ------------------------------------------------------------------ - print("Cutting mesh for phi<0 (inside) …") - cut_inside = cutcells.cut_mesh_view(mesh_view, level_set, "phi<0", triangulate=True) - print( - f" Inside cut mesh: {len(cut_inside.vtk_types)} cells, " - f"{len(cut_inside.vertex_coords)} vertices" - ) - - # ------------------------------------------------------------------ - # 5. (Optional) also build nodal values and use LevelSetFunction - # with nodal_values instead of value callable – same result - # ------------------------------------------------------------------ - print("\nBuilding nodal level-set values manually …") - ls_nodal = np.array([circle_phi(p) for p in grid.points], dtype=np.float64) - level_set_nodal = cutcells.LevelSetFunction( - nodal_values=ls_nodal, - gdim=mesh_view.gdim, - ) - print( - f" LevelSetFunction (nodal) has_nodal_values={level_set_nodal.has_nodal_values()}" - ) - - cut_inside_nodal = cutcells.cut_mesh_view( - mesh_view, level_set_nodal, "phi<0", triangulate=True - ) - assert len(cut_inside_nodal.vtk_types) == len(cut_inside.vtk_types), ( - "Nodal and callable level-set should give identical cut meshes" - ) - print(" Nodal and callable results agree ✓") - - # ------------------------------------------------------------------ - # 6. Runtime Quadrature and Physical Mapping - # ------------------------------------------------------------------ - print("\nComputing runtime quadrature for phi<0 …") - order = 2 - # Use the flat arrays from the grid - points_flat = np.asarray(grid.points, dtype=np.float64).ravel() - connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) - offsets = np.asarray(grid.offset, dtype=np.int32) - celltypes = np.asarray(grid.celltypes, dtype=np.int32) - - # Calculate quadrature rules in reference space for phi<0 - q_rules_inside = cutcells.runtime_quadrature( - ls_nodal, - points_flat, - connectivity, - offsets, - celltypes, - "phi<0", - triangulate=True, - order=order, - ) - print(f" Inside: Generated {len(q_rules_inside.weights)} total quadrature points.") - - # Calculate quadrature rules in reference space for phi=0 - print("Computing runtime quadrature for phi=0 …") - q_rules_interface = cutcells.runtime_quadrature( - ls_nodal, - points_flat, - connectivity, - offsets, - celltypes, - "phi=0", - triangulate=True, - order=order, - ) - print( - f" Interface: Generated {len(q_rules_interface.weights)} total quadrature points." - ) - - # Map to physical space - print(" Mapping quadrature points to physical space …") - q_points_inside_phys = cutcells.physical_points( - q_rules_inside, points_flat, connectivity, offsets, celltypes - ).reshape(-1, 3) - - q_points_interface_phys = cutcells.physical_points( - q_rules_interface, points_flat, connectivity, offsets, celltypes - ).reshape(-1, 3) - - # ------------------------------------------------------------------ - # 7. Visualise - # ------------------------------------------------------------------ - if args.no_plot: - print("\n--no-plot specified, skipping visualisation.") - return - - # -- wrap cut results as pyvista grids -- - pv_interface = pv.UnstructuredGrid( - cut_interface.cells, - cut_interface.vtk_types, - np.asarray(cut_interface.vertex_coords, dtype=np.float64), - ) - pv_inside = pv.UnstructuredGrid( - cut_inside.cells, - cut_inside.vtk_types, - np.asarray(cut_inside.vertex_coords, dtype=np.float64), - ) - - # -- inside background cells (entirely inside the circle) -- - points_flat = np.asarray(grid.points, dtype=np.float64).ravel() - connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) - offsets = np.asarray(grid.offset, dtype=np.int32) - celltypes = np.asarray(grid.celltypes, dtype=np.int32) - inside_ids = cutcells.locate_cells( - ls_nodal, points_flat, connectivity, offsets, celltypes, "phi<0" - ) - pv_inside_bg = grid.extract_cells(inside_ids) - - # -- combined inside view: background inside cells + cut cells -- - pv_combined = pv_inside_bg.merge(pv_inside) - - # -- wrap quadrature points as pyvista points -- - pv_q_points_inside = pv.PolyData(q_points_inside_phys) - pv_q_points_interface = pv.PolyData(q_points_interface_phys) - - # Plot - pl = pv.Plotter(shape=(1, 2), title="CutCells – MeshView + LevelSetFunction demo") - - pl.subplot(0, 0) - pl.add_title("Interface (phi = 0) + Quad Points") - pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) - pl.add_mesh(pv_interface, show_edges=True, color="red") - pl.add_mesh( - pv_q_points_interface, - color="yellow", - point_size=10, - render_points_as_spheres=True, - label=f"Interface Points ({len(q_rules_interface.weights)})", - ) - pl.add_legend() - pl.view_xy() - - pl.subplot(0, 1) - pl.add_title("Inside domain (phi < 0) + Quad Points") - pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) - pl.add_mesh(pv_combined, show_edges=True, color="steelblue", opacity=0.8) - pl.add_mesh( - pv_q_points_inside, - color="orange", - point_size=10, - render_points_as_spheres=True, - label=f"Inside Points ({len(q_rules_inside.weights)})", - ) - pl.add_legend() - pl.view_xy() - - pl.show() - - -if __name__ == "__main__": - main() diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py new file mode 100644 index 0000000..5d14f93 --- /dev/null +++ b/python/tests/test_certification_refinement.py @@ -0,0 +1,499 @@ +import unittest + +import numpy as np + +import cutcells + + +def _single_triangle_mesh(): + coords = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], dtype=np.float64) + connectivity = np.array([0, 1, 2], dtype=np.int32) + offsets = np.array([0, 3], dtype=np.int32) + cell_types = np.array([5], dtype=np.int32) # VTK_TRIANGLE + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=2) + + +def _single_tetra_mesh(): + coords = np.array( + [[0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0]], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3], dtype=np.int32) + offsets = np.array([0, 4], dtype=np.int32) + cell_types = np.array([10], dtype=np.int32) # VTK_TETRA + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) + + +class CertificationRefinementTests(unittest.TestCase): + def test_restrict_edge_bernstein_exact_matches_parent_evaluation(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[0] * X[1] + 0.25 * X[1] * X[1] - 0.1, + degree=2, + ) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + xi_a = np.array([0.1, 0.15], dtype=np.float64) + xi_b = np.array([0.7, 0.2], dtype=np.float64) + edge_coeffs = cutcells.restrict_edge_bernstein_exact( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + xi_a, + xi_b, + ) + + for t in np.linspace(0.0, 1.0, 9): + xi = (1.0 - t) * xi_a + t * xi_b + parent_val = cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + xi, + ) + edge_val = cutcells.evaluate_bernstein( + cutcells.CellType.interval, + ls_cell.bernstein_order, + np.asarray(edge_coeffs), + np.array([t], dtype=np.float64), + ) + self.assertAlmostEqual(edge_val, parent_val, places=10) + + def test_edge_endpoint_zero_semantics(self): + one_root_tag, split = cutcells.classify_edge_roots( + np.array([0.0, 0.0, 1.0], dtype=np.float64), + max_depth=18, + ) + self.assertEqual(one_root_tag, cutcells.EdgeRootTag.one_root) + self.assertIsNone(split) + + multi_tag, split = cutcells.classify_edge_roots( + np.array([0.0, -0.3, 0.4], dtype=np.float64), + max_depth=18, + ) + self.assertEqual(multi_tag, cutcells.EdgeRootTag.multiple_roots) + self.assertGreater(split, 0.0) + self.assertLess(split, 1.0) + + def test_green_refinement_on_multiple_root_edge(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: (X[0] - 0.2) * (X[0] - 0.8), + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + tags = np.asarray(adapt.edge_root_tags(0)) + self.assertGreaterEqual( + np.count_nonzero(tags == cutcells.EdgeRootTag.multiple_roots.value), + 1, + ) + + changed = cutcells.refine_green_on_multiple_root_edges(adapt, 0) + self.assertTrue(changed) + self.assertEqual(adapt.num_vertices(), 4) + self.assertEqual(adapt.num_cells(), 2) + + def test_green_refinement_only_invalidates_new_edges(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: (X[0] - 0.2) * (X[0] - 0.8), + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + before = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(before == cutcells.EdgeRootTag.not_classified.value), + 0, + ) + + changed = cutcells.refine_green_on_multiple_root_edges(adapt, 0) + self.assertTrue(changed) + + after_refine = np.asarray(adapt.edge_root_tags(0)) + self.assertGreater( + np.count_nonzero(after_refine == cutcells.EdgeRootTag.not_classified.value), + 0, + ) + self.assertGreater( + np.count_nonzero(after_refine != cutcells.EdgeRootTag.not_classified.value), + 0, + ) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + after_recertify = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(after_recertify == cutcells.EdgeRootTag.not_classified.value), + 0, + ) + + def test_red_refinement_on_ambiguous_triangle(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[1] * (1.0 - X[0] - X[1]) - 1e-3, + degree=3, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + cutcells.classify_leaf_cells(adapt, ls_cell, 0) + self.assertEqual( + np.asarray(adapt.cell_cert_tags(0)).tolist(), + [cutcells.CellCertTag.ambiguous.value], + ) + + changed = cutcells.refine_red_on_ambiguous_cells(adapt, 0) + self.assertTrue(changed) + self.assertEqual(adapt.num_vertices(), 6) + self.assertEqual(adapt.num_cells(), 4) + self.assertEqual(adapt.num_edges(), 9) + + def test_triangle_cell_is_marked_ready_to_cut(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.3, + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + cutcells.classify_leaf_cells(adapt, ls_cell, 0) + self.assertEqual( + np.asarray(adapt.cell_cert_tags(0)).tolist(), + [cutcells.CellCertTag.ready_to_cut.value], + ) + + edge_tags = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.one_root.value), + 2, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.multiple_roots.value), + 0, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.zero.value), + 0, + ) + + def test_triangle_ready_to_cut_cell_is_replaced_by_polygonal_lut_cells(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.3, + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_refine_and_process_ready_cells(adapt, ls_cell, 0) + self.assertEqual(adapt.num_cells(), 2) + + cell_types = np.asarray(adapt.cell_types) + self.assertIn(cutcells.CellType.triangle.value, cell_types.tolist()) + self.assertIn(cutcells.CellType.quadrilateral.value, cell_types.tolist()) + + tags = np.asarray(adapt.cell_cert_tags(0)) + self.assertEqual( + sorted(np.unique(tags).tolist()), + sorted([ + cutcells.CellCertTag.negative.value, + cutcells.CellCertTag.positive.value, + ]), + ) + + def test_ready_to_cut_triangle_uses_exact_one_root_vertices(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] - 0.25, + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_refine_and_process_ready_cells(adapt, ls_cell, 0) + + coords = np.asarray(adapt.vertex_coords).reshape(-1, 2) + source_edges = np.asarray(adapt.vertex_source_edge_id) + + expected_curved_root = np.array([0.5, 0.0]) + expected_linear_root = np.array([0.25, 0.0]) + expected_second_root = np.array([0.0, 0.25]) + + d_curved = np.linalg.norm(coords - expected_curved_root, axis=1) + d_second = np.linalg.norm(coords - expected_second_root, axis=1) + d_linear = np.linalg.norm(coords - expected_linear_root, axis=1) + + curved_id = int(np.argmin(d_curved)) + second_id = int(np.argmin(d_second)) + + self.assertLess(d_curved[curved_id], 1e-10) + self.assertLess(d_second[second_id], 1e-10) + self.assertGreater(np.min(d_linear), 1e-2) + self.assertGreaterEqual(int(source_edges[curved_id]), 0) + self.assertGreaterEqual(int(source_edges[second_id]), 0) + + def test_ho_cut_smoke_uses_certified_triangle_pipeline(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.3, + degree=1, + name="phi", + ) + + result = cutcells.ho_cut(mesh, ls) + + self.assertEqual(result.num_cut_cells, 1) + np.testing.assert_array_equal(np.asarray(result.parent_cell_ids), np.array([0])) + self.assertEqual(np.asarray(result.cell_domains).shape, (1, 1)) + + def test_cut_overload_supports_mesh_part_selection(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.3, + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls) + negative = result["phi < 0"] + interface = result["phi = 0"] + + self.assertEqual(result.num_cut_cells, 1) + np.testing.assert_array_equal(np.asarray(negative.cut_cell_ids), np.array([0])) + np.testing.assert_array_equal(np.asarray(interface.cut_cell_ids), np.array([0])) + + adapt = result.adapt_cell(0) + self.assertGreater(adapt.num_cells(), 0) + self.assertGreater(adapt.num_vertices(), 3) + + def test_cut_detects_quadratic_interior_tetra_intersection(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: ( + (X[0] - 0.2) * (X[0] - 0.2) + + (X[1] - 0.2) * (X[1] - 0.2) + + (X[2] - 0.2) * (X[2] - 0.2) + - 0.09 + ), + degree=2, + name="phi", + ) + + vertex_vals = np.array( + [ + (node[0] - 0.2) * (node[0] - 0.2) + + (node[1] - 0.2) * (node[1] - 0.2) + + (node[2] - 0.2) * (node[2] - 0.2) + - 0.09 + for node in np.asarray(mesh.coordinates) + ], + dtype=np.float64, + ) + self.assertTrue(np.all(vertex_vals > 0.0)) + + ls_cell = cutcells.make_cell_level_set(ls, 0) + self.assertLess(np.min(np.asarray(ls_cell.bernstein_coeffs)), 0.0) + + result = cutcells.cut(mesh, ls) + interface = result["phi = 0"] + + self.assertEqual(result.num_cut_cells, 1) + np.testing.assert_array_equal(np.asarray(interface.cut_cell_ids), np.array([0])) + + def test_certify_and_refine_runs_triangle_workflow(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[1] * (1.0 - X[0] - X[1]) - 1e-3, + degree=3, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_and_refine(adapt, ls_cell, 0, max_iterations=2) + self.assertGreater(adapt.num_cells(), 1) + self.assertGreater(adapt.num_vertices(), 3) + + def test_green_refinement_on_multiple_root_tetra_edge(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: (X[0] - 0.1) * (X[0] - 0.6), + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + tags = np.asarray(adapt.edge_root_tags(0)) + splits = np.asarray(adapt.edge_green_split_params(0)) + edge_connectivity = np.asarray(adapt.edge_connectivity) + edge_offsets = np.asarray(adapt.edge_offsets) + self.assertGreaterEqual( + np.count_nonzero(tags == cutcells.EdgeRootTag.multiple_roots.value), + 3, + ) + + split_edge = int(np.flatnonzero(tags == cutcells.EdgeRootTag.multiple_roots.value)[0]) + a, b = edge_connectivity[edge_offsets[split_edge]:edge_offsets[split_edge + 1]] + split_t = float(splits[split_edge]) + self.assertNotAlmostEqual(split_t, 0.5, places=6) + + vertex_coords = np.asarray(adapt.vertex_coords).reshape(-1, 3) + expected = (1.0 - split_t) * vertex_coords[a] + split_t * vertex_coords[b] + + changed = cutcells.refine_green_on_multiple_root_edges(adapt, 0) + self.assertTrue(changed) + self.assertEqual(adapt.num_vertices(), 5) + self.assertEqual(adapt.num_cells(), 2) + np.testing.assert_allclose( + np.asarray(adapt.vertex_coords).reshape(-1, 3)[-1], + expected, + atol=1e-12, + ) + + def test_tetra_cell_is_marked_ready_to_cut_for_triangular_interface(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] + X[2] - 0.2, + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + cutcells.classify_leaf_cells(adapt, ls_cell, 0) + self.assertEqual( + np.asarray(adapt.cell_cert_tags(0)).tolist(), + [cutcells.CellCertTag.ready_to_cut.value], + ) + + edge_tags = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.one_root.value), + 3, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.multiple_roots.value), + 0, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.zero.value), + 0, + ) + + def test_tetra_cell_is_marked_ready_to_cut_for_quadrilateral_interface(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - X[2] - 0.2, + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + cutcells.classify_leaf_cells(adapt, ls_cell, 0) + self.assertEqual( + np.asarray(adapt.cell_cert_tags(0)).tolist(), + [cutcells.CellCertTag.ready_to_cut.value], + ) + + edge_tags = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.one_root.value), + 4, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.multiple_roots.value), + 0, + ) + self.assertEqual( + np.count_nonzero(edge_tags == cutcells.EdgeRootTag.zero.value), + 0, + ) + + def test_tetra_ready_to_cut_cell_is_replaced_without_tetra_triangulation(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] + X[2] - 0.2, + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_refine_and_process_ready_cells(adapt, ls_cell, 0) + self.assertGreaterEqual(adapt.num_cells(), 2) + + cell_types = np.asarray(adapt.cell_types) + self.assertIn(cutcells.CellType.tetrahedron.value, cell_types.tolist()) + self.assertIn(cutcells.CellType.prism.value, cell_types.tolist()) + + tags = np.asarray(adapt.cell_cert_tags(0)) + self.assertEqual( + sorted(np.unique(tags).tolist()), + sorted([ + cutcells.CellCertTag.negative.value, + cutcells.CellCertTag.positive.value, + ]), + ) + + def test_certify_and_refine_recursively_splits_tetra_multiple_root_edges(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: (X[0] - 0.2) * (X[0] - 0.8), + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_and_refine(adapt, ls_cell, 0, max_iterations=8) + self.assertGreater(adapt.num_cells(), 2) + self.assertGreater(adapt.num_vertices(), 5) + + cutcells.classify_new_edges(adapt, ls_cell, 0) + tags = np.asarray(adapt.edge_root_tags(0)) + self.assertEqual( + np.count_nonzero(tags == cutcells.EdgeRootTag.multiple_roots.value), + 0, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_ho_part_straight_output.py b/python/tests/test_ho_part_straight_output.py new file mode 100644 index 0000000..16bddad --- /dev/null +++ b/python/tests/test_ho_part_straight_output.py @@ -0,0 +1,69 @@ +from pathlib import Path + +import numpy as np + +import cutcells + + +def _single_tetra_mesh(): + coords = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3], dtype=np.int32) + offsets = np.array([0, 4], dtype=np.int32) + cell_types = np.array([10], dtype=np.int32) + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) + + +def test_homeshpart_straight_output_bridge(tmp_path: Path): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.6, + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls) + negative = result["phi < 0"] + interface = result["phi = 0"] + + vis_cut = negative.visualization_mesh(mode="cut_only") + vis_full = negative.visualization_mesh(mode="full") + vis_interface = interface.visualization_mesh(mode="cut_only") + vis_cut_triangulated = negative.visualization_mesh(mode="cut_only", triangulate=True) + vis_interface_triangulated = interface.visualization_mesh( + mode="cut_only", triangulate=True + ) + + assert len(np.asarray(vis_cut.types)) > 0 + assert len(np.asarray(vis_full.types)) >= len(np.asarray(vis_cut.types)) + assert len(np.asarray(vis_interface.types)) > 0 + assert np.unique(np.asarray(vis_cut.vtk_types)).tolist() == [13] + assert np.unique(np.asarray(vis_interface.vtk_types)).tolist() == [9] + assert np.unique(np.asarray(vis_cut_triangulated.vtk_types)).tolist() == [10] + assert np.unique(np.asarray(vis_interface_triangulated.vtk_types)).tolist() == [5] + + q_cut = negative.quadrature(order=3, mode="cut_only") + q_full = negative.quadrature(order=3, mode="full") + q_interface = interface.quadrature(order=3, mode="cut_only") + + assert q_cut.weights.sum() > 0.0 + assert q_full.weights.sum() >= q_cut.weights.sum() + assert q_interface.weights.sum() > 0.0 + + negative_path = tmp_path / "negative_full.vtu" + interface_path = tmp_path / "interface.vtu" + negative.write_vtu(str(negative_path), mode="full") + interface.write_vtu(str(interface_path), mode="cut_only") + + assert negative_path.exists() + assert interface_path.exists() + assert "Name=\"types\" format=\"ascii\">13 " in negative_path.read_text() + assert "Name=\"types\" format=\"ascii\">9 " in interface_path.read_text() diff --git a/python/tests/test_level_set_mesh_data.py b/python/tests/test_level_set_mesh_data.py index 09a3ef8..0a229b0 100644 --- a/python/tests/test_level_set_mesh_data.py +++ b/python/tests/test_level_set_mesh_data.py @@ -91,3 +91,48 @@ def test_create_level_set_mesh_data_from_arrays(): np.testing.assert_array_equal(np.asarray(data.cell_offsets), cell_offsets) np.testing.assert_allclose(np.asarray(data.dof_coordinates), dof_coordinates) assert np.asarray(data.dof_parent_dim).size == 0 + + +def test_make_cell_level_set_matches_generated_mesh_data_without_provenance(): + coords = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3], dtype=np.int32) + offsets = np.array([0, 4], dtype=np.int32) + cell_types = np.array([10], dtype=np.int32) # VTK_TETRA + mesh = cutcells.MeshView(coords, connectivity, offsets, cell_types, 3) + + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + 0.5 * X[1] * X[2] - 0.25 * X[2] + 0.1, + degree=2, + name="phi", + ) + ls_cell_generated = cutcells.make_cell_level_set(ls, 0) + + mesh_data = ls.mesh_data + mesh_data_from_arrays = cutcells.create_level_set_mesh_data( + np.array(mesh_data.dof_coordinates), + np.array(mesh_data.cell_dofs), + np.array(mesh_data.cell_offsets), + degree=mesh_data.degree, + tdim=mesh_data.tdim, + cell_types=np.array([10], dtype=np.int32), + ) + ls_from_arrays = cutcells.create_level_set_function( + mesh_data_from_arrays, + np.array(ls.dof_values), + name="phi", + ) + ls_cell_from_arrays = cutcells.make_cell_level_set(ls_from_arrays, 0) + + np.testing.assert_allclose( + np.asarray(ls_cell_from_arrays.bernstein_coeffs), + np.asarray(ls_cell_generated.bernstein_coeffs), + ) diff --git a/python/tests/test_level_set_vtu.py b/python/tests/test_level_set_vtu.py index 822e339..5487c2a 100644 --- a/python/tests/test_level_set_vtu.py +++ b/python/tests/test_level_set_vtu.py @@ -147,6 +147,27 @@ def test_write_level_set_vtu_xml_matches_vtk_expectations(tmp_path): assert types.attrib["type"] == "UInt8" +def test_write_vtk_generic_writer_triangle(tmp_path): + output = tmp_path / "triangle_linear.vtu" + points = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], dtype=np.float64) + connectivity = np.array([0, 1, 2], dtype=np.int32) + offsets = np.array([0, 3], dtype=np.int32) + element_types = np.array([cutcells.CellType.triangle.value], dtype=np.int32) + + cutcells.write_vtk( + str(output), + points.reshape(-1), + connectivity, + offsets, + element_types, + gdim=2, + ) + + assert [int(x) for x in _read_dataarray(output, "connectivity").split()] == [0, 1, 2] + assert [int(x) for x in _read_dataarray(output, "offsets").split()] == [3] + assert [int(x) for x in _read_dataarray(output, "types").split()] == [5] + + @pytest.mark.parametrize( ("cell_family", "degree", "expected_type", "expected_local_dofs"), [ From 7c556a74c19b5d507c3f854cf928e751ad19558c Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Fri, 17 Apr 2026 18:20:08 +0200 Subject: [PATCH 09/23] make consistent basix numbering --- cpp/src/cell_topology.h | 29 ++++++++++------- cpp/src/ho_mesh_part_output.cpp | 10 ++---- cpp/src/write_vtk.cpp | 1 + cpp/src/write_vtk.h | 56 +++++++++++++++++++++++++++++++++ python/cutcells/wrapper.cpp | 10 +----- 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/cpp/src/cell_topology.h b/cpp/src/cell_topology.h index 8dcf41a..4dec37e 100644 --- a/cpp/src/cell_topology.h +++ b/cpp/src/cell_topology.h @@ -183,28 +183,35 @@ inline std::span> edges(type cell_type) /// Hexahedron: 6 faces (quads) /// Face order: -X, +X, -Y, +Y, -Z, +Z (left, right, front, back, bottom, top) +/// Vertex ordering follows basix tensor-product convention for quadrilateral: +/// basix quad: 0=(0,0), 1=(1,0), 2=(0,1), 3=(1,1) (NOT cyclic). +/// Conversion from VTK cyclic {a,b,c,d} -> basix {a,b,d,c}. inline constexpr std::array, 6> hexahedron_faces = {{ - {0, 3, 7, 4}, // -X (left) - {1, 2, 6, 5}, // +X (right) - {0, 1, 5, 4}, // -Y (front) - {3, 2, 6, 7}, // +Y (back) - {0, 1, 2, 3}, // -Z (bottom) - {4, 5, 6, 7} // +Z (top) + {0, 3, 4, 7}, // -X (left) was VTK {0,3,7,4} + {1, 2, 5, 6}, // +X (right) was VTK {1,2,6,5} + {0, 1, 4, 5}, // -Y (front) was VTK {0,1,5,4} + {3, 2, 7, 6}, // +Y (back) was VTK {3,2,6,7} + {0, 1, 2, 3}, // -Z (bottom) was VTK {0,1,3,2} -> basix {0,1,2,3} + {4, 5, 6, 7} // +Z (top) was VTK {4,5,7,6} -> basix {4,5,6,7} }}; /// Prism: 5 faces (2 triangles + 3 quads) -/// Represented as max 4 vertices per face, with -1 padding for triangles +/// Represented as max 4 vertices per face, with -1 padding for triangles. +/// Quad faces use basix tensor-product ordering: 0=(0,0),1=(1,0),2=(0,1),3=(1,1). +/// Conversion from VTK cyclic {a,b,c,d} -> basix {a,b,d,c}. inline constexpr std::array, 5> prism_faces = {{ {0, 1, 2, -1}, // bottom triangle {3, 4, 5, -1}, // top triangle - {0, 1, 4, 3}, // quad face - {1, 2, 5, 4}, // quad face - {2, 0, 3, 5} // quad face + {0, 1, 3, 4}, // quad (y=0 plane) was VTK {0,1,4,3} + {1, 2, 4, 5}, // quad (x+y=1 plane) was VTK {1,2,5,4} + {2, 0, 5, 3} // quad (x=0 plane) was VTK {2,0,3,5} }}; /// Pyramid: 5 faces (1 quad base + 4 triangles) +/// Base quad uses basix tensor-product ordering: 0=(0,0),1=(1,0),2=(0,1),3=(1,1). +/// Conversion from VTK cyclic {0,1,2,3} -> basix {0,1,3,2}. inline constexpr std::array, 5> pyramid_faces = {{ - {0, 1, 2, 3}, // base quad + {0, 1, 3, 2}, // base quad: basix ordering, was VTK {0,1,2,3} {0, 1, 4, -1}, // triangle {1, 2, 4, -1}, // triangle {2, 3, 4, -1}, // triangle diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index 2cc434f..2648ca4 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -546,12 +546,6 @@ void append_cut_entities(mesh::CutMesh& out, mesh.gdim, std::span(ref_coords.data(), ref_coords.size())); - // Volume entities (from adapt_cell) are in basix vertex ordering; - // face entities (from cell_topology.h face_vertices) are already - // in VTK cyclic ordering and must NOT be permuted again. - const bool entity_is_basix = - (cell::get_tdim(entity.type) == adapt_cell.tdim); - append_mesh_entity( out, std::span(phys_coords.data(), phys_coords.size()), @@ -559,7 +553,7 @@ void append_cut_entities(mesh::CutMesh& out, entity.type, static_cast(parent_cell_id), triangulate, - entity_is_basix); + /*input_is_basix=*/true); if (rules != nullptr) { @@ -573,7 +567,7 @@ void append_cut_entities(mesh::CutMesh& out, static_cast(parent_cell_id), quadrature_order, triangulate || !is_simplex(entity.type), - entity_is_basix); + /*input_is_basix=*/true); } } } diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index 9b94e96..f157af8 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -15,6 +15,7 @@ #include "write_vtk.h" #include "cell_types.h" +#include "reference_cell.h" namespace { diff --git a/cpp/src/write_vtk.h b/cpp/src/write_vtk.h index 3dd48cf..4e844a3 100644 --- a/cpp/src/write_vtk.h +++ b/cpp/src/write_vtk.h @@ -6,7 +6,11 @@ #pragma once #include +#include +#include #include "cut_cell.h" +#include "cut_mesh.h" +#include "reference_cell.h" #include "level_set.h" namespace cutcells::io @@ -19,6 +23,58 @@ namespace cutcells::io void write_vtk(std::string filename, cell::CutCell& cut_cell); + /// Write a CutMesh (basix-ordered internally) to a VTU file. + /// Non-simplex cells are permuted to VTK vertex ordering before writing. + template + void write_vtk(std::string filename, const mesh::CutMesh& cut_mesh) + { + const int n_cells = cut_mesh._num_cells; + const int gdim = cut_mesh._gdim; + + // Permute non-simplex cell connectivity from basix to VTK ordering. + std::vector vtk_connectivity; + vtk_connectivity.reserve(cut_mesh._connectivity.size()); + + for (int c = 0; c < n_cells; ++c) + { + const int start = (c == 0) ? 0 : cut_mesh._offset[static_cast(c)]; + const int end = cut_mesh._offset[static_cast(c + 1)]; + const int nv = end - start; + const cell::type ctype = cut_mesh._types[static_cast(c)]; + + if (ctype == cell::type::point + || ctype == cell::type::interval + || ctype == cell::type::triangle + || ctype == cell::type::tetrahedron) + { + for (int k = start; k < end; ++k) + vtk_connectivity.push_back(cut_mesh._connectivity[static_cast(k)]); + } + else + { + const auto perm = cell::basix_to_vtk_vertex_permutation(ctype); + if (static_cast(perm.size()) != nv) + throw std::runtime_error("write_vtk(CutMesh): cell vertex count mismatch"); + for (int j = 0; j < nv; ++j) + vtk_connectivity.push_back( + cut_mesh._connectivity[static_cast( + start + perm[static_cast(j)])]); + } + } + + // write_vtk low-level function accepts only double coords; convert if needed. + std::vector coords_d(cut_mesh._vertex_coords.begin(), + cut_mesh._vertex_coords.end()); + write_vtk( + filename, + std::span(coords_d.data(), coords_d.size()), + std::span(vtk_connectivity.data(), vtk_connectivity.size()), + std::span(cut_mesh._offset.data(), cut_mesh._offset.size()), + std::span(const_cast(cut_mesh._types.data()), + cut_mesh._types.size()), + gdim); + } + void write_level_set_vtu(std::string filename, const cutcells::LevelSetFunction& ls, std::string field_name = "phi"); diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 85d567c..38259ea 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -1829,15 +1829,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) bool triangulate) { nb::gil_scoped_release release; auto vis = part_visualization_mesh(self, mode, triangulate); - std::vector coords( - vis._vertex_coords.begin(), vis._vertex_coords.end()); - io::write_vtk( - filename, - std::span(coords.data(), coords.size()), - std::span(vis._connectivity.data(), vis._connectivity.size()), - std::span(vis._offset.data(), vis._offset.size()), - std::span(vis._types.data(), vis._types.size()), - vis._gdim); + io::write_vtk(filename, vis); }, nb::arg("filename"), nb::arg("mode") = "full", From a1a05b53acbc01719fb801b074bb77fb2325480d Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Fri, 17 Apr 2026 19:14:14 +0200 Subject: [PATCH 10/23] some simplifications, numbering change --- cpp/src/ho_mesh_part_output.cpp | 12 ++++++--- python/cutcells/wrapper.cpp | 47 ++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index 2648ca4..e08f8cc 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -355,12 +355,16 @@ void append_mesh_entity(mesh::CutMesh& out, if (out._tdim == 0) out._tdim = cell::get_tdim(cell_type); - std::vector vtk_coords; + // CutMesh stores vertex coordinates in basix ordering internally. + // Basix-ordered inputs are stored as-is; VTK-ordered inputs are + // permuted to basix ordering before storage. + std::vector reordered_coords; std::span output_coords = physical_coords; - if (input_is_basix && !is_simplex(cell_type)) + if (!input_is_basix && !is_simplex(cell_type)) { - vtk_coords = reorder_vertex_coords_to_vtk(cell_type, physical_coords, gdim); - output_coords = std::span(vtk_coords.data(), vtk_coords.size()); + const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); + reordered_coords = cell::permute_vertex_data(physical_coords, gdim, perm); + output_coords = std::span(reordered_coords.data(), reordered_coords.size()); } const int nv = static_cast(output_coords.size()) / gdim; diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 38259ea..4710e17 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -134,6 +134,44 @@ static std::vector csr_to_vtk_cells_impl(std::span connectivity, return out; } +/// Variant of csr_to_vtk_cells_impl for CutMesh data stored in basix ordering. +/// Non-simplex cells are permuted from basix to VTK vertex ordering. +static std::vector csr_to_vtk_cells_basix_impl( + std::span connectivity, + std::span offsets, + std::span types) +{ + std::vector out; + if (offsets.size() == 0) + return out; + const std::size_t ncells = offsets.size() - 1; + out.reserve(connectivity.size() + ncells); + for (std::size_t i = 0; i < ncells; ++i) + { + const int start = offsets[i]; + const int end = offsets[i + 1]; + const int n = end - start; + out.push_back(n); + const cutcells::cell::type ctype = types[i]; + if (ctype == cutcells::cell::type::point + || ctype == cutcells::cell::type::interval + || ctype == cutcells::cell::type::triangle + || ctype == cutcells::cell::type::tetrahedron) + { + for (int j = start; j < end; ++j) + out.push_back(connectivity[static_cast(j)]); + } + else + { + const auto perm = cutcells::cell::basix_to_vtk_vertex_permutation(ctype); + for (int j = 0; j < n; ++j) + out.push_back(connectivity[static_cast( + start + perm[static_cast(j)])]); + } + } + return out; +} + template cutcells::MeshView make_mesh_view_from_numpy( const ndarray2& coordinates, @@ -1473,13 +1511,14 @@ void declare_float(nb::module_& m, std::string type) .def_prop_ro( "cells", [](const mesh::CutMesh& self) { - return as_nbarray(csr_to_vtk_cells_impl( + return as_nbarray(csr_to_vtk_cells_basix_impl( std::span(self._connectivity.data(), self._connectivity.size()), - std::span(self._offset.data(), self._offset.size()))); + std::span(self._offset.data(), self._offset.size()), + std::span(self._types.data(), self._types.size()))); }, nb::rv_policy::move, - "Convenience packed cells view [n, v0, ...] for VTK-style consumers (allocates)." - " For high-performance workflows, prefer zero-copy 'connectivity' + 'offset'.") + "Packed cells view [n, v0, ...] with basix-to-VTK vertex permutation applied." + " Suitable for pv.UnstructuredGrid. Allocates on each call.") .def_prop_ro( "parent_map", [](const mesh::CutMesh& self) { From 06b6c04a21de106ad1472920a17c457d15f17b61 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Wed, 22 Apr 2026 18:23:15 +0200 Subject: [PATCH 11/23] introducing new midpoint on uncut edges splitting scheme to make subtriangulation quasi normal to interface --- cpp/src/CMakeLists.txt | 4 + cpp/src/cut_tetrahedron.cpp | 98 +++- cpp/src/cut_triangle.cpp | 80 ++- cpp/src/ho_mesh_part_output.cpp | 320 ++++++++++- cpp/src/prism_midpoint_split.cpp | 115 ++++ cpp/src/prism_midpoint_split.h | 428 ++++++++++++++ cpp/src/quad_midpoint_split.cpp | 100 ++++ cpp/src/quad_midpoint_split.h | 128 +++++ cpp/src/triangulation.h | 45 +- python/cutcells/__init__.py | 12 + .../demo/demo_meshview_ho_cut_triangulated.py | 526 ++++++++++++++++++ python/tests/test_ho_part_straight_output.py | 111 ++++ .../test_tetra_triangulated_cut_edges.py | 185 ++++++ .../test_tetra_triangulation_analysis.py | 48 ++ python/tests/test_triangle.py | 24 +- 15 files changed, 2142 insertions(+), 82 deletions(-) create mode 100644 cpp/src/prism_midpoint_split.cpp create mode 100644 cpp/src/prism_midpoint_split.h create mode 100644 cpp/src/quad_midpoint_split.cpp create mode 100644 cpp/src/quad_midpoint_split.h create mode 100644 python/demo/demo_meshview_ho_cut_triangulated.py create mode 100644 python/tests/test_tetra_triangulated_cut_edges.py create mode 100644 python/tests/test_tetra_triangulation_analysis.py diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 214bcf9..3b6f073 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -20,6 +20,8 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/cut_quadrilateral.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_hexahedron.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_prism.h + ${CMAKE_CURRENT_SOURCE_DIR}/quad_midpoint_split.h + ${CMAKE_CURRENT_SOURCE_DIR}/prism_midpoint_split.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_pyramid.h ${CMAKE_CURRENT_SOURCE_DIR}/cut_tetrahedron.h ${CMAKE_CURRENT_SOURCE_DIR}/triangulation.h @@ -51,6 +53,8 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cut_quadrilateral.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_hexahedron.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_prism.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/quad_midpoint_split.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/prism_midpoint_split.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_pyramid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_tetrahedron.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cut_cell.cpp diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index f676970..a01b925 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -7,6 +7,8 @@ #include "cut_tetrahedron.h" #include "cut_interval.h" #include "cell_flags.h" +#include "reference_cell.h" +#include "prism_midpoint_split.h" #include "triangulation.h" #include "span_math.h" #include "utils.h" @@ -159,7 +161,16 @@ namespace tetrahedron{ if(cell_type == type::prism && triangulate == true) { - num_sub_elements = 3; + int num_roots = 0; + for (int i = 0; i < 6; ++i) + num_roots += tetrahedron_sub_element[flag][i] < 100 ? 1 : 0; + + if (num_roots == 3) + num_sub_elements = 7; + else if (num_roots == 4) + num_sub_elements = 4; + else + throw std::runtime_error("tetrahedron::get_num_sub_elements invalid prism family"); } return num_sub_elements; } @@ -278,6 +289,60 @@ namespace tetrahedron{ cutcells::cell::clear_cell_topology(cut_cell); const cut_type cut_kind = string_to_cut_type(cut_type_str); + auto append_midpoint_split_prism = [&](int flag) + { + std::array prism_tokens = {}; + for (int j = 0; j < 6; ++j) + prism_tokens[j] = tetrahedron_sub_element[flag][j]; + + std::vector prism_vertex_coords(static_cast(6 * gdim)); + for (int local_id = 0; local_id < 6; ++local_id) + { + const int token = prism_tokens[static_cast(local_id)]; + const int vertex_id = vertex_case_map[token]; + if (vertex_id < 0) + throw std::runtime_error("tetrahedron::create_cut_cell missing prism vertex token"); + + for (int d = 0; d < gdim; ++d) + { + prism_vertex_coords[static_cast(local_id * gdim + d)] + = cut_cell._vertex_coords[static_cast(vertex_id * gdim + d)]; + } + } + + int next_token_base = 200; + auto split = cutcells::cell::prism_midpoint::split_tetra_derived_prism( + std::span(prism_vertex_coords.data(), prism_vertex_coords.size()), + gdim, + std::span(prism_tokens.data(), prism_tokens.size()), + next_token_base); + + const int old_num_vertices = static_cast(cut_cell._vertex_coords.size() / gdim); + cut_cell._vertex_coords.insert( + cut_cell._vertex_coords.end(), + split.added_vertex_coords.begin(), + split.added_vertex_coords.end()); + + for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) + { + const int token = split.added_vertex_tokens[i]; + if (token < 0 || token >= static_cast(vertex_case_map.size())) + throw std::runtime_error("tetrahedron::create_cut_cell midpoint token out of bounds"); + vertex_case_map[token] = old_num_vertices + static_cast(i); + } + + for (const auto& tet_tokens : split.tets) + { + std::array t = { + vertex_case_map[tet_tokens[0]], + vertex_case_map[tet_tokens[1]], + vertex_case_map[tet_tokens[2]], + vertex_case_map[tet_tokens[3]], + }; + cutcells::cell::append_cell(cut_cell, type::tetrahedron, t, 4); + } + }; + if(cut_kind == cut_type::phieq0) { cut_cell._tdim = 2; @@ -293,8 +358,13 @@ namespace tetrahedron{ type iface_type = interface_sub_element_cell_types[flag_interior]; if(iface_type == type::quadrilateral && triangulate) { + std::array iface_tokens; + for (int i = 0; i < 4; ++i) + iface_tokens[i] = tetrahedron_intersected_edges[flag_interior][i]; + cell::reorder_subcell_vertices_from_vtk_to_basix( + iface_type, iface_tokens, 4); std::vector> triangles; - triangulation(iface_type, tetrahedron_intersected_edges[flag_interior], triangles); + triangulation(iface_type, iface_tokens.data(), triangles); for(int i = 0; i < static_cast(triangles.size()); ++i) { std::array t; @@ -323,17 +393,7 @@ namespace tetrahedron{ type sub_cell_type = tetrahedron_sub_element_cell_types[flag_interior]; if(sub_cell_type == type::prism && triangulate) { - std::vector> tets; - triangulation(sub_cell_type, tetrahedron_sub_element[flag_interior], tets); - for(int i = 0; i < static_cast(tets.size()); ++i) - { - std::array t; - t[0] = vertex_case_map[tets[i][0]]; - t[1] = vertex_case_map[tets[i][1]]; - t[2] = vertex_case_map[tets[i][2]]; - t[3] = vertex_case_map[tets[i][3]]; - cutcells::cell::append_cell(cut_cell, type::tetrahedron, t, 4); - } + append_midpoint_split_prism(flag_interior); } else { @@ -354,17 +414,7 @@ namespace tetrahedron{ type sub_cell_type = tetrahedron_sub_element_cell_types[flag_exterior]; if(sub_cell_type == type::prism && triangulate) { - std::vector> tets; - triangulation(sub_cell_type, tetrahedron_sub_element[flag_exterior], tets); - for(int i = 0; i < static_cast(tets.size()); ++i) - { - std::array t; - t[0] = vertex_case_map[tets[i][0]]; - t[1] = vertex_case_map[tets[i][1]]; - t[2] = vertex_case_map[tets[i][2]]; - t[3] = vertex_case_map[tets[i][3]]; - cutcells::cell::append_cell(cut_cell, type::tetrahedron, t, 4); - } + append_midpoint_split_prism(flag_exterior); } else { diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index 6eff2cb..7f32ca4 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -7,6 +7,8 @@ #include "cut_triangle.h" #include "cut_interval.h" #include "cell_flags.h" +#include "quad_midpoint_split.h" +#include "reference_cell.h" #include "triangulation.h" #include "span_math.h" #include "utils.h" @@ -93,7 +95,7 @@ namespace triangle{ if(cell_type == type::quadrilateral && triangulate == true) { - num_sub_elements = 2; + num_sub_elements = 3; } return num_sub_elements; } @@ -195,6 +197,59 @@ namespace triangle{ cutcells::cell::clear_cell_topology(cut_cell); const cut_type cut_kind = string_to_cut_type(cut_type_str); + auto append_midpoint_split_quad = [&](int flag) + { + std::array quad_tokens = {}; + for (int j = 0; j < 4; ++j) + quad_tokens[j] = triangle_sub_element[flag][j]; + + std::vector quad_vertex_coords(static_cast(4 * gdim)); + for (int local_id = 0; local_id < 4; ++local_id) + { + const int token = quad_tokens[static_cast(local_id)]; + const int vertex_id = vertex_case_map[token]; + if (vertex_id < 0) + throw std::runtime_error("triangle::create_cut_cell missing quadrilateral vertex token"); + + for (int d = 0; d < gdim; ++d) + { + quad_vertex_coords[static_cast(local_id * gdim + d)] + = cut_cell._vertex_coords[static_cast(vertex_id * gdim + d)]; + } + } + + int next_token_base = 200; + auto split = cutcells::cell::quad_midpoint::split_triangle_derived_quadrilateral( + std::span(quad_vertex_coords.data(), quad_vertex_coords.size()), + gdim, + std::span(quad_tokens.data(), quad_tokens.size()), + next_token_base); + + const int old_num_vertices = static_cast(cut_cell._vertex_coords.size() / gdim); + cut_cell._vertex_coords.insert( + cut_cell._vertex_coords.end(), + split.added_vertex_coords.begin(), + split.added_vertex_coords.end()); + + for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) + { + const int token = split.added_vertex_tokens[i]; + if (token < 0 || token >= static_cast(vertex_case_map.size())) + throw std::runtime_error("triangle::create_cut_cell midpoint token out of bounds"); + vertex_case_map[token] = old_num_vertices + static_cast(i); + } + + for (const auto& tri_tokens : split.triangles) + { + std::array t = { + vertex_case_map[tri_tokens[0]], + vertex_case_map[tri_tokens[1]], + vertex_case_map[tri_tokens[2]], + }; + cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); + } + }; + if(cut_kind == cut_type::phieq0) { cut_cell._tdim = 1; @@ -220,17 +275,7 @@ namespace triangle{ type sub_cell_type = triangle_sub_element_cell_types[flag_interior]; if(sub_cell_type == type::quadrilateral && triangulate) { - // Triangulate the quad: produce two triangles via triangulation helper - std::vector> triangles; - triangulation(sub_cell_type, triangle_sub_element[flag_interior], triangles); - for(int i = 0; i < static_cast(triangles.size()); ++i) - { - std::array t; - t[0] = vertex_case_map[triangles[i][0]]; - t[1] = vertex_case_map[triangles[i][1]]; - t[2] = vertex_case_map[triangles[i][2]]; - cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); - } + append_midpoint_split_quad(flag_interior); } else { @@ -251,16 +296,7 @@ namespace triangle{ type sub_cell_type = triangle_sub_element_cell_types[flag_exterior]; if(sub_cell_type == type::quadrilateral && triangulate) { - std::vector> triangles; - triangulation(sub_cell_type, triangle_sub_element[flag_exterior], triangles); - for(int i = 0; i < static_cast(triangles.size()); ++i) - { - std::array t; - t[0] = vertex_case_map[triangles[i][0]]; - t[1] = vertex_case_map[triangles[i][1]]; - t[2] = vertex_case_map[triangles[i][2]]; - cutcells::cell::append_cell(cut_cell, type::triangle, t, 3); - } + append_midpoint_split_quad(flag_exterior); } else { diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index e08f8cc..cd77d15 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -9,6 +9,8 @@ #include "quadrature_tables.h" #include "reference_cell.h" #include "cell_topology.h" +#include "quad_midpoint_split.h" +#include "prism_midpoint_split.h" #include "triangulation.h" #include @@ -348,7 +350,9 @@ void append_mesh_entity(mesh::CutMesh& out, cell::type cell_type, int parent_cell_id, bool triangulate, - bool input_is_basix) + bool input_is_basix, + cell::type source_parent_type, + std::span root_vertex_flags) { if (out._gdim == 0) out._gdim = gdim; @@ -369,6 +373,105 @@ void append_mesh_entity(mesh::CutMesh& out, const int nv = static_cast(output_coords.size()) / gdim; const int vertex_base = out._num_vertices; + + if (triangulate + && cell_type == cell::type::quadrilateral + && source_parent_type == cell::type::triangle + && root_vertex_flags.size() == static_cast(nv)) + { + std::array quad_tokens = {}; + for (int i = 0; i < nv; ++i) + { + quad_tokens[static_cast(i)] + = root_vertex_flags[static_cast(i)] ? i : 100 + i; + } + + int next_token_base = 200; + auto split = cell::quad_midpoint::split_triangle_derived_quadrilateral( + output_coords, + gdim, + std::span(quad_tokens.data(), quad_tokens.size()), + next_token_base); + + out._vertex_coords.insert( + out._vertex_coords.end(), output_coords.begin(), output_coords.end()); + out._num_vertices += nv; + + const int midpoint_vertex_base = out._num_vertices; + out._vertex_coords.insert( + out._vertex_coords.end(), + split.added_vertex_coords.begin(), + split.added_vertex_coords.end()); + out._num_vertices += static_cast(split.added_vertex_tokens.size()); + + std::array token_to_vertex; + token_to_vertex.fill(-1); + for (int i = 0; i < nv; ++i) + token_to_vertex[quad_tokens[static_cast(i)]] = vertex_base + i; + for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) + { + token_to_vertex[split.added_vertex_tokens[i]] + = midpoint_vertex_base + static_cast(i); + } + + for (const auto& tri_tokens : split.triangles) + { + for (int k = 0; k < 3; ++k) + out._connectivity.push_back(token_to_vertex[tri_tokens[static_cast(k)]]); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(cell::type::triangle); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; + } + return; + } + + if (triangulate + && cell_type == cell::type::prism + && source_parent_type == cell::type::tetrahedron + && root_vertex_flags.size() == static_cast(nv)) + { + std::array prism_tokens = {}; + for (int i = 0; i < nv; ++i) + prism_tokens[static_cast(i)] = root_vertex_flags[static_cast(i)] ? i : 100 + i; + + int next_token_base = 200; + auto split = cell::prism_midpoint::split_tetra_derived_prism( + output_coords, + gdim, + std::span(prism_tokens.data(), prism_tokens.size()), + next_token_base); + + out._vertex_coords.insert( + out._vertex_coords.end(), output_coords.begin(), output_coords.end()); + out._num_vertices += nv; + + const int midpoint_vertex_base = out._num_vertices; + out._vertex_coords.insert( + out._vertex_coords.end(), + split.added_vertex_coords.begin(), + split.added_vertex_coords.end()); + out._num_vertices += static_cast(split.added_vertex_tokens.size()); + + std::array token_to_vertex; + token_to_vertex.fill(-1); + for (int i = 0; i < nv; ++i) + token_to_vertex[prism_tokens[static_cast(i)]] = vertex_base + i; + for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) + token_to_vertex[split.added_vertex_tokens[i]] = midpoint_vertex_base + static_cast(i); + + for (const auto& tet_tokens : split.tets) + { + for (int k = 0; k < 4; ++k) + out._connectivity.push_back(token_to_vertex[tet_tokens[static_cast(k)]]); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(cell::type::tetrahedron); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; + } + return; + } + out._vertex_coords.insert( out._vertex_coords.end(), output_coords.begin(), output_coords.end()); out._num_vertices += nv; @@ -444,29 +547,192 @@ void append_entity_quadrature(quadrature::QuadratureRules& rules, int parent_cell_id, int order, bool triangulate, - bool input_is_basix) + bool input_is_basix, + cell::type source_parent_type, + std::span root_vertex_flags) { if (rules._tdim == 0) rules._tdim = parent_tdim; if (rules._offset.empty()) rules._offset.push_back(0); - std::vector ref_vertices_vtk; - std::vector phys_vertices_vtk; std::span ref_use = ref_vertices; std::span phys_use = physical_vertices; - if (input_is_basix && !is_simplex(cell_type)) + std::vector ref_vertices_basix; + std::vector phys_vertices_basix; + if (triangulate && !input_is_basix && !is_simplex(cell_type)) { - ref_vertices_vtk = reorder_vertex_coords_to_vtk(cell_type, ref_vertices, parent_tdim); - phys_vertices_vtk = reorder_vertex_coords_to_vtk(cell_type, physical_vertices, gdim); - ref_use = std::span(ref_vertices_vtk.data(), ref_vertices_vtk.size()); - phys_use = std::span(phys_vertices_vtk.data(), phys_vertices_vtk.size()); + const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); + ref_vertices_basix = cell::permute_vertex_data(ref_vertices, parent_tdim, perm); + phys_vertices_basix = cell::permute_vertex_data(physical_vertices, gdim, perm); + ref_use = std::span(ref_vertices_basix.data(), ref_vertices_basix.size()); + phys_use = std::span(phys_vertices_basix.data(), phys_vertices_basix.size()); } const int entity_dim = cell::get_tdim(cell_type); + const int nv = static_cast(ref_use.size()) / parent_tdim; + + if (triangulate + && cell_type == cell::type::quadrilateral + && source_parent_type == cell::type::triangle + && root_vertex_flags.size() == static_cast(nv)) + { + std::array quad_tokens = {}; + for (int i = 0; i < nv; ++i) + { + quad_tokens[static_cast(i)] + = root_vertex_flags[static_cast(i)] ? i : 100 + i; + } + + int next_ref_token = 200; + auto ref_split = cell::quad_midpoint::split_triangle_derived_quadrilateral( + ref_use, + parent_tdim, + std::span(quad_tokens.data(), quad_tokens.size()), + next_ref_token); + + int next_phys_token = 200; + auto phys_split = cell::quad_midpoint::split_triangle_derived_quadrilateral( + phys_use, + gdim, + std::span(quad_tokens.data(), quad_tokens.size()), + next_phys_token); + + std::array token_to_local; + token_to_local.fill(-1); + for (int i = 0; i < nv; ++i) + token_to_local[quad_tokens[static_cast(i)]] = i; + for (std::size_t i = 0; i < ref_split.added_vertex_tokens.size(); ++i) + token_to_local[ref_split.added_vertex_tokens[i]] = nv + static_cast(i); + + std::vector ref_all(ref_use.begin(), ref_use.end()); + ref_all.insert( + ref_all.end(), + ref_split.added_vertex_coords.begin(), + ref_split.added_vertex_coords.end()); + + std::vector phys_all(phys_use.begin(), phys_use.end()); + phys_all.insert( + phys_all.end(), + phys_split.added_vertex_coords.begin(), + phys_split.added_vertex_coords.end()); + + std::vector ref_simplex; + std::vector phys_simplex; + for (const auto& tri_tokens : ref_split.triangles) + { + const std::array local_ids = { + token_to_local[tri_tokens[0]], + token_to_local[tri_tokens[1]], + token_to_local[tri_tokens[2]], + }; + gather_subcell_vertices( + std::span(ref_all.data(), ref_all.size()), + parent_tdim, + std::span(local_ids.data(), local_ids.size()), + ref_simplex); + gather_subcell_vertices( + std::span(phys_all.data(), phys_all.size()), + gdim, + std::span(local_ids.data(), local_ids.size()), + phys_simplex); + append_simplex_quadrature( + rules, + cell::type::triangle, + std::span(ref_simplex.data(), ref_simplex.size()), + std::span(phys_simplex.data(), phys_simplex.size()), + parent_tdim, + gdim, + order); + } + + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); + return; + } + + if (triangulate + && cell_type == cell::type::prism + && source_parent_type == cell::type::tetrahedron + && root_vertex_flags.size() == static_cast(nv)) + { + std::array prism_tokens = {}; + for (int i = 0; i < nv; ++i) + { + prism_tokens[static_cast(i)] + = root_vertex_flags[static_cast(i)] ? i : 100 + i; + } + + int next_ref_token = 200; + auto ref_split = cell::prism_midpoint::split_tetra_derived_prism( + ref_use, + parent_tdim, + std::span(prism_tokens.data(), prism_tokens.size()), + next_ref_token); + + int next_phys_token = 200; + auto phys_split = cell::prism_midpoint::split_tetra_derived_prism( + phys_use, + gdim, + std::span(prism_tokens.data(), prism_tokens.size()), + next_phys_token); + + std::array token_to_local; + token_to_local.fill(-1); + for (int i = 0; i < nv; ++i) + token_to_local[prism_tokens[static_cast(i)]] = i; + for (std::size_t i = 0; i < ref_split.added_vertex_tokens.size(); ++i) + token_to_local[ref_split.added_vertex_tokens[i]] = nv + static_cast(i); + + std::vector ref_all(ref_use.begin(), ref_use.end()); + ref_all.insert( + ref_all.end(), + ref_split.added_vertex_coords.begin(), + ref_split.added_vertex_coords.end()); + + std::vector phys_all(phys_use.begin(), phys_use.end()); + phys_all.insert( + phys_all.end(), + phys_split.added_vertex_coords.begin(), + phys_split.added_vertex_coords.end()); + + std::vector ref_simplex; + std::vector phys_simplex; + for (const auto& tet_tokens : ref_split.tets) + { + const std::array local_ids = { + token_to_local[tet_tokens[0]], + token_to_local[tet_tokens[1]], + token_to_local[tet_tokens[2]], + token_to_local[tet_tokens[3]], + }; + gather_subcell_vertices( + std::span(ref_all.data(), ref_all.size()), + parent_tdim, + std::span(local_ids.data(), local_ids.size()), + ref_simplex); + gather_subcell_vertices( + std::span(phys_all.data(), phys_all.size()), + gdim, + std::span(local_ids.data(), local_ids.size()), + phys_simplex); + append_simplex_quadrature( + rules, + cell::type::tetrahedron, + std::span(ref_simplex.data(), ref_simplex.size()), + std::span(phys_simplex.data(), phys_simplex.size()), + parent_tdim, + gdim, + order); + } + + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); + return; + } + if (triangulate && !is_simplex(cell_type)) { - const int nv = static_cast(ref_use.size()) / parent_tdim; std::vector local_ids(static_cast(nv)); std::iota(local_ids.begin(), local_ids.end(), 0); @@ -542,6 +808,24 @@ void append_cut_entities(mesh::CutMesh& out, for (const auto& entity : entities) { + std::vector root_vertex_flags; + if ((entity.type == cell::type::prism + && adapt_cell.parent_cell_type == cell::type::tetrahedron) + || (entity.type == cell::type::quadrilateral + && adapt_cell.parent_cell_type == cell::type::triangle)) + { + root_vertex_flags.resize(entity.vertices.size(), 0); + for (std::size_t j = 0; j < entity.vertices.size(); ++j) + { + root_vertex_flags[j] = vertex_is_zero_for_level_set( + adapt_cell, + entity.vertices[j], + /*level_set_id=*/0) + ? 1 + : 0; + } + } + const auto ref_coords = entity_reference_coords( adapt_cell, std::span(entity.vertices.data(), entity.vertices.size())); const auto phys_coords = cell::push_forward_affine_map( @@ -557,7 +841,9 @@ void append_cut_entities(mesh::CutMesh& out, entity.type, static_cast(parent_cell_id), triangulate, - /*input_is_basix=*/true); + /*input_is_basix=*/true, + adapt_cell.parent_cell_type, + std::span(root_vertex_flags.data(), root_vertex_flags.size())); if (rules != nullptr) { @@ -571,7 +857,9 @@ void append_cut_entities(mesh::CutMesh& out, static_cast(parent_cell_id), quadrature_order, triangulate || !is_simplex(entity.type), - /*input_is_basix=*/true); + /*input_is_basix=*/true, + adapt_cell.parent_cell_type, + std::span(root_vertex_flags.data(), root_vertex_flags.size())); } } } @@ -599,7 +887,9 @@ void append_uncut_volume_cells(mesh::CutMesh& out, ctype, static_cast(cell_id), triangulate, - /*input_is_basix=*/false); + /*input_is_basix=*/false, + cell::type::point, + std::span()); if (rules != nullptr) { @@ -614,7 +904,9 @@ void append_uncut_volume_cells(mesh::CutMesh& out, static_cast(cell_id), quadrature_order, triangulate, - /*input_is_basix=*/false); + /*input_is_basix=*/false, + cell::type::point, + std::span()); } } } diff --git a/cpp/src/prism_midpoint_split.cpp b/cpp/src/prism_midpoint_split.cpp new file mode 100644 index 0000000..f6a10d5 --- /dev/null +++ b/cpp/src/prism_midpoint_split.cpp @@ -0,0 +1,115 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT + +#include "prism_midpoint_split.h" + +#include +#include + +namespace cutcells::cell::prism_midpoint +{ + +namespace +{ + +constexpr std::array prism_edge_list = {{ + {0, 1}, {1, 2}, {2, 0}, + {3, 4}, {4, 5}, {5, 3}, + {0, 3}, {1, 4}, {2, 5}, +}}; + +[[noreturn]] void throw_invalid_prism_tokens(std::span prism_tokens, + const std::string& message) +{ + std::string full = "prism_midpoint_split: " + message + " tokens=["; + for (std::size_t i = 0; i < prism_tokens.size(); ++i) + { + if (i > 0) + full += ","; + full += std::to_string(prism_tokens[i]); + } + full += "]"; + throw std::runtime_error(full); +} + +} // namespace + +bool is_root_token(int token) +{ + return token >= 0 && token < 100; +} + +std::span prism_edges() +{ + return std::span(prism_edge_list.data(), prism_edge_list.size()); +} + +family classify_family(std::span prism_tokens) +{ + if (prism_tokens.size() != 6) + throw std::invalid_argument("classify_family: expected 6 prism tokens"); + + int num_roots = 0; + for (const int token : prism_tokens) + num_roots += is_root_token(token) ? 1 : 0; + + if (num_roots == 3) + return family::roots3; + if (num_roots == 4) + return family::roots4; + + throw_invalid_prism_tokens( + prism_tokens, + "tetra-derived prism must contain exactly 3 or 4 root vertices"); +} + +PrismRoleAnalysis analyze_tetra_derived_prism(std::span prism_tokens) +{ + if (prism_tokens.size() != 6) + throw std::invalid_argument("analyze_tetra_derived_prism: expected 6 prism tokens"); + + PrismRoleAnalysis out; + out.split_family = classify_family(prism_tokens); + + for (int local_id = 0; local_id < 6; ++local_id) + { + const int token = prism_tokens[static_cast(local_id)]; + if (is_root_token(token)) + { + const std::size_t idx = static_cast(out.num_roots); + out.root_local_ids[idx] = local_id; + ++out.num_roots; + } + else + { + const std::size_t idx = static_cast(out.num_non_roots); + out.non_root_local_ids[idx] = local_id; + ++out.num_non_roots; + } + } + + const auto edges = prism_edges(); + for (const LocalEdge edge : edges) + { + const bool a_root = is_root_token(prism_tokens[static_cast(edge.a)]); + const bool b_root = is_root_token(prism_tokens[static_cast(edge.b)]); + if (!a_root && !b_root) + out.midpoint_edges.push_back(edge); + } + + const std::size_t expected_midpoints + = (out.split_family == family::roots3) ? std::size_t(3) : std::size_t(1); + if (out.midpoint_edges.size() != expected_midpoints) + { + throw_invalid_prism_tokens( + prism_tokens, + "unexpected number of uncut non-root prism edges for midpoint insertion"); + } + + return out; +} + +} // namespace cutcells::cell::prism_midpoint diff --git a/cpp/src/prism_midpoint_split.h b/cpp/src/prism_midpoint_split.h new file mode 100644 index 0000000..b29cb27 --- /dev/null +++ b/cpp/src/prism_midpoint_split.h @@ -0,0 +1,428 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include +#include + +namespace cutcells::cell::prism_midpoint +{ + +/// Prism family induced by cutting a tetrahedron. +/// +/// roots3: +/// The prism carries three root vertices and three original tetra vertices. +/// The quasi-normal midpoint strategy inserts one midpoint on each prism edge +/// connecting two original vertices. +/// +/// roots4: +/// The prism carries four root vertices and two original tetra vertices. +/// The quasi-normal midpoint strategy inserts one midpoint on the single prism +/// edge connecting the two original vertices. +enum class family +{ + roots3, + roots4, +}; + +/// Local prism edge with local vertex ids in the prism ordering used by the +/// tetrahedron LUT subcell definition. +struct LocalEdge +{ + int a = -1; + int b = -1; +}; + +/// Static classification of a tetra-derived prism. +struct PrismRoleAnalysis +{ + family split_family = family::roots3; + std::array root_local_ids = {-1, -1, -1, -1, -1, -1}; + int num_roots = 0; + std::array non_root_local_ids = {-1, -1, -1, -1, -1, -1}; + int num_non_roots = 0; + std::vector midpoint_edges; +}; + +/// True for tetra-cut root tokens. In the current tetra LUT these are the +/// intersection-point edge ids 0..5. Original tetra vertices use 100+vid. +bool is_root_token(int token); + +/// Prism edge list in the local ordering expected by the tetrahedron cutter and +/// by cutcells::cell::triangulation(type::prism, ...). +std::span prism_edges(); + +/// Classify a tetra-derived prism purely from its 6 tokens. +family classify_family(std::span prism_tokens); + +/// Return root/non-root local ids and the default uncut-edge midpoint placement +/// suggested by the tetra-prism analysis. +PrismRoleAnalysis analyze_tetra_derived_prism(std::span prism_tokens); + +template +struct MidpointInsertionResult +{ + PrismRoleAnalysis analysis; + std::vector added_vertex_coords; // flat coord_dim storage + std::vector added_vertex_tokens; // synthetic tokens chosen by caller + std::vector added_vertex_edges; + std::vector> tets; // token-based tetrahedra +}; + +/// Insert the default midpoint set for a tetra-derived prism. +/// +/// The returned midpoints are not yet wired into any tetra connectivity. This +/// helper exists so the new prism strategy can be implemented in a standalone +/// module first, then integrated at the prism-to-tet call sites. +/// +/// @param prism_vertex_coords flat 6*coord_dim local coordinate storage +/// @param coord_dim prism coordinate dimension, expected 3 for tetra cuts +/// @param prism_tokens 6 tokens from the tetra LUT prism subcell +/// @param next_token_base caller-owned synthetic-token counter; incremented for +/// each created midpoint +template +MidpointInsertionResult create_default_midpoints( + std::span prism_vertex_coords, + int coord_dim, + std::span prism_tokens, + int& next_token_base) +{ + if (coord_dim <= 0) + throw std::invalid_argument("create_default_midpoints: coord_dim must be positive"); + if (prism_vertex_coords.size() != static_cast(6 * coord_dim)) + { + throw std::invalid_argument( + "create_default_midpoints: expected 6 prism vertices in flat storage"); + } + if (prism_tokens.size() != 6) + throw std::invalid_argument("create_default_midpoints: expected 6 prism tokens"); + + MidpointInsertionResult result; + result.analysis = analyze_tetra_derived_prism(prism_tokens); + result.added_vertex_coords.reserve( + static_cast(result.analysis.midpoint_edges.size() * coord_dim)); + result.added_vertex_tokens.reserve(result.analysis.midpoint_edges.size()); + result.added_vertex_edges.reserve(result.analysis.midpoint_edges.size()); + + for (const LocalEdge edge : result.analysis.midpoint_edges) + { + result.added_vertex_tokens.push_back(next_token_base++); + result.added_vertex_edges.push_back(edge); + for (int d = 0; d < coord_dim; ++d) + { + const T xa = prism_vertex_coords[static_cast(edge.a * coord_dim + d)]; + const T xb = prism_vertex_coords[static_cast(edge.b * coord_dim + d)]; + result.added_vertex_coords.push_back(T(0.5) * (xa + xb)); + } + } + + return result; +} + +template +T signed_tet_jacobian(std::span vertex_coords, + int coord_dim, + const std::array& tet_local_ids); + +template +void orient_tet_positive(std::span vertex_coords, + int coord_dim, + std::array& tet_local_ids); + +/// Build the standalone tetrahedral split for a tetra-derived prism using the +/// default midpoint insertion pattern for the detected family. +/// +/// The returned tetrahedra are expressed in tokens: original prism tokens are +/// reused and new midpoint vertices receive synthetic tokens chosen by the +/// caller. This lets the tetrahedron cutter integrate the result by only +/// appending the new coordinates and extending its token->local-index map. +template +MidpointInsertionResult split_tetra_derived_prism( + std::span prism_vertex_coords, + int coord_dim, + std::span prism_tokens, + int& next_token_base) +{ + constexpr std::array, 6> tetra_edges = {{ + {{0, 1}}, + {{1, 2}}, + {{2, 0}}, + {{0, 3}}, + {{1, 3}}, + {{2, 3}}, + }}; + + MidpointInsertionResult result = create_default_midpoints( + prism_vertex_coords, coord_dim, prism_tokens, next_token_base); + + std::array canonical_to_actual = {0, 1, 2, 3, 4, 5}; + + auto root_local_for_original_local = [&](int non_root_local_id) -> int + { + const int original_token = prism_tokens[static_cast(non_root_local_id)]; + if (is_root_token(original_token)) + { + throw std::runtime_error( + "split_tetra_derived_prism: expected original tetra vertex token for non-root prism vertex"); + } + + const int original_vertex = original_token - 100; + for (int i = 0; i < result.analysis.num_roots; ++i) + { + const int root_local_id = result.analysis.root_local_ids[static_cast(i)]; + const int root_token = prism_tokens[static_cast(root_local_id)]; + const auto& edge = tetra_edges[static_cast(root_token)]; + if (edge[0] == original_vertex || edge[1] == original_vertex) + return root_local_id; + } + + throw std::runtime_error( + "split_tetra_derived_prism: failed to match original prism vertex to a root edge"); + }; + + if (result.analysis.split_family == family::roots3) + { + const bool bottom_non_roots + = result.analysis.num_non_roots == 3 + && result.analysis.non_root_local_ids[0] == 0 + && result.analysis.non_root_local_ids[1] == 1 + && result.analysis.non_root_local_ids[2] == 2; + const bool top_non_roots + = result.analysis.num_non_roots == 3 + && result.analysis.non_root_local_ids[0] == 3 + && result.analysis.non_root_local_ids[1] == 4 + && result.analysis.non_root_local_ids[2] == 5; + + if (bottom_non_roots) + { + canonical_to_actual = { + 0, + 1, + 2, + root_local_for_original_local(0), + root_local_for_original_local(1), + root_local_for_original_local(2), + }; + } + else if (top_non_roots) + { + canonical_to_actual = { + 3, + 4, + 5, + root_local_for_original_local(3), + root_local_for_original_local(4), + root_local_for_original_local(5), + }; + } + else + { + throw std::runtime_error( + "split_tetra_derived_prism: roots3 family expected one full prism triangle of non-root vertices"); + } + } + else + { + if (result.analysis.num_non_roots != 2) + { + throw std::runtime_error( + "split_tetra_derived_prism: roots4 family expected exactly two non-root vertices"); + } + + const int a = result.analysis.non_root_local_ids[0]; + const int b = result.analysis.non_root_local_ids[1]; + + int k = -1; + if ((a == 0 && b == 3) || (a == 3 && b == 0)) + k = 0; + else if ((a == 1 && b == 4) || (a == 4 && b == 1)) + k = 1; + else if ((a == 2 && b == 5) || (a == 5 && b == 2)) + k = 2; + + if (k < 0) + { + throw std::runtime_error( + "split_tetra_derived_prism: roots4 family expected non-root vertices on one prism vertical edge"); + } + + canonical_to_actual = { + k, + (k + 1) % 3, + (k + 2) % 3, + k + 3, + ((k + 1) % 3) + 3, + ((k + 2) % 3) + 3, + }; + } + + std::vector all_tokens(6); + for (int i = 0; i < 6; ++i) + all_tokens[static_cast(i)] = prism_tokens[static_cast(i)]; + for (int token : result.added_vertex_tokens) + all_tokens.push_back(token); + + std::vector all_coords(prism_vertex_coords.begin(), prism_vertex_coords.end()); + all_coords.insert( + all_coords.end(), + result.added_vertex_coords.begin(), + result.added_vertex_coords.end()); + + auto midpoint_local_id_for_actual_edge = [&](int actual_a, int actual_b) -> int + { + for (std::size_t i = 0; i < result.added_vertex_edges.size(); ++i) + { + const LocalEdge edge = result.added_vertex_edges[i]; + const bool matches + = (edge.a == actual_a && edge.b == actual_b) + || (edge.a == actual_b && edge.b == actual_a); + if (matches) + return 6 + static_cast(i); + } + + throw std::runtime_error( + "split_tetra_derived_prism: failed to match canonical midpoint edge"); + }; + + auto map_canonical_local_id = [&](int canonical_id) -> int + { + if (canonical_id < 6) + return canonical_to_actual[static_cast(canonical_id)]; + + if (result.analysis.split_family == family::roots3) + { + switch (canonical_id) + { + case 6: + return midpoint_local_id_for_actual_edge( + canonical_to_actual[0], canonical_to_actual[1]); + case 7: + return midpoint_local_id_for_actual_edge( + canonical_to_actual[1], canonical_to_actual[2]); + case 8: + return midpoint_local_id_for_actual_edge( + canonical_to_actual[2], canonical_to_actual[0]); + default: + break; + } + } + else + { + if (canonical_id == 6) + { + return midpoint_local_id_for_actual_edge( + canonical_to_actual[0], canonical_to_actual[3]); + } + } + + throw std::runtime_error("split_tetra_derived_prism: unsupported canonical local id"); + }; + + std::vector> canonical_tets; + if (result.analysis.split_family == family::roots3) + { + // Build the three original-vertex corner tetrahedra first. The + // remaining central cell has vertices {3,4,5,6,7,8} and is an + // octahedron, not a prism. Split it into four tetrahedra around the + // root-midpoint diagonal (5,6), which yields one central tet of the + // preferred midpoint-root-root-root form. + canonical_tets = { + std::array{0, 6, 8, 3}, + std::array{1, 7, 6, 4}, + std::array{2, 8, 7, 5}, + std::array{5, 6, 3, 4}, + std::array{5, 6, 4, 7}, + std::array{5, 6, 7, 8}, + std::array{5, 6, 8, 3}, + }; + } + else + { + // Delaunay-style split on the canonical prism with one midpoint on the + // non-root edge (0,3). + canonical_tets = { + std::array{6, 2, 1, 0}, + std::array{6, 5, 4, 3}, + std::array{6, 4, 2, 1}, + std::array{6, 5, 4, 2}, + }; + } + + result.tets.clear(); + result.tets.reserve(canonical_tets.size()); + + for (std::array tet : canonical_tets) + { + std::array local_ids = { + map_canonical_local_id(tet[0]), + map_canonical_local_id(tet[1]), + map_canonical_local_id(tet[2]), + map_canonical_local_id(tet[3]), + }; + orient_tet_positive( + std::span(all_coords.data(), all_coords.size()), + coord_dim, + local_ids); + + std::array token_tet = { + all_tokens[static_cast(local_ids[0])], + all_tokens[static_cast(local_ids[1])], + all_tokens[static_cast(local_ids[2])], + all_tokens[static_cast(local_ids[3])], + }; + result.tets.push_back(token_tet); + } + + return result; +} + +/// Signed tetra determinant in local coordinates. Positive means the tet +/// orientation is consistent with the ambient local coordinate frame. +template +T signed_tet_jacobian(std::span vertex_coords, + int coord_dim, + const std::array& tet_local_ids) +{ + if (coord_dim != 3) + throw std::invalid_argument("signed_tet_jacobian: coord_dim must be 3"); + + auto coord = [&](int local_id, int d) -> T + { + return vertex_coords[static_cast(local_id * coord_dim + d)]; + }; + + const T ax = coord(tet_local_ids[1], 0) - coord(tet_local_ids[0], 0); + const T ay = coord(tet_local_ids[1], 1) - coord(tet_local_ids[0], 1); + const T az = coord(tet_local_ids[1], 2) - coord(tet_local_ids[0], 2); + + const T bx = coord(tet_local_ids[2], 0) - coord(tet_local_ids[0], 0); + const T by = coord(tet_local_ids[2], 1) - coord(tet_local_ids[0], 1); + const T bz = coord(tet_local_ids[2], 2) - coord(tet_local_ids[0], 2); + + const T cx = coord(tet_local_ids[3], 0) - coord(tet_local_ids[0], 0); + const T cy = coord(tet_local_ids[3], 1) - coord(tet_local_ids[0], 1); + const T cz = coord(tet_local_ids[3], 2) - coord(tet_local_ids[0], 2); + + return ax * (by * cz - bz * cy) + - ay * (bx * cz - bz * cx) + + az * (bx * cy - by * cx); +} + +/// Flip the last two vertices if needed so the tetra has non-negative +/// orientation in the local coordinate frame. +template +void orient_tet_positive(std::span vertex_coords, + int coord_dim, + std::array& tet_local_ids) +{ + if (signed_tet_jacobian(vertex_coords, coord_dim, tet_local_ids) < T(0)) + std::swap(tet_local_ids[2], tet_local_ids[3]); +} + +} // namespace cutcells::cell::prism_midpoint diff --git a/cpp/src/quad_midpoint_split.cpp b/cpp/src/quad_midpoint_split.cpp new file mode 100644 index 0000000..8241864 --- /dev/null +++ b/cpp/src/quad_midpoint_split.cpp @@ -0,0 +1,100 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT + +#include "quad_midpoint_split.h" + +#include + +namespace cutcells::cell::quad_midpoint +{ + +namespace +{ + +[[noreturn]] void throw_invalid_quad_tokens(std::span quad_tokens, + const std::string& message) +{ + std::string full = "quad_midpoint_split: " + message + " tokens=["; + for (std::size_t i = 0; i < quad_tokens.size(); ++i) + { + if (i > 0) + full += ","; + full += std::to_string(quad_tokens[i]); + } + full += "]"; + throw std::runtime_error(full); +} + +} // namespace + +bool is_root_token(int token) +{ + return token >= 0 && token < 100; +} + +QuadRoleAnalysis analyze_triangle_derived_quadrilateral( + std::span quad_tokens) +{ + if (quad_tokens.size() != 4) + { + throw std::invalid_argument( + "analyze_triangle_derived_quadrilateral: expected 4 quadrilateral tokens"); + } + + QuadRoleAnalysis out; + + for (int local_id = 0; local_id < 4; ++local_id) + { + const int token = quad_tokens[static_cast(local_id)]; + if (is_root_token(token)) + { + out.root_local_ids[static_cast(out.num_roots)] = local_id; + ++out.num_roots; + } + else + { + out.non_root_local_ids[static_cast(out.num_non_roots)] = local_id; + ++out.num_non_roots; + } + } + + if (out.num_roots != 2 || out.num_non_roots != 2) + { + throw_invalid_quad_tokens( + quad_tokens, + "triangle-derived quadrilateral must contain exactly 2 root and 2 non-root vertices"); + } + + bool found_canonical_order = false; + for (int start = 0; start < 4; ++start) + { + const int v0 = start; + const int v1 = (start + 1) % 4; + const int v2 = (start + 2) % 4; + const int v3 = (start + 3) % 4; + if (is_root_token(quad_tokens[static_cast(v0)]) + && is_root_token(quad_tokens[static_cast(v1)]) + && !is_root_token(quad_tokens[static_cast(v2)]) + && !is_root_token(quad_tokens[static_cast(v3)])) + { + out.canonical_to_actual = {v0, v1, v2, v3}; + out.midpoint_edge = {v2, v3}; + found_canonical_order = true; + break; + } + } + + if (!found_canonical_order) + { + throw_invalid_quad_tokens( + quad_tokens, + "expected cyclic ordering with two consecutive root vertices and two consecutive non-root vertices"); + } + + return out; +} + +} // namespace cutcells::cell::quad_midpoint diff --git a/cpp/src/quad_midpoint_split.h b/cpp/src/quad_midpoint_split.h new file mode 100644 index 0000000..e3833ab --- /dev/null +++ b/cpp/src/quad_midpoint_split.h @@ -0,0 +1,128 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include +#include + +namespace cutcells::cell::quad_midpoint +{ + +struct LocalEdge +{ + int a = -1; + int b = -1; +}; + +struct QuadRoleAnalysis +{ + std::array root_local_ids = {-1, -1, -1, -1}; + int num_roots = 0; + std::array non_root_local_ids = {-1, -1, -1, -1}; + int num_non_roots = 0; + std::array canonical_to_actual = {0, 1, 2, 3}; + LocalEdge midpoint_edge = {-1, -1}; +}; + +bool is_root_token(int token); + +QuadRoleAnalysis analyze_triangle_derived_quadrilateral( + std::span quad_tokens); + +template +struct MidpointInsertionResult +{ + QuadRoleAnalysis analysis; + std::vector added_vertex_coords; + std::vector added_vertex_tokens; + std::vector added_vertex_edges; + std::vector> triangles; +}; + +template +MidpointInsertionResult split_triangle_derived_quadrilateral( + std::span quad_vertex_coords, + int coord_dim, + std::span quad_tokens, + int& next_token_base) +{ + if (coord_dim <= 0) + { + throw std::invalid_argument( + "split_triangle_derived_quadrilateral: coord_dim must be positive"); + } + if (quad_vertex_coords.size() != static_cast(4 * coord_dim)) + { + throw std::invalid_argument( + "split_triangle_derived_quadrilateral: expected 4 quadrilateral vertices in flat storage"); + } + if (quad_tokens.size() != 4) + { + throw std::invalid_argument( + "split_triangle_derived_quadrilateral: expected 4 quadrilateral tokens"); + } + + MidpointInsertionResult result; + result.analysis = analyze_triangle_derived_quadrilateral(quad_tokens); + result.added_vertex_tokens.push_back(next_token_base++); + result.added_vertex_edges.push_back(result.analysis.midpoint_edge); + + for (int d = 0; d < coord_dim; ++d) + { + const T xa = quad_vertex_coords[static_cast( + result.analysis.midpoint_edge.a * coord_dim + d)]; + const T xb = quad_vertex_coords[static_cast( + result.analysis.midpoint_edge.b * coord_dim + d)]; + result.added_vertex_coords.push_back(T(0.5) * (xa + xb)); + } + + auto map_canonical_local_id = [&](int canonical_id) -> int + { + if (canonical_id < 4) + return result.analysis.canonical_to_actual[static_cast(canonical_id)]; + if (canonical_id == 4) + return 4; + throw std::runtime_error( + "split_triangle_derived_quadrilateral: unsupported canonical local id"); + }; + + const std::vector all_tokens = { + quad_tokens[0], + quad_tokens[1], + quad_tokens[2], + quad_tokens[3], + result.added_vertex_tokens[0], + }; + + const std::array, 3> canonical_triangles = {{ + {2, 0, 4}, + {4, 0, 1}, + {3, 4, 1}, + }}; + + result.triangles.reserve(canonical_triangles.size()); + for (const auto& tri : canonical_triangles) + { + const std::array local_ids = { + map_canonical_local_id(tri[0]), + map_canonical_local_id(tri[1]), + map_canonical_local_id(tri[2]), + }; + + result.triangles.push_back({ + all_tokens[static_cast(local_ids[0])], + all_tokens[static_cast(local_ids[1])], + all_tokens[static_cast(local_ids[2])], + }); + } + + return result; +} + +} // namespace cutcells::cell::quad_midpoint diff --git a/cpp/src/triangulation.h b/cpp/src/triangulation.h index 5dab1cd..c4a34ad 100644 --- a/cpp/src/triangulation.h +++ b/cpp/src/triangulation.h @@ -24,33 +24,36 @@ namespace cutcells::cell /// prism → 3 tetrahedra (4 vertices each) /// pyramid → 2 tetrahedra (4 vertices each) /// - /// VTK vertex ordering is assumed for all cell types. + /// Basix vertex ordering is assumed for all cell types. inline void triangulation(const type cell_type, int* vertices, std::vector>& tris) { switch(cell_type) { case type::quadrilateral: tris.resize(2, std::vector(3)); - tris = {{vertices[0],vertices[1],vertices[2]}, {vertices[0],vertices[2],vertices[3]}}; + // Basix quad: v0=(0,0), v1=(1,0), v2=(0,1), v3=(1,1) + // Split along the true diagonal v0-v3. + tris = {{vertices[0],vertices[1],vertices[3]}, + {vertices[0],vertices[3],vertices[2]}}; break; case type::hexahedron: - // VTK hex: v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) - // v4=(0,0,1) v5=(1,0,1) v6=(1,1,1) v7=(0,1,1) + // Basix hex: v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(1,1,0) + // v4=(0,0,1) v5=(1,0,1) v6=(0,1,1) v7=(1,1,1) // - // Kuhn triangulation (6 tets, fan through the v0→v6 diagonal). + // Kuhn triangulation (6 tets, fan through the v0→v7 diagonal). // Each tet has |det J| = 1 on the unit cube → total volume = 6 × 1/6 = 1 ✓ tris.resize(6, std::vector(4)); - tris = {{vertices[0],vertices[1],vertices[2],vertices[6]}, - {vertices[0],vertices[1],vertices[5],vertices[6]}, - {vertices[0],vertices[3],vertices[2],vertices[6]}, - {vertices[0],vertices[3],vertices[7],vertices[6]}, - {vertices[0],vertices[4],vertices[5],vertices[6]}, - {vertices[0],vertices[4],vertices[7],vertices[6]}}; + tris = {{vertices[0],vertices[1],vertices[3],vertices[7]}, + {vertices[0],vertices[1],vertices[5],vertices[7]}, + {vertices[0],vertices[2],vertices[3],vertices[7]}, + {vertices[0],vertices[2],vertices[6],vertices[7]}, + {vertices[0],vertices[4],vertices[5],vertices[7]}, + {vertices[0],vertices[4],vertices[6],vertices[7]}}; break; case type::prism: - // VTK wedge: v0,v1,v2 bottom △, v3,v4,v5 top △ + // Basix prism matches VTK wedge: v0,v1,v2 bottom △, v3,v4,v5 top △ // Tetrahedron 0: { v0, v2, v1, v3 } // Tetrahedron 1: { v1, v3, v5, v4 } // Tetrahedron 2: { v1, v2, v5, v3 } @@ -61,12 +64,12 @@ namespace cutcells::cell break; case type::pyramid: - // VTK pyramid: v0=(0,0,0) v1=(1,0,0) v2=(1,1,0) v3=(0,1,0) v4=apex - // Tetrahedron 0: { v0, v1, v3, v4 } - // Tetrahedron 1: { v1, v2, v3, v4 } + // Basix pyramid: v0=(0,0,0) v1=(1,0,0) v2=(0,1,0) v3=(1,1,0) v4=apex + // Tetrahedron 0: { v0, v1, v2, v4 } + // Tetrahedron 1: { v1, v3, v2, v4 } tris.resize(2, std::vector(4)); - tris = {{vertices[0],vertices[1],vertices[3],vertices[4]}, - {vertices[1],vertices[2],vertices[3],vertices[4]}}; + tris = {{vertices[0],vertices[1],vertices[2],vertices[4]}, + {vertices[1],vertices[3],vertices[2],vertices[4]}}; break; default: @@ -85,17 +88,17 @@ namespace cutcells::cell case type::triangle: return {T(0),T(0), T(1),T(0), T(0),T(1)}; case type::quadrilateral: - return {T(0),T(0), T(1),T(0), T(1),T(1), T(0),T(1)}; + return {T(0),T(0), T(1),T(0), T(0),T(1), T(1),T(1)}; case type::tetrahedron: return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), T(0),T(0),T(1)}; case type::hexahedron: - return {T(0),T(0),T(0), T(1),T(0),T(0), T(1),T(1),T(0), T(0),T(1),T(0), - T(0),T(0),T(1), T(1),T(0),T(1), T(1),T(1),T(1), T(0),T(1),T(1)}; + return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), T(1),T(1),T(0), + T(0),T(0),T(1), T(1),T(0),T(1), T(0),T(1),T(1), T(1),T(1),T(1)}; case type::prism: return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), T(0),T(0),T(1), T(1),T(0),T(1), T(0),T(1),T(1)}; case type::pyramid: - return {T(0),T(0),T(0), T(1),T(0),T(0), T(1),T(1),T(0), T(0),T(1),T(0), T(0),T(0),T(1)}; + return {T(0),T(0),T(0), T(1),T(0),T(0), T(0),T(1),T(0), T(1),T(1),T(0), T(0),T(0),T(1)}; default: throw std::invalid_argument("canonical_vertices: unsupported cell type"); } diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 873720f..9d45eb7 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -125,3 +125,15 @@ def _load_cpp_module(): box_tetrahedron_mesh, box_hex_mesh, ) + +from .triangulation_analysis import ( + CellCase, + analyze_all_cases, + analyze_single_cell_case, + classify_new_triangulation_edges, + classify_new_triangulation_simplices, + enumerate_cases, + single_cell_level_set, + single_cell_mesh, + summarize_analysis, +) diff --git a/python/demo/demo_meshview_ho_cut_triangulated.py b/python/demo/demo_meshview_ho_cut_triangulated.py new file mode 100644 index 0000000..e6fbe1a --- /dev/null +++ b/python/demo/demo_meshview_ho_cut_triangulated.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: MeshView + create_level_set + cut(mesh, ls) triangulated straight output. + +This is the triangulated counterpart of demo_meshview_ho_cut.py: +1. build a pyvista tetrahedral mesh, +2. convert it to a MeshView, +3. build one polynomial level set with create_level_set(...), +4. call cut(mesh, ls), +5. select inside/interface/outside HOMeshPart objects, +6. triangulate the straight interface and straight cut-volume output, +7. test whether the triangulation-created edges carry a root of the level set, +8. write VTU files and plot the triangulated result in pyvista. + +The edge check is important because a triangulation diagonal that crosses the +zero level set can later matter for quadrature mappings and Jacobian signs. +""" + +from __future__ import annotations + +import argparse +from collections import Counter +from pathlib import Path + +import numpy as np + +import cutcells + + +CENTER = np.array([0.1, -0.05, 0.0], dtype=np.float64) +RADIUS = 0.55 +VTK_TETRA = 10 + +EDGE_PATTERNS = { + 5: ((0, 1), (1, 2), (2, 0)), # triangle + 9: ((0, 1), (1, 2), (2, 3), (3, 0)), # quadrilateral + 10: ((0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)), # tetrahedron + 13: ( + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 5), + (5, 3), + (0, 3), + (1, 4), + (2, 5), + ), # prism / wedge +} + +TAG_LABELS = { + int(cutcells.EdgeRootTag.no_root.value): "no_root", + int(cutcells.EdgeRootTag.one_root.value): "one_root", + int(cutcells.EdgeRootTag.multiple_roots.value): "multiple_roots", + int(cutcells.EdgeRootTag.zero.value): "zero", +} + + +def phi_batch(X: np.ndarray) -> np.ndarray: + return np.sqrt( + (X[0] - CENTER[0]) ** 2 + + (X[1] - CENTER[1]) ** 2 + + (X[2] - CENTER[2]) ** 2 + ) - RADIUS + + +def structured_tetra_mesh(x0, y0, z0, x1, y1, z1, nx, ny, nz): + xs = np.linspace(x0, x1, num=nx) + ys = np.linspace(y0, y1, num=ny) + zs = np.linspace(z0, z1, num=nz) + xx, yy, zz = np.meshgrid(xs, ys, zs, indexing="ij") + points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()].astype(np.float64, copy=False) + + def vid(i: int, j: int, k: int) -> int: + return i + nx * (j + ny * k) + + tet_list = [] + for k in range(nz - 1): + for j in range(ny - 1): + for i in range(nx - 1): + v000 = vid(i, j, k) + v100 = vid(i + 1, j, k) + v010 = vid(i, j + 1, k) + v110 = vid(i + 1, j + 1, k) + v001 = vid(i, j, k + 1) + v101 = vid(i + 1, j, k + 1) + v011 = vid(i, j + 1, k + 1) + v111 = vid(i + 1, j + 1, k + 1) + + tet_list.extend( + ( + [v000, v100, v110, v111], + [v000, v110, v010, v111], + [v000, v010, v011, v111], + [v000, v011, v001, v111], + [v000, v001, v101, v111], + [v000, v101, v100, v111], + ) + ) + + connectivity = np.asarray(tet_list, dtype=np.int32).reshape(-1) + offsets = np.arange(0, connectivity.size + 1, 4, dtype=np.int32) + cell_types = np.full(offsets.size - 1, VTK_TETRA, dtype=np.int32) + + # Ensure positive tetra orientation for the affine maps used later. + for cell_id in range(cell_types.size): + start = offsets[cell_id] + tet = connectivity[start : start + 4].copy() + verts = points[tet] + J = np.column_stack((verts[1] - verts[0], verts[2] - verts[0], verts[3] - verts[0])) + if np.linalg.det(J) < 0.0: + connectivity[start + 1], connectivity[start + 2] = ( + connectivity[start + 2], + connectivity[start + 1], + ) + + mesh = cutcells.MeshView(points, connectivity, offsets, cell_types, tdim=3) + return mesh, points, connectivity, offsets, cell_types + + +def mesh_to_pyvista(points: np.ndarray, connectivity: np.ndarray, offsets: np.ndarray, cell_types: np.ndarray, pv): + cells = np.empty(cell_types.size * 5, dtype=np.int64) + cells[0::5] = 4 + cells.reshape(-1, 5)[:, 1:] = connectivity.reshape(-1, 4) + return pv.UnstructuredGrid(cells, np.asarray(cell_types, dtype=np.uint8), points) + + +def cutmesh_to_pyvista(cut_mesh, pv): + return pv.UnstructuredGrid( + np.array(cut_mesh.cells, dtype=np.int64, copy=True), + np.array(cut_mesh.vtk_types, dtype=np.uint8, copy=True), + np.array(cut_mesh.vertex_coords, dtype=np.float64, copy=True), + ) + + +def tetra_parent_vertices(mesh, cell_id: int) -> np.ndarray: + connectivity = np.asarray(mesh.connectivity, dtype=np.int32) + offsets = np.asarray(mesh.offsets, dtype=np.int32) + coordinates = np.asarray(mesh.coordinates, dtype=np.float64) + start = int(offsets[cell_id]) + end = int(offsets[cell_id + 1]) + vertex_ids = connectivity[start:end] + return np.array(coordinates[vertex_ids], dtype=np.float64, copy=True) + + +def physical_to_parent_reference_tetra(parent_vertices: np.ndarray, x_phys: np.ndarray) -> np.ndarray: + x0 = parent_vertices[0] + J = np.column_stack( + ( + parent_vertices[1] - x0, + parent_vertices[2] - x0, + parent_vertices[3] - x0, + ) + ) + xi = np.linalg.solve(J, np.asarray(x_phys, dtype=np.float64) - x0) + return np.array(xi, dtype=np.float64, copy=True) + + +def edge_key(parent_cell_id: int, xa: np.ndarray, xb: np.ndarray, digits: int = 12): + pa = tuple(np.round(np.asarray(xa, dtype=np.float64), digits)) + pb = tuple(np.round(np.asarray(xb, dtype=np.float64), digits)) + return (int(parent_cell_id), tuple(sorted((pa, pb)))) + + +def collect_edges_by_parent(cut_mesh) -> dict: + points = np.asarray(cut_mesh.vertex_coords, dtype=np.float64) + cells = np.asarray(cut_mesh.cells, dtype=np.int64) + vtk_types = np.asarray(cut_mesh.vtk_types, dtype=np.int64) + parent_map = np.asarray(cut_mesh.parent_map, dtype=np.int32) + + if parent_map.size != vtk_types.size: + raise RuntimeError("cut_mesh.parent_map must contain one entry per output cell.") + + edges = {} + cursor = 0 + for cell_id, vtk_type in enumerate(vtk_types): + num_verts = int(cells[cursor]) + local_ids = cells[cursor + 1 : cursor + 1 + num_verts] + cursor += num_verts + 1 + parent_cell_id = int(parent_map[cell_id]) + + for local_a, local_b in EDGE_PATTERNS[int(vtk_type)]: + xa = np.array(points[int(local_ids[local_a])], dtype=np.float64, copy=True) + xb = np.array(points[int(local_ids[local_b])], dtype=np.float64, copy=True) + edges[edge_key(parent_cell_id, xa, xb)] = { + "parent_cell_id": parent_cell_id, + "xa_phys": xa, + "xb_phys": xb, + } + + return edges + + +def classify_new_triangulation_edges(mesh, ls, base_mesh, triangulated_mesh) -> list[dict]: + base_edges = collect_edges_by_parent(base_mesh) + tri_edges = collect_edges_by_parent(triangulated_mesh) + ls_cell_cache: dict[int, object] = {} + parent_vertex_cache: dict[int, np.ndarray] = {} + records = [] + + for key in sorted(set(tri_edges) - set(base_edges)): + record = dict(tri_edges[key]) + parent_cell_id = record["parent_cell_id"] + + if parent_cell_id not in ls_cell_cache: + ls_cell_cache[parent_cell_id] = cutcells.make_cell_level_set(ls, parent_cell_id) + parent_vertex_cache[parent_cell_id] = tetra_parent_vertices(mesh, parent_cell_id) + + ls_cell = ls_cell_cache[parent_cell_id] + parent_vertices = parent_vertex_cache[parent_cell_id] + xa_ref = physical_to_parent_reference_tetra(parent_vertices, record["xa_phys"]) + xb_ref = physical_to_parent_reference_tetra(parent_vertices, record["xb_phys"]) + + edge_coeffs = np.asarray( + cutcells.restrict_edge_bernstein_exact( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + xa_ref, + xb_ref, + ), + dtype=np.float64, + ) + tag, split_t = cutcells.classify_edge_roots( + edge_coeffs, + zero_tol=1.0e-12, + sign_tol=1.0e-12, + max_depth=20, + ) + tag_value = int(tag.value) + phi_a = float( + cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + xa_ref, + ) + ) + phi_b = float( + cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + xb_ref, + ) + ) + has_zero_endpoint = abs(phi_a) <= 1.0e-12 or abs(phi_b) <= 1.0e-12 + has_root = tag_value in { + int(cutcells.EdgeRootTag.one_root.value), + int(cutcells.EdgeRootTag.multiple_roots.value), + int(cutcells.EdgeRootTag.zero.value), + } + has_interior_root = ( + tag_value in { + int(cutcells.EdgeRootTag.one_root.value), + int(cutcells.EdgeRootTag.multiple_roots.value), + } + and not has_zero_endpoint + ) + + record["xa_ref"] = xa_ref + record["xb_ref"] = xb_ref + record["phi_a"] = phi_a + record["phi_b"] = phi_b + record["edge_root_tag"] = tag_value + record["edge_has_root"] = int(has_root) + record["edge_has_zero_endpoint"] = int(has_zero_endpoint) + record["edge_has_interior_root"] = int(has_interior_root) + record["green_split_t"] = float(split_t) if split_t is not None else np.nan + records.append(record) + + return records + + +def write_line_vtu(path: Path, records: list[dict]) -> None: + if not records: + return + + points = [] + connectivity = [] + offsets = [] + root_tags = [] + has_root = [] + parent_ids = [] + split_t = [] + + for edge_id, record in enumerate(records): + points.extend(record["xa_phys"].tolist()) + points.extend(record["xb_phys"].tolist()) + connectivity.extend((2 * edge_id, 2 * edge_id + 1)) + offsets.append(2 * edge_id + 2) + root_tags.append(record["edge_root_tag"]) + has_root.append(record["edge_has_root"]) + parent_ids.append(record["parent_cell_id"]) + split_t.append(record["green_split_t"]) + + point_text = " ".join(f"{value:.16g}" for value in points) + connectivity_text = " ".join(str(value) for value in connectivity) + offsets_text = " ".join(str(value) for value in offsets) + types_text = " ".join("3" for _ in records) # VTK_LINE + root_tags_text = " ".join(str(value) for value in root_tags) + has_root_text = " ".join(str(value) for value in has_root) + parent_ids_text = " ".join(str(value) for value in parent_ids) + split_t_text = " ".join( + "nan" if np.isnan(value) else f"{value:.16g}" for value in split_t + ) + + xml = f""" + + + + + + {point_text} + + + + + {connectivity_text} + + + {offsets_text} + + + {types_text} + + + + + {root_tags_text} + + + {has_root_text} + + + {parent_ids_text} + + + {split_t_text} + + + + + +""" + path.write_text(xml) + + +def summarize_records(records: list[dict]) -> dict[str, int]: + counts = Counter(TAG_LABELS[record["edge_root_tag"]] for record in records) + return dict(sorted(counts.items())) + + +def count_interior_root_edges(records: list[dict]) -> int: + return int(sum(record["edge_has_interior_root"] for record in records)) + + +def count_zero_endpoint_edges(records: list[dict]) -> int: + return int(sum(record["edge_has_zero_endpoint"] for record in records)) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Triangulated HO straight-output demo with triangulation-edge root checks." + ) + parser.add_argument( + "--n", + type=int, + default=8, + help="Structured tetrahedral mesh resolution parameter.", + ) + parser.add_argument( + "--degree", + type=int, + default=2, + help="Polynomial degree for create_level_set(...).", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("demo_meshview_ho_cut_triangulated_output"), + help="Directory for VTU files.", + ) + parser.add_argument( + "--no-plot", + action="store_true", + help="Skip interactive pyvista plotting.", + ) + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Creating tetrahedral pyvista mesh with n={args.n} ...") + mesh, points, connectivity, offsets, cell_types = structured_tetra_mesh( + -1.0, -1.0, -1.0, + 1.0, 1.0, 1.0, + args.n, args.n, args.n, + ) + print(f" tetra cells={mesh.num_cells()}, points={mesh.num_nodes()}") + print(f" MeshView gdim={mesh.gdim}, tdim={mesh.tdim}") + + print(f"Building polynomial level set with degree={args.degree} ...") + ls = cutcells.create_level_set(mesh, phi_batch, degree=args.degree, name="phi") + print(f" level-set dofs={ls.mesh_data.num_dofs()}") + + print("Running cut(mesh, ls) ...") + result = cutcells.cut(mesh, ls) + negative = result["phi < 0"] + interface = result["phi = 0"] + positive = result["phi > 0"] + + print(f" num_cut_cells={result.num_cut_cells}") + print(f" phi < 0 : cut={negative.num_cut_cells}, uncut={negative.num_uncut_cells}") + print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") + print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") + + print("Building triangulated straight visualization meshes from HOMeshPart ...") + inside_full = negative.visualization_mesh(mode="full", triangulate=True) + inside_cut = negative.visualization_mesh(mode="cut_only", triangulate=True) + outside_full = positive.visualization_mesh(mode="full", triangulate=True) + outside_cut = positive.visualization_mesh(mode="cut_only", triangulate=True) + interface_mesh = interface.visualization_mesh(mode="cut_only", triangulate=True) + + print(f" inside full cells={len(np.asarray(inside_full.types))}") + print(f" inside cut-only cells={len(np.asarray(inside_cut.types))}") + print(f" interface cells={len(np.asarray(interface_mesh.types))}") + print(f" outside full cells={len(np.asarray(outside_full.types))}") + print(f" outside cut-only cells={len(np.asarray(outside_cut.types))}") + + print("Building non-triangulated reference meshes for the new-edge check ...") + inside_full_base = negative.visualization_mesh(mode="full", triangulate=False) + inside_cut_base = negative.visualization_mesh(mode="cut_only", triangulate=False) + outside_full_base = positive.visualization_mesh(mode="full", triangulate=False) + outside_cut_base = positive.visualization_mesh(mode="cut_only", triangulate=False) + interface_base = interface.visualization_mesh(mode="cut_only", triangulate=False) + + print("Checking whether triangulation-created edges carry level-set roots ...") + edge_checks = { + "phi_negative_full": classify_new_triangulation_edges( + mesh, ls, inside_full_base, inside_full + ), + "phi_negative_cut_only": classify_new_triangulation_edges( + mesh, ls, inside_cut_base, inside_cut + ), + "phi_interface": classify_new_triangulation_edges( + mesh, ls, interface_base, interface_mesh + ), + "phi_positive_full": classify_new_triangulation_edges( + mesh, ls, outside_full_base, outside_full + ), + "phi_positive_cut_only": classify_new_triangulation_edges( + mesh, ls, outside_cut_base, outside_cut + ), + } + + for name, records in edge_checks.items(): + print( + f" {name}: new_edges={len(records)}, " + f"interior_root_edges={count_interior_root_edges(records)}, " + f"zero_endpoint_edges={count_zero_endpoint_edges(records)}, " + f"tags={summarize_records(records)}" + ) + + for stem, records in edge_checks.items(): + if not records: + continue + path = args.output_dir / f"{stem}_triangulation_new_edges.vtu" + write_line_vtu(path, records) + print(f" wrote {path}") + + mesh_outputs = [ + (negative, args.output_dir / "phi_negative_full_triangulated.vtu", "full"), + (negative, args.output_dir / "phi_negative_cut_only_triangulated.vtu", "cut_only"), + (interface, args.output_dir / "phi_interface_triangulated.vtu", "cut_only"), + (positive, args.output_dir / "phi_positive_full_triangulated.vtu", "full"), + (positive, args.output_dir / "phi_positive_cut_only_triangulated.vtu", "cut_only"), + ] + + for part, path, mode in mesh_outputs: + part.write_vtu(str(path), mode=mode, triangulate=True) + print(f" wrote {path}") + + if args.no_plot: + return + + try: + import pyvista as pv + except Exception as exc: + raise SystemExit( + "pyvista is required for plotting this demo. " + "The VTU files were still written successfully.\n" + f"Import error: {exc}" + ) + + pv_inside_full = cutmesh_to_pyvista(inside_full, pv) + pv_interface = cutmesh_to_pyvista(interface_mesh, pv) + pv_outside_full = cutmesh_to_pyvista(outside_full, pv) + grid = mesh_to_pyvista(points, connectivity, offsets, cell_types, pv) + + plotter = pv.Plotter(shape=(1, 3), title="CutCells HO triangulated straight-output demo") + plotter.subplot(0, 0) + plotter.add_title("phi < 0, mode='full', triangulated") + plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.18) + plotter.add_mesh(pv_inside_full, color="steelblue", opacity=0.78, show_edges=True) + + plotter.subplot(0, 1) + plotter.add_title("phi = 0, mode='cut_only', triangulated") + plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.12) + plotter.add_mesh(pv_interface, color="crimson", opacity=0.95, show_edges=True) + + plotter.subplot(0, 2) + plotter.add_title("phi > 0, mode='full', triangulated") + plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.18) + plotter.add_mesh(pv_outside_full, color="burlywood", opacity=0.78, show_edges=True) + + plotter.link_views() + plotter.show() + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_ho_part_straight_output.py b/python/tests/test_ho_part_straight_output.py index 16bddad..06ddf12 100644 --- a/python/tests/test_ho_part_straight_output.py +++ b/python/tests/test_ho_part_straight_output.py @@ -21,6 +21,56 @@ def _single_tetra_mesh(): return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) +def _single_triangle_mesh(): + coords = np.array( + [ + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2], dtype=np.int32) + offsets = np.array([0, 3], dtype=np.int32) + cell_types = np.array([5], dtype=np.int32) + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=2) + + +def _edge_key(xa: np.ndarray, xb: np.ndarray, digits: int = 12): + pa = tuple(np.round(np.asarray(xa, dtype=np.float64), digits)) + pb = tuple(np.round(np.asarray(xb, dtype=np.float64), digits)) + return tuple(sorted((pa, pb))) + + +def _vtk_edges(vtk_type: int): + if vtk_type == 5: # triangle + return ((0, 1), (1, 2), (2, 0)) + if vtk_type == 9: # quad + return ((0, 1), (1, 2), (2, 3), (3, 0)) + raise ValueError(f"Unsupported vtk_type {vtk_type} for boundary-edge extraction") + + +def _edge_counts(cut_mesh): + points = np.asarray(cut_mesh.vertex_coords, dtype=np.float64) + cells = np.asarray(cut_mesh.cells, dtype=np.int64) + vtk_types = np.asarray(cut_mesh.vtk_types, dtype=np.int64) + + counts = {} + cursor = 0 + for vtk_type in vtk_types: + num_verts = int(cells[cursor]) + local_ids = cells[cursor + 1 : cursor + 1 + num_verts] + cursor += num_verts + 1 + + for local_a, local_b in _vtk_edges(int(vtk_type)): + xa = points[int(local_ids[local_a])] + xb = points[int(local_ids[local_b])] + key = _edge_key(xa, xb) + counts[key] = counts.get(key, 0) + 1 + + return counts + + def test_homeshpart_straight_output_bridge(tmp_path: Path): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( @@ -53,10 +103,14 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): q_cut = negative.quadrature(order=3, mode="cut_only") q_full = negative.quadrature(order=3, mode="full") q_interface = interface.quadrature(order=3, mode="cut_only") + q_cut_triangulated = negative.quadrature( + order=3, mode="cut_only", triangulate=True + ) assert q_cut.weights.sum() > 0.0 assert q_full.weights.sum() >= q_cut.weights.sum() assert q_interface.weights.sum() > 0.0 + assert np.isclose(q_cut_triangulated.weights.sum(), q_cut.weights.sum()) negative_path = tmp_path / "negative_full.vtu" interface_path = tmp_path / "interface.vtu" @@ -67,3 +121,60 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): assert interface_path.exists() assert "Name=\"types\" format=\"ascii\">13 " in negative_path.read_text() assert "Name=\"types\" format=\"ascii\">9 " in interface_path.read_text() + + +def test_triangulated_interface_preserves_quad_boundary_without_bowtie(): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.6, + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls) + interface = result["phi = 0"] + + vis_interface = interface.visualization_mesh(mode="cut_only", triangulate=False) + vis_interface_triangulated = interface.visualization_mesh( + mode="cut_only", triangulate=True + ) + + quad_edges = _edge_counts(vis_interface) + tri_edges = _edge_counts(vis_interface_triangulated) + + quad_boundary = {edge for edge, count in quad_edges.items() if count == 1} + tri_boundary = {edge for edge, count in tri_edges.items() if count == 1} + tri_interior = {edge for edge, count in tri_edges.items() if count == 2} + + assert np.asarray(vis_interface.vtk_types).tolist() == [9] + assert np.asarray(vis_interface_triangulated.vtk_types).tolist() == [5, 5] + assert tri_boundary == quad_boundary + assert len(tri_interior) == 1 + + +def test_triangle_volume_output_uses_quad_midpoint_split(): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: 0.1 - 0.2 * X[0] - 0.1 * X[1], + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls) + negative = result["phi < 0"] + + vis_base = negative.visualization_mesh(mode="cut_only", triangulate=False) + vis_triangulated = negative.visualization_mesh(mode="cut_only", triangulate=True) + q_base = negative.quadrature(order=3, mode="cut_only", triangulate=False) + q_triangulated = negative.quadrature(order=3, mode="cut_only", triangulate=True) + + tri_points = np.asarray(vis_triangulated.vertex_coords, dtype=np.float64) + + assert np.asarray(vis_base.vtk_types).tolist() == [9] + assert np.asarray(vis_triangulated.vtk_types).tolist() == [5, 5, 5] + assert np.asarray(vis_base.vertex_coords).shape[0] == 4 + assert tri_points.shape[0] == 5 + assert np.any(np.all(np.isclose(tri_points, np.array([1.0, 0.5])), axis=1)) + assert np.isclose(q_triangulated.weights.sum(), q_base.weights.sum()) diff --git a/python/tests/test_tetra_triangulated_cut_edges.py b/python/tests/test_tetra_triangulated_cut_edges.py new file mode 100644 index 0000000..7f95a8b --- /dev/null +++ b/python/tests/test_tetra_triangulated_cut_edges.py @@ -0,0 +1,185 @@ +import numpy as np + +import cutcells + + +EDGE_PATTERNS = { + 5: ((0, 1), (1, 2), (2, 0)), # triangle + 9: ((0, 1), (1, 2), (2, 3), (3, 0)), # quad + 10: ((0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)), # tet + 13: ( + (0, 1), + (1, 2), + (2, 0), + (3, 4), + (4, 5), + (5, 3), + (0, 3), + (1, 4), + (2, 5), + ), # prism / wedge +} + + +def _single_tetra_mesh(): + coords = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3], dtype=np.int32) + offsets = np.array([0, 4], dtype=np.int32) + cell_types = np.array([10], dtype=np.int32) + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3), coords + + +def _edge_key(xa: np.ndarray, xb: np.ndarray, digits: int = 12): + pa = tuple(np.round(np.asarray(xa, dtype=np.float64), digits)) + pb = tuple(np.round(np.asarray(xb, dtype=np.float64), digits)) + return tuple(sorted((pa, pb))) + + +def _collect_unique_edges(cut_cell): + points = np.asarray(cut_cell.vertex_coords, dtype=np.float64) + cells = np.asarray(cut_cell.cells, dtype=np.int64) + vtk_types = np.asarray(cut_cell.vtk_types, dtype=np.int64) + + edges = {} + cursor = 0 + for vtk_type in vtk_types: + num_verts = int(cells[cursor]) + local_ids = cells[cursor + 1 : cursor + 1 + num_verts] + cursor += num_verts + 1 + + for local_a, local_b in EDGE_PATTERNS[int(vtk_type)]: + xa = points[int(local_ids[local_a])] + xb = points[int(local_ids[local_b])] + edges[_edge_key(xa, xb)] = ( + np.array(xa, dtype=np.float64, copy=True), + np.array(xb, dtype=np.float64, copy=True), + ) + + return edges + + +def _classify_new_edges(ls_cell, base_cut, triangulated_cut): + base_edges = _collect_unique_edges(base_cut) + triangulated_edges = _collect_unique_edges(triangulated_cut) + + records = [] + for key in sorted(set(triangulated_edges) - set(base_edges)): + xa, xb = triangulated_edges[key] + coeffs = np.asarray( + cutcells.restrict_edge_bernstein_exact( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + np.asarray(xa, dtype=np.float64), + np.asarray(xb, dtype=np.float64), + ), + dtype=np.float64, + ) + tag, _ = cutcells.classify_edge_roots( + coeffs, + zero_tol=1.0e-12, + sign_tol=1.0e-12, + max_depth=20, + ) + phi_a = float( + cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + np.asarray(xa, dtype=np.float64), + ) + ) + phi_b = float( + cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + np.asarray(xb, dtype=np.float64), + ) + ) + has_zero_endpoint = abs(phi_a) <= 1.0e-12 or abs(phi_b) <= 1.0e-12 + records.append( + { + "tag": tag, + "has_zero_endpoint": has_zero_endpoint, + "has_interior_root": tag == cutcells.EdgeRootTag.multiple_roots + or (tag == cutcells.EdgeRootTag.one_root and not has_zero_endpoint), + } + ) + + return records + + +def test_triangulated_tetra_cut_edges_are_classified_against_zero_level_set(): + mesh, coords = _single_tetra_mesh() + nodal_phi = coords[:, 0] + coords[:, 1] - 0.6 + ls = cutcells.create_level_set(mesh, lambda X: X[0] + X[1] - 0.6, degree=1, name="phi") + ls_cell = cutcells.make_cell_level_set(ls, 0) + + expected = { + "phi<0": { + "base_types": [13], + "triangulated_types": [10, 10, 10, 10], + "new_edge_count": 7, + "zero_tag_count": 1, + "zero_endpoint_count": 5, + "interior_root_count": 0, + }, + "phi=0": { + "base_types": [9], + "triangulated_types": [5, 5], + "new_edge_count": 1, + "zero_tag_count": 1, + "zero_endpoint_count": 1, + "interior_root_count": 0, + }, + "phi>0": { + "base_types": [13], + "triangulated_types": [10, 10, 10, 10], + "new_edge_count": 7, + "zero_tag_count": 1, + "zero_endpoint_count": 5, + "interior_root_count": 0, + }, + } + + for expr, exp in expected.items(): + base_cut = cutcells.cut( + cutcells.CellType.tetrahedron, + coords.ravel(), + 3, + nodal_phi, + expr, + False, + ) + triangulated_cut = cutcells.cut( + cutcells.CellType.tetrahedron, + coords.ravel(), + 3, + nodal_phi, + expr, + True, + ) + + assert np.asarray(base_cut.vtk_types).tolist() == exp["base_types"] + assert np.asarray(triangulated_cut.vtk_types).tolist() == exp["triangulated_types"] + + edge_records = _classify_new_edges(ls_cell, base_cut, triangulated_cut) + assert len(edge_records) == exp["new_edge_count"] + assert sum(record["tag"] == cutcells.EdgeRootTag.zero for record in edge_records) == exp[ + "zero_tag_count" + ] + assert sum(record["has_zero_endpoint"] for record in edge_records) == exp[ + "zero_endpoint_count" + ] + assert sum(record["has_interior_root"] for record in edge_records) == exp[ + "interior_root_count" + ] diff --git a/python/tests/test_tetra_triangulation_analysis.py b/python/tests/test_tetra_triangulation_analysis.py new file mode 100644 index 0000000..d1a65cb --- /dev/null +++ b/python/tests/test_tetra_triangulation_analysis.py @@ -0,0 +1,48 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from cutcells import CellType, analyze_all_cases, summarize_analysis + + +def test_tetra_parent_summary_excludes_zero_only_edges_from_interior_root_count(): + records, summary = analyze_all_cases(CellType.tetrahedron) + + assert summary["cases"] == 42 + assert summary["total_new_edges"] == 218 + assert summary["new_edges_with_interior_roots"] == 0 + assert summary["new_edges_with_zero_tag"] == 18 + assert summary["new_edges_with_zero_endpoint"] == 122 + assert summary["total_new_simplices"] == 108 + assert summary["negative_new_simplices"] == 0 + assert summary["degenerate_new_simplices"] == 0 + + +def test_tetra_parent_interface_quads_have_no_interior_root_edges_or_negative_triangles(): + records, _ = analyze_all_cases(CellType.tetrahedron) + interface_summary = summarize_analysis([record for record in records if record["expr"] == "phi=0"]) + + assert interface_summary["cases"] == 14 + assert interface_summary["source_type_counts"] == {"quadrilateral": 12} + assert interface_summary["new_edges_with_interior_roots"] == 0 + assert interface_summary["new_edges_with_zero_tag"] == 6 + assert interface_summary["new_edges_with_zero_endpoint"] == 6 + assert interface_summary["negative_new_simplices"] == 0 + assert interface_summary["degenerate_new_simplices"] == 0 + + +def test_tetra_parent_prism_volume_cases_have_only_endpoint_roots_and_positive_tets(): + records, _ = analyze_all_cases(CellType.tetrahedron) + + negative_summary = summarize_analysis([record for record in records if record["expr"] == "phi<0"]) + positive_summary = summarize_analysis([record for record in records if record["expr"] == "phi>0"]) + + for summary in (negative_summary, positive_summary): + assert summary["cases"] == 14 + assert summary["source_type_counts"] == {"prism": 48} + assert summary["new_edges_with_interior_roots"] == 0 + assert summary["new_edges_with_zero_tag"] == 6 + assert summary["new_edges_with_zero_endpoint"] == 58 + assert summary["negative_new_simplices"] == 0 + assert summary["degenerate_new_simplices"] == 0 diff --git a/python/tests/test_triangle.py b/python/tests/test_triangle.py index 9062a20..c30d768 100644 --- a/python/tests/test_triangle.py +++ b/python/tests/test_triangle.py @@ -7,6 +7,8 @@ import numpy as np import pytest +TRIANGLE = int(cutcells.CellType.triangle.value) + level_set_values = [(np.array([0.1,-0.1,0.2]),1./12.), (np.array([-0.1,0.1,0.2]),1./12.), (np.array([0.1,0.1,-0.2]),2./9.), @@ -45,4 +47,24 @@ def test_triangle_exterior(ls_values, vol_ex): cut_cell = cutcells.cut(cell_type, vertex_coordinates, gdim, ls_values, "phi>0", triangulate) vol = cut_cell.volume() - assert np.isclose(vol,vol_ex) \ No newline at end of file + assert np.isclose(vol,vol_ex) + + +def test_triangle_quad_midpoint_split(): + vertex_coordinates = np.array([0.,0.,1.,0.,1.,1.], dtype=np.float64) + ls_values = np.array([0.1,-0.1,-0.2], dtype=np.float64) + + cut_cell = cutcells.cut( + cutcells.CellType.triangle, + vertex_coordinates, + 2, + ls_values, + "phi<0", + True, + ) + + coords = np.asarray(cut_cell.vertex_coords, dtype=np.float64) + + assert list(cut_cell.types) == [TRIANGLE, TRIANGLE, TRIANGLE] + assert coords.shape == (5, 2) + assert np.any(np.all(np.isclose(coords, np.array([1.0, 0.5])), axis=1)) From 3681aebdde8318d4f9323f36e8b1ac4778885fbc Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Fri, 24 Apr 2026 15:06:26 +0200 Subject: [PATCH 12/23] testing 2 level sets, cuts in triangle and tetrahedron working, hole mesh cuts still hitting corner cases to be fixed --- cpp/src/adapt_cell.cpp | 123 +++++++ cpp/src/adapt_cell.h | 26 ++ cpp/src/cell_certification.cpp | 98 ++++-- cpp/src/cell_certification.h | 10 +- cpp/src/cut_tetrahedron.cpp | 4 +- cpp/src/cut_triangle.cpp | 7 +- cpp/src/edge_certification.cpp | 49 ++- cpp/src/ho_cut_mesh.cpp | 316 ++++++++++++++---- cpp/src/ho_mesh_part_output.cpp | 293 ++++++++++------ cpp/src/prism_midpoint_split.h | 63 +++- cpp/src/quad_midpoint_split.h | 41 ++- cpp/src/refine_cell.cpp | 5 + cpp/src/selection_expr.cpp | 2 +- python/cutcells/__init__.py | 136 +++++++- python/cutcells/mesh_utils.py | 59 ++++ python/cutcells/wrapper.cpp | 194 +++++++++-- python/demo/demo_meshview_ho_cut.py | 15 +- .../demo/demo_meshview_ho_cut_triangulated.py | 26 +- python/tests/test_ho_part_straight_output.py | 73 ++-- python/tests/test_mesh_utils.py | 12 + .../test_tetra_triangulation_analysis.py | 4 +- 21 files changed, 1222 insertions(+), 334 deletions(-) diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp index 513c188..a6705e9 100644 --- a/cpp/src/adapt_cell.cpp +++ b/cpp/src/adapt_cell.cpp @@ -7,6 +7,7 @@ #include "cell_topology.h" #include "reference_cell.h" +#include #include #include #include @@ -175,6 +176,8 @@ AdaptCell make_adapt_cell(const MeshView& mesh, I cell_id) if (tdim == 3) build_faces(ac); + recompute_active_level_set_masks(ac, /*num_level_sets=*/0); + return ac; } @@ -205,6 +208,122 @@ void fill_vertex_signs(AdaptCell& ac, } } +// --------------------------------------------------------------------------- +// recompute_active_level_set_masks +// --------------------------------------------------------------------------- + +template +void recompute_active_level_set_masks(AdaptCell& ac, int num_level_sets) +{ + const int tdim = ac.tdim; + const int n_cells = ac.n_entities(tdim); + + ac.cell_active_level_set_mask.assign(static_cast(n_cells), + std::uint64_t(0)); + ac.active_level_set_mask = 0; + + if (num_level_sets <= 0 || n_cells <= 0) + return; + + const int nls = std::min(num_level_sets, 64); + + for (int c = 0; c < n_cells; ++c) + { + auto verts = ac.entity_to_vertex[tdim][static_cast(c)]; + std::uint64_t leaf_mask = 0; + + for (int ls = 0; ls < nls; ++ls) + { + const std::uint64_t bit = std::uint64_t(1) << ls; + bool has_neg = false; + bool has_pos = false; + bool has_zero = false; + + for (const auto v : verts) + { + const auto zv = ac.zero_mask_per_vertex[static_cast(v)]; + const auto nv = ac.negative_mask_per_vertex[static_cast(v)]; + if ((zv & bit) != 0) + has_zero = true; + else if ((nv & bit) != 0) + has_neg = true; + else + has_pos = true; + } + + if (has_zero || (has_neg && has_pos)) + leaf_mask |= bit; + } + + ac.cell_active_level_set_mask[static_cast(c)] = leaf_mask; + ac.active_level_set_mask |= leaf_mask; + } +} + +// --------------------------------------------------------------------------- +// rebuild_zero_entity_inventory +// --------------------------------------------------------------------------- + +template +void rebuild_zero_entity_inventory(AdaptCell& ac) +{ + ac.zero_entity_dim.clear(); + ac.zero_entity_id.clear(); + ac.zero_entity_zero_mask.clear(); + ac.zero_entity_is_owned.clear(); + ac.zero_entity_parent_dim.clear(); + ac.zero_entity_parent_id.clear(); + + const int n_vertices = ac.n_vertices(); + for (int v = 0; v < n_vertices; ++v) + { + const auto mask = ac.zero_mask_per_vertex[static_cast(v)]; + if (mask == 0) + continue; + + ac.zero_entity_dim.push_back(std::uint8_t(0)); + ac.zero_entity_id.push_back(static_cast(v)); + ac.zero_entity_zero_mask.push_back(mask); + ac.zero_entity_is_owned.push_back(std::uint8_t(1)); + ac.zero_entity_parent_dim.push_back( + ac.vertex_parent_dim.empty() ? std::int8_t(-1) + : ac.vertex_parent_dim[static_cast(v)]); + ac.zero_entity_parent_id.push_back( + ac.vertex_parent_id.empty() ? std::int32_t(-1) + : ac.vertex_parent_id[static_cast(v)]); + } + + for (int dim = 1; dim < ac.tdim; ++dim) + { + const int n_entities = ac.n_entities(dim); + for (int e = 0; e < n_entities; ++e) + { + auto verts = ac.entity_to_vertex[dim][static_cast(e)]; + if (verts.empty()) + continue; + + std::uint64_t mask = ~std::uint64_t(0); + for (const auto v : verts) + { + mask &= ac.zero_mask_per_vertex[static_cast(v)]; + if (mask == 0) + break; + } + if (mask == 0) + continue; + + ac.zero_entity_dim.push_back(static_cast(dim)); + ac.zero_entity_id.push_back(static_cast(e)); + ac.zero_entity_zero_mask.push_back(mask); + ac.zero_entity_is_owned.push_back(std::uint8_t(1)); + ac.zero_entity_parent_dim.push_back(std::int8_t(-1)); + ac.zero_entity_parent_id.push_back(std::int32_t(-1)); + } + } + + ++ac.zero_entity_version; +} + // --------------------------------------------------------------------------- // Explicit template instantiations // --------------------------------------------------------------------------- @@ -222,5 +341,9 @@ template AdaptCell make_adapt_cell(const MeshView&, long); template void fill_vertex_signs(AdaptCell&, std::span, int, double); template void fill_vertex_signs(AdaptCell&, std::span, int, float); +template void recompute_active_level_set_masks(AdaptCell&, int); +template void recompute_active_level_set_masks(AdaptCell&, int); +template void rebuild_zero_entity_inventory(AdaptCell&); +template void rebuild_zero_entity_inventory(AdaptCell&); } // namespace cutcells diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h index 888805e..2018771 100644 --- a/cpp/src/adapt_cell.h +++ b/cpp/src/adapt_cell.h @@ -112,6 +112,11 @@ struct AdaptCell ///< bit i set → level set i is active on this cell (cuts through it) std::uint64_t active_level_set_mask = 0; + /// Per-leaf-cell active level-set mask. + /// Parallel to entity_types[tdim]/entity_to_vertex[tdim]: + /// bit i set ↔ level set i intersects that leaf cell. + std::vector cell_active_level_set_mask; + // --------------------------------------------------------------- // Vertices (dimension 0) // @@ -527,4 +532,25 @@ void build_edges(AdaptCell& ac); template void build_faces(AdaptCell& ac); +/// Recompute per-leaf and per-cell active level-set masks from vertex masks. +/// +/// For each top-dimensional leaf cell, bit i is set if the cell has a +/// sign change for level set i or contains at least one zero vertex for i. +/// +/// @param ac AdaptCell to update in place. +/// @param num_level_sets Number of level sets to scan (bits 0..num_level_sets-1). +template +void recompute_active_level_set_masks(AdaptCell& ac, int num_level_sets); + +/// Rebuild the zero-entity inventory from current topology and vertex masks. +/// +/// Zero entities are registered for: +/// - dim 0: vertices with nonzero zero-mask +/// - dim 1..tdim-1: entities whose all vertices share at least one zero bit +/// (mask = intersection of vertex zero masks). +/// +/// @param ac AdaptCell to update in place. +template +void rebuild_zero_entity_inventory(AdaptCell& ac); + } // namespace cutcells diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index 6e2372f..65abf81 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -5,6 +5,7 @@ #include "cell_certification.h" #include "bernstein.h" +#include "cell_flags.h" #include "cell_topology.h" #include "cut_cell.h" #include "cut_tetrahedron.h" @@ -278,16 +279,6 @@ void gather_adapt_edge_bernstein(const AdaptCell& adapt_cell, int edge_id, std::vector& edge_coeffs) { - int parent_edge_id = -1; - if (edge_is_on_single_parent_edge(adapt_cell, edge_id, parent_edge_id)) - { - extract_parent_edge_bernstein( - ls_cell.cell_type, ls_cell.bernstein_order, - std::span(ls_cell.bernstein_coeffs), - parent_edge_id, edge_coeffs); - return; - } - auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; const int tdim = adapt_cell.tdim; std::span xi_a( @@ -546,11 +537,34 @@ void append_ready_cut_part_cells( if (token >= 0 && token < 100) { - // Straight cut vertices lie on the straight interface by construction. - // They should therefore participate in phi=0 face extraction even when - // the curved level set evaluated at this affine point is not exactly zero. - set_vertex_sign_for_level_set( - adapt_cell, vertex_id, level_set_id, T(0), zero_tol); + const int local_edge_id = token; + const bool is_level_set_root = + local_edge_id >= 0 + && local_edge_id < static_cast(old_edge_ids_by_local_edge.size()) + && old_edge_ids_by_local_edge[static_cast(local_edge_id)] >= 0 + && adapt_cell.get_edge_root_tag( + level_set_id, + old_edge_ids_by_local_edge[static_cast(local_edge_id)]) + == EdgeRootTag::one_root; + + if (is_level_set_root) + { + // Straight cut vertices lie on the straight interface by + // construction. They should participate in phi=0 + // extraction even when the curved level set evaluated at + // this affine point is not exactly zero. + set_vertex_sign_for_level_set( + adapt_cell, vertex_id, level_set_id, T(0), zero_tol); + } + else + { + // LUT triangulation may insert midpoints on uncut parent + // edges. They inherit parent-edge provenance, but are not + // zero vertices for this level set. + const T value = ls_cell.value(x); + set_vertex_sign_for_level_set( + adapt_cell, vertex_id, level_set_id, value, zero_tol); + } } else { @@ -1209,6 +1223,10 @@ void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, const int tdim = adapt_cell.tdim; for (int v = 0; v < n_vertices; ++v) { + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + if ((adapt_cell.zero_mask_per_vertex[static_cast(v)] & bit) != 0) + continue; + std::span xi( adapt_cell.vertex_coords.data() + static_cast(v) * static_cast(tdim), @@ -1228,7 +1246,8 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, int level_set_id, T zero_tol, T sign_tol, - int edge_max_depth) + int edge_max_depth, + bool triangulate_cut_parts) { const int tdim = adapt_cell.tdim; const int n_cells = adapt_cell.n_entities(tdim); @@ -1290,6 +1309,23 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, const std::vector ls_values = gather_leaf_cell_vertex_level_set_values(adapt_cell, ls_cell, c); + bool has_strict_negative = false; + bool has_strict_positive = false; + for (const T value : ls_values) + { + has_strict_negative = has_strict_negative || value < -zero_tol; + has_strict_positive = has_strict_positive || value > zero_tol; + } + if (!(has_strict_negative && has_strict_positive)) + { + std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); + append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + explicit_current_ls_tags.push_back( + has_strict_negative ? CellCertTag::negative : CellCertTag::positive); + continue; + } + std::array old_edge_ids_by_local_edge; old_edge_ids_by_local_edge.fill(-1); const auto ledges = cell::edges(leaf_cell_type); @@ -1343,14 +1379,14 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, std::span(ls_values), "phi<0", negative_part, - /*triangulate=*/false); + triangulate_cut_parts); cell::triangle::cut( std::span(vertex_coords), tdim, std::span(ls_values), "phi>0", positive_part, - /*triangulate=*/false); + triangulate_cut_parts); append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, @@ -1370,11 +1406,11 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, cell::tetrahedron::cut( std::span(vertex_coords), tdim, std::span(ls_values), "phi<0", - negative_part, /*triangulate=*/false); + negative_part, triangulate_cut_parts); cell::tetrahedron::cut( std::span(vertex_coords), tdim, std::span(ls_values), "phi>0", - positive_part, /*triangulate=*/false); + positive_part, triangulate_cut_parts); append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, @@ -1484,14 +1520,16 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, int level_set_id, int max_iterations, T zero_tol, T sign_tol, - int edge_max_depth) + int edge_max_depth, + bool triangulate_cut_parts) { fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); certify_and_refine(adapt_cell, ls_cell, level_set_id, max_iterations, zero_tol, sign_tol, edge_max_depth); fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, - zero_tol, sign_tol, edge_max_depth); + zero_tol, sign_tol, edge_max_depth, + triangulate_cut_parts); } // ===================================================================== @@ -1564,16 +1602,16 @@ template void fill_all_vertex_signs_from_level_set(AdaptCell&, template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, - int, double, double, int); + int, double, double, int, bool); template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, - int, float, float, int); + int, float, float, int, bool); template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, - int, double, double, int); + int, double, double, int, bool); template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, - int, float, float, int); + int, float, float, int, bool); template void certify_and_refine(AdaptCell&, const LevelSetCell&, @@ -1587,15 +1625,15 @@ template void certify_and_refine(AdaptCell&, template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, - int, int, double, double, int); + int, int, double, double, int, bool); template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, - int, int, float, float, int); + int, int, float, float, int, bool); template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, - int, int, double, double, int); + int, int, double, double, int, bool); template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, - int, int, float, float, int); + int, int, float, float, int, bool); } // namespace cutcells diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index daac87d..7b46a28 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -140,15 +140,16 @@ void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, int level_set_id, T zero_tol); -/// Replace leaf cells marked ready_to_cut by the non-triangulated LUT cut -/// decomposition on the positive and negative side. +/// Replace leaf cells marked ready_to_cut by the LUT cut decomposition on the +/// positive and negative side. template void process_ready_to_cut_cells(AdaptCell& adapt_cell, const LevelSetCell& ls_cell, int level_set_id, T zero_tol, T sign_tol, - int edge_max_depth); + int edge_max_depth, + bool triangulate_cut_parts = false); // ===================================================================== // Top-level certification + refinement driver @@ -189,6 +190,7 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, int level_set_id, int max_iterations, T zero_tol, T sign_tol, - int edge_max_depth); + int edge_max_depth, + bool triangulate_cut_parts = false); } // namespace cutcells diff --git a/cpp/src/cut_tetrahedron.cpp b/cpp/src/cut_tetrahedron.cpp index a01b925..f812dad 100644 --- a/cpp/src/cut_tetrahedron.cpp +++ b/cpp/src/cut_tetrahedron.cpp @@ -498,7 +498,9 @@ namespace tetrahedron{ create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 6, 4); + cutcells::utils::create_vertex_parent_entity_map( + vertex_case_map, cut_cell._vertex_parent_entity, + /*n_edges=*/6, /*n_vertices=*/4); } template diff --git a/cpp/src/cut_triangle.cpp b/cpp/src/cut_triangle.cpp index 7f32ca4..7956337 100644 --- a/cpp/src/cut_triangle.cpp +++ b/cpp/src/cut_triangle.cpp @@ -379,7 +379,9 @@ namespace triangle{ create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str, cut_cell, triangulate, intersection_points, vertex_case_map); - cutcells::utils::create_vertex_parent_entity_map(vertex_case_map, cut_cell._vertex_parent_entity, 3, 3); + cutcells::utils::create_vertex_parent_entity_map( + vertex_case_map, cut_cell._vertex_parent_entity, + /*n_edges=*/3, /*n_vertices=*/3); }; // cut triangle version with vector of string in case multiple parts of the cut-cell are needed (very common) @@ -412,7 +414,8 @@ namespace triangle{ create_cut_cell(vertex_coordinates, gdim, ls_values, cut_type_str[i], cut_cell[i], triangulate, intersection_points, vertex_case_map); cutcells::utils::create_vertex_parent_entity_map( - vertex_case_map, cut_cell[i]._vertex_parent_entity, 3, 3); + vertex_case_map, cut_cell[i]._vertex_parent_entity, + /*n_edges=*/3, /*n_vertices=*/3); } }; diff --git a/cpp/src/edge_certification.cpp b/cpp/src/edge_certification.cpp index 8381a74..03486f5 100644 --- a/cpp/src/edge_certification.cpp +++ b/cpp/src/edge_certification.cpp @@ -986,35 +986,26 @@ void classify_new_edges(AdaptCell& adapt_cell, != EdgeRootTag::not_classified) continue; - // Extract edge Bernstein coefficients. - int parent_edge_id = -1; - if (edge_is_on_single_parent_edge(adapt_cell, e, parent_edge_id)) - { - extract_parent_edge_bernstein( - ls_cell.cell_type, ls_cell.bernstein_order, - std::span(ls_cell.bernstein_coeffs), - parent_edge_id, edge_coeffs); - } - else - { - // Arbitrary adaptive edge: get endpoint reference coordinates. - auto verts = adapt_cell.entity_to_vertex[1][ - static_cast(e)]; - const int tdim = adapt_cell.tdim; - std::span xi_a( - adapt_cell.vertex_coords.data() - + static_cast(verts[0]) * static_cast(tdim), - static_cast(tdim)); - std::span xi_b( - adapt_cell.vertex_coords.data() - + static_cast(verts[1]) * static_cast(tdim), - static_cast(tdim)); - - restrict_edge_bernstein_exact( - ls_cell.cell_type, ls_cell.bernstein_order, - std::span(ls_cell.bernstein_coeffs), - xi_a, xi_b, edge_coeffs); - } + // Extract edge Bernstein coefficients on the actual adaptive edge segment. + // + // Even if an adaptive edge lies on a parent edge, using the parent-edge + // fast-path can be incorrect for subsegments (root localization would be + // in the wrong parameterization). Restrict by endpoints for robustness. + auto verts = adapt_cell.entity_to_vertex[1][static_cast(e)]; + const int tdim = adapt_cell.tdim; + std::span xi_a( + adapt_cell.vertex_coords.data() + + static_cast(verts[0]) * static_cast(tdim), + static_cast(tdim)); + std::span xi_b( + adapt_cell.vertex_coords.data() + + static_cast(verts[1]) * static_cast(tdim), + static_cast(tdim)); + + restrict_edge_bernstein_exact( + ls_cell.cell_type, ls_cell.bernstein_order, + std::span(ls_cell.bernstein_coeffs), + xi_a, xi_b, edge_coeffs); // Classify. T green_split_t = T(0); diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index e356a28..15f4ae7 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -8,8 +8,10 @@ #include "edge_certification.h" #include +#include #include #include +#include #include #include @@ -109,6 +111,176 @@ cell::domain classify_cell_domain_fast(const MeshView& mesh, static_cast(nv))); } +template +bool cell_contains_all_vertices(std::span cell_verts, + std::span entity_verts) +{ + for (const int v : entity_verts) + { + bool found = false; + for (const auto cv : cell_verts) + { + if (cv == v) + { + found = true; + break; + } + } + if (!found) + return false; + } + return true; +} + +template +bool vertex_state_for_level_set(const AdaptCell& ac, + int vertex_id, + int level_set_id, + bool& is_negative, + bool& is_positive, + bool& is_zero) +{ + if (level_set_id < 0 || level_set_id >= 64) + return false; + + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; + const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; + is_zero = (zm & bit) != 0; + is_negative = !is_zero && ((nm & bit) != 0); + is_positive = !is_zero && !is_negative; + return true; +} + +template +bool leaf_cell_matches_sign_requirements( + const AdaptCell& ac, + std::span cell_verts, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + const int nls = std::min(bg.num_level_sets, 64); + for (int li = 0; li < nls; ++li) + { + const std::uint64_t bit = std::uint64_t(1) << li; + const bool require_neg = (expr.negative_required & bit) != 0; + const bool require_pos = (expr.positive_required & bit) != 0; + if (!require_neg && !require_pos) + continue; + + const bool ls_is_active = (cut_cell_active_mask & bit) != 0; + if (!ls_is_active) + { + const auto dom = bg.domain(li, parent_cell_id); + if (require_neg && dom != cell::domain::inside) + return false; + if (require_pos && dom != cell::domain::outside) + return false; + continue; + } + + bool has_neg = false; + bool has_pos = false; + for (const auto cv : cell_verts) + { + bool is_neg = false; + bool is_pos = false; + bool is_zero = false; + vertex_state_for_level_set(ac, static_cast(cv), li, is_neg, is_pos, is_zero); + has_neg = has_neg || is_neg; + has_pos = has_pos || is_pos; + } + + if (require_neg) + { + if (has_pos || !has_neg) + return false; + } + if (require_pos) + { + if (has_neg || !has_pos) + return false; + } + } + + return true; +} + +template +bool zero_entity_matches( + const AdaptCell& ac, + int zero_entity_index, + int target_dim, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + if (ac.zero_entity_dim[static_cast(zero_entity_index)] != target_dim) + return false; + + const auto zero_mask = ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; + if ((zero_mask & expr.zero_required) != expr.zero_required) + return false; + + if (expr.negative_required == 0 && expr.positive_required == 0) + return true; + + std::vector zero_verts; + const int zdim = ac.zero_entity_dim[static_cast(zero_entity_index)]; + const int zid = ac.zero_entity_id[static_cast(zero_entity_index)]; + if (zdim == 0) + { + zero_verts.push_back(zid); + } + else + { + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + zero_verts.reserve(verts.size()); + for (const auto v : verts) + zero_verts.push_back(static_cast(v)); + } + + const int tdim = ac.tdim; + const int n_cells = ac.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + if (!cell_contains_all_vertices(cell_verts, std::span(zero_verts))) + continue; + + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, expr, cut_cell_active_mask, bg, parent_cell_id)) + { + return true; + } + } + + return false; +} + +template +void refresh_adapt_cell_semantics( + AdaptCell& ac, + std::span processed_level_set_indices, + std::span> processed_level_set_cells, + int total_num_level_sets, + T zero_tol) +{ + for (std::size_t i = 0; i < processed_level_set_indices.size(); ++i) + { + fill_all_vertex_signs_from_level_set( + ac, + processed_level_set_cells[i], + processed_level_set_indices[i], + zero_tol); + } + + recompute_active_level_set_masks(ac, total_num_level_sets); +} + } // anonymous namespace // ===================================================================== @@ -172,7 +344,24 @@ cut(const MeshView& mesh, const LevelSetFunction& ls) certify_refine_and_process_ready_cells( ac, hc.level_set_cells.back(), /*level_set_id=*/0, - /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); + /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20, + /*triangulate_cut_parts=*/false); + { + const std::array processed_ids = {0}; + const auto* processed_cell = &hc.level_set_cells.back(); + refresh_adapt_cell_semantics( + ac, + std::span(processed_ids.data(), processed_ids.size()), + std::span>(processed_cell, std::size_t(1)), + /*total_num_level_sets=*/1, + T(1e-12)); + } + + // Finalize derived topology/semantic layers used by selection + output. + build_edges(ac); + if (ac.tdim == 3) + build_faces(ac); + rebuild_zero_entity_inventory(ac); hc.adapt_cells.push_back(std::move(ac)); // Single LS: bit 0 is always set. @@ -266,24 +455,65 @@ cut(const MeshView& mesh, if (!any_intersected) continue; + std::vector all_level_set_indices(static_cast(nls)); + std::iota(all_level_set_indices.begin(), all_level_set_indices.end(), 0); + std::vector> all_level_set_cells; + all_level_set_cells.reserve(static_cast(nls)); + for (int li = 0; li < nls; ++li) + { + all_level_set_cells.push_back( + make_cell_level_set(level_sets[static_cast(li)], ci)); + } + const int cut_idx = hc.num_cut_cells(); bg.cell_to_cut_index[static_cast(ci)] = cut_idx; // Build AdaptCell once per cell. AdaptCell ac = make_adapt_cell(mesh, ci); - // Build LevelSetCell and fill vertex signs for each intersecting LS. + // Process intersecting level sets recursively (input order). + // + // Multi-level-set cutting must leave the AdaptCell leaf mesh as + // simplexes after each cut; otherwise a later level set may be asked to + // cut quad/prism leaves, which the ready-to-cut certification path does + // not support. std::uint64_t cell_active_mask = 0; for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) { const int li = intersected_ls_indices[k]; - hc.level_set_cells.push_back(std::move(intersected_ls_cells[k])); certify_refine_and_process_ready_cells( - ac, hc.level_set_cells.back(), li, - /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20); + ac, intersected_ls_cells[k], li, + /*max_iterations=*/8, T(1e-12), T(1e-12), + /*edge_max_depth=*/20, /*triangulate_cut_parts=*/true); + + // New vertices created while processing level set li must be + // reclassified for all already-processed level sets. + refresh_adapt_cell_semantics( + ac, + std::span(intersected_ls_indices.data(), k + 1), + std::span>(intersected_ls_cells.data(), k + 1), + nls, + T(1e-12)); cell_active_mask |= std::uint64_t(1) << li; } + + // Finalize derived topology/semantic layers used by selection + output. + build_edges(ac); + if (ac.tdim == 3) + build_faces(ac); + recompute_active_level_set_masks(ac, nls); + refresh_adapt_cell_semantics( + ac, + std::span(all_level_set_indices.data(), all_level_set_indices.size()), + std::span>(all_level_set_cells.data(), all_level_set_cells.size()), + nls, + T(1e-12)); + rebuild_zero_entity_inventory(ac); + + // Persist per-cell LevelSetCell blocks in HOCutCells CSR storage. + for (auto& ls_cell : intersected_ls_cells) + hc.level_set_cells.push_back(std::move(ls_cell)); hc.ls_offsets.push_back( static_cast(hc.level_set_cells.size())); hc.active_level_set_mask.push_back(cell_active_mask); @@ -350,84 +580,52 @@ HOMeshPart select_part(const HOCutCells& cut_cells, part.uncut_cell_ids.push_back(ci); } - // --- Cut cells: check vertex bitmasks for volume selections, - // or entity inventory for interface selections --- + // --- Cut cells --- const int ncut = cut_cells.num_cut_cells(); for (int k = 0; k < ncut; ++k) { const auto& ac = cut_cells.adapt_cells[static_cast(k)]; - const int nv = ac.n_vertices(); + const I bg_cell = cut_cells.parent_cell_ids[static_cast(k)]; + const std::uint64_t cut_active_mask = + cut_cells.active_level_set_mask[static_cast(k)]; if (is_volume) { - // Volume selection: a cut cell contributes if at least one vertex - // satisfies all sign constraints. - // Actually, a cut cell always contributes to a volume part - // (it has sub-entities on the requested side), so we include it. - // More refined: check if the required bitmask pattern is present. - bool has_matching_vertex = false; - for (int v = 0; v < nv; ++v) + bool has_matching_leaf = false; + const int tdim = ac.tdim; + const int n_leaf = ac.n_entities(tdim); + for (int c = 0; c < n_leaf; ++c) { - const auto zm = ac.zero_mask_per_vertex[static_cast(v)]; - const auto nm = ac.negative_mask_per_vertex[static_cast(v)]; - - // Positive bit: not zero, not negative. - const auto pm = ~zm & ~nm; - - bool vertex_ok = true; - - // Check if zero requirements are met (for EqualTo clauses). - if ((zm & part.expr.zero_required) != part.expr.zero_required) - vertex_ok = false; - - // Check if negative requirements are met. - if ((nm & part.expr.negative_required) != part.expr.negative_required) - vertex_ok = false; - - // Check if positive requirements are met. - if ((pm & part.expr.positive_required) != part.expr.positive_required) - vertex_ok = false; - - if (vertex_ok) + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, part.expr, cut_active_mask, bg, bg_cell)) { - has_matching_vertex = true; + has_matching_leaf = true; break; } } - if (has_matching_vertex) + if (has_matching_leaf) part.cut_cell_ids.push_back(static_cast(k)); } else { - // Interface selection: for now include any cut cell that has - // the required zero level set(s) intersecting it. - // A cell is included if: - // - the zero_required LSs actually cut the cell - // - the sign constraints on the non-zero LSs are satisfiable - // (at least one vertex per non-zero constraint). - bool has_zero_ls = true; - for (int v = 0; v < nv && has_zero_ls; ++v) - { - // Check eventually; for now, if the LS is marked as - // intersected in cell_domains, the zero surface exists. - } + if ((cut_active_mask & part.expr.zero_required) != part.expr.zero_required) + continue; - // Check that the intersecting LS indices match zero_required. - bool zero_ok = true; - for (const auto& clause : part.expr.clauses) + bool has_matching_zero_entity = false; + const int n_zero = ac.n_zero_entities(); + for (int z = 0; z < n_zero; ++z) { - if (clause.relation != Relation::EqualTo) continue; - const int li = clause.level_set_index; - const I bg_cell = cut_cells.parent_cell_ids[static_cast(k)]; - if (bg.domain(li, bg_cell) != cell::domain::intersected) + if (zero_entity_matches( + ac, z, part.dim, part.expr, cut_active_mask, bg, bg_cell)) { - zero_ok = false; + has_matching_zero_entity = true; break; } } - if (zero_ok) + if (has_matching_zero_entity) part.cut_cell_ids.push_back(static_cast(k)); } } diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index cd77d15..c91929a 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -14,7 +14,6 @@ #include "triangulation.h" #include -#include #include #include #include @@ -82,39 +81,173 @@ bool vertex_is_zero_for_level_set(const AdaptCell& adapt_cell, return (adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)] & bit) != 0; } +template +bool cell_contains_all_vertices(std::span cell_verts, + std::span entity_verts) +{ + for (const int v : entity_verts) + { + bool found = false; + for (const auto cv : cell_verts) + { + if (cv == v) + { + found = true; + break; + } + } + if (!found) + return false; + } + return true; +} + +template +void vertex_state_for_level_set(const AdaptCell& ac, + int vertex_id, + int level_set_id, + bool& is_negative, + bool& is_positive, + bool& is_zero) +{ + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; + const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; + is_zero = (zm & bit) != 0; + is_negative = !is_zero && ((nm & bit) != 0); + is_positive = !is_zero && !is_negative; +} + template -std::vector selected_entities(const HOMeshPart& part, - const AdaptCell& adapt_cell) +bool leaf_cell_matches_sign_requirements( + const AdaptCell& ac, + std::span cell_verts, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) { - if (part.expr.clauses.size() != 1 || part.expr.clauses.front().level_set_index != 0) + const int nls = std::min(bg.num_level_sets, 64); + for (int li = 0; li < nls; ++li) { - throw std::runtime_error( - "HOMeshPart output currently supports only one-clause single-level-set selections"); + const std::uint64_t bit = std::uint64_t(1) << li; + const bool require_neg = (expr.negative_required & bit) != 0; + const bool require_pos = (expr.positive_required & bit) != 0; + if (!require_neg && !require_pos) + continue; + + const bool ls_is_active = (cut_cell_active_mask & bit) != 0; + if (!ls_is_active) + { + const auto dom = bg.domain(li, parent_cell_id); + if (require_neg && dom != cell::domain::inside) + return false; + if (require_pos && dom != cell::domain::outside) + return false; + continue; + } + + bool has_neg = false; + bool has_pos = false; + for (const auto cv : cell_verts) + { + bool is_neg = false; + bool is_pos = false; + bool is_zero = false; + vertex_state_for_level_set(ac, static_cast(cv), li, is_neg, is_pos, is_zero); + has_neg = has_neg || is_neg; + has_pos = has_pos || is_pos; + } + + if (require_neg) + { + if (has_pos || !has_neg) + return false; + } + if (require_pos) + { + if (has_neg || !has_pos) + return false; + } } - const auto relation = part.expr.clauses.front().relation; - std::vector entities; + return true; +} - if (part.dim == adapt_cell.tdim) +template +bool zero_entity_matches(const HOMeshPart& part, + const AdaptCell& ac, + int zero_entity_index, + std::uint64_t cut_cell_active_mask, + I parent_cell_id) +{ + if (ac.zero_entity_dim[static_cast(zero_entity_index)] != part.dim) + return false; + + const auto zero_mask = ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; + if ((zero_mask & part.expr.zero_required) != part.expr.zero_required) + return false; + + if (part.expr.negative_required == 0 && part.expr.positive_required == 0) + return true; + + std::vector zero_verts; + const int zdim = ac.zero_entity_dim[static_cast(zero_entity_index)]; + const int zid = ac.zero_entity_id[static_cast(zero_entity_index)]; + if (zdim == 0) + { + zero_verts.push_back(zid); + } + else { - if (relation == Relation::EqualTo) - throw std::runtime_error("HOMeshPart output: phi = 0 is not a volume selection"); + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + zero_verts.reserve(verts.size()); + for (const auto v : verts) + zero_verts.push_back(static_cast(v)); + } - if (adapt_cell.cell_cert_tag_num_level_sets <= 0) - throw std::runtime_error("HOMeshPart output: missing cell certification tags"); + const int tdim = ac.tdim; + const int n_cells = ac.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) + { + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + if (!cell_contains_all_vertices(cell_verts, std::span(zero_verts))) + continue; - const auto target = (relation == Relation::LessThan) - ? CellCertTag::negative - : CellCertTag::positive; + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, part.expr, cut_cell_active_mask, *part.bg, parent_cell_id)) + { + return true; + } + } + + return false; +} +template +std::vector selected_entities(const HOMeshPart& part, + const AdaptCell& adapt_cell, + int cut_cell_id) +{ + std::vector entities; + const I parent_cell_id = + part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]; + const std::uint64_t cut_active_mask = + part.cut_cells->active_level_set_mask[static_cast(cut_cell_id)]; + + if (part.dim == adapt_cell.tdim) + { const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); entities.reserve(static_cast(n_cells)); for (int c = 0; c < n_cells; ++c) { - if (adapt_cell.get_cell_cert_tag(/*level_set_id=*/0, c) != target) + auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + if (!leaf_cell_matches_sign_requirements( + adapt_cell, verts, part.expr, cut_active_mask, *part.bg, parent_cell_id)) + { continue; + } - auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; SelectedEntity entity; entity.type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; entity.vertices.assign(verts.begin(), verts.end()); @@ -123,90 +256,35 @@ std::vector selected_entities(const HOMeshPart& part, return entities; } - if (relation != Relation::EqualTo) - { - throw std::runtime_error( - "HOMeshPart output: lower-dimensional direct export currently supports only phi = 0"); - } - - if (part.dim < 1 || part.dim >= adapt_cell.tdim) - throw std::runtime_error("HOMeshPart output: unsupported selection dimension"); - if (part.dim != adapt_cell.tdim - 1) - { - throw std::runtime_error( - "HOMeshPart output currently supports only codim-1 phi = 0 selections"); - } + if ((cut_active_mask & part.expr.zero_required) != part.expr.zero_required) + return entities; - std::map, SelectedEntity> unique_entities; - const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); - for (int c = 0; c < n_cells; ++c) + const int n_zero = adapt_cell.n_zero_entities(); + entities.reserve(static_cast(n_zero)); + for (int z = 0; z < n_zero; ++z) { - const auto cell_type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; - auto cell_verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; + if (!zero_entity_matches(part, adapt_cell, z, cut_active_mask, parent_cell_id)) + continue; - if (adapt_cell.tdim == 2) + const int zdim = adapt_cell.zero_entity_dim[static_cast(z)]; + const int zid = adapt_cell.zero_entity_id[static_cast(z)]; + SelectedEntity entity; + entity.type = (zdim == 0) + ? cell::type::point + : adapt_cell.entity_types[zdim][static_cast(zid)]; + if (zdim == 0) { - for (const auto& edge : cell::edges(cell_type)) - { - SelectedEntity entity; - entity.type = cell::type::interval; - entity.vertices = { - static_cast(cell_verts[static_cast(edge[0])]), - static_cast(cell_verts[static_cast(edge[1])])}; - - bool all_zero = true; - for (int gv : entity.vertices) - { - if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) - { - all_zero = false; - break; - } - } - if (!all_zero) - continue; - - auto key = entity.vertices; - std::sort(key.begin(), key.end()); - unique_entities.try_emplace(std::move(key), std::move(entity)); - } - continue; + entity.vertices.push_back(zid); } - - const int n_faces = cell::num_faces(cell_type); - for (int fi = 0; fi < n_faces; ++fi) + else { - auto local_face = cell::face_vertices(cell_type, fi); - SelectedEntity entity; - entity.type = cell::face_type(cell_type, fi); - entity.vertices.reserve(local_face.size()); - for (auto lv : local_face) - { - entity.vertices.push_back( - static_cast(cell_verts[static_cast(lv)])); - } - - bool all_zero = true; - for (int gv : entity.vertices) - { - if (!vertex_is_zero_for_level_set(adapt_cell, gv, /*level_set_id=*/0)) - { - all_zero = false; - break; - } - } - if (!all_zero) - continue; - - auto key = entity.vertices; - std::sort(key.begin(), key.end()); - unique_entities.try_emplace(std::move(key), std::move(entity)); + auto verts = adapt_cell.entity_to_vertex[zdim][static_cast(zid)]; + entity.vertices.assign(verts.begin(), verts.end()); } - } - entities.reserve(unique_entities.size()); - for (auto& [key, entity] : unique_entities) entities.push_back(std::move(entity)); + } + return entities; } @@ -476,7 +554,7 @@ void append_mesh_entity(mesh::CutMesh& out, out._vertex_coords.end(), output_coords.begin(), output_coords.end()); out._num_vertices += nv; - if (triangulate && !is_simplex(cell_type)) + if (triangulate && !is_simplex(cell_type) && cell::get_tdim(cell_type) >= 2) { std::vector local_ids(static_cast(nv)); std::iota(local_ids.begin(), local_ids.end(), 0); @@ -731,7 +809,7 @@ void append_entity_quadrature(quadrature::QuadratureRules& rules, return; } - if (triangulate && !is_simplex(cell_type)) + if (triangulate && !is_simplex(cell_type) && entity_dim >= 2) { std::vector local_ids(static_cast(nv)); std::iota(local_ids.begin(), local_ids.end(), 0); @@ -799,7 +877,7 @@ void append_cut_entities(mesh::CutMesh& out, for (std::int32_t cut_id : part.cut_cell_ids) { const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; - const auto entities = selected_entities(part, adapt_cell); + const auto entities = selected_entities(part, adapt_cell, cut_id); if (entities.empty()) continue; @@ -817,12 +895,18 @@ void append_cut_entities(mesh::CutMesh& out, root_vertex_flags.resize(entity.vertices.size(), 0); for (std::size_t j = 0; j < entity.vertices.size(); ++j) { - root_vertex_flags[j] = vertex_is_zero_for_level_set( - adapt_cell, - entity.vertices[j], - /*level_set_id=*/0) - ? 1 - : 0; + const auto zm = adapt_cell.zero_mask_per_vertex[ + static_cast(entity.vertices[j])]; + bool is_root = false; + if (part.expr.zero_required != 0) + { + is_root = (zm & part.expr.zero_required) == part.expr.zero_required; + } + else + { + is_root = zm != 0; + } + root_vertex_flags[j] = is_root ? 1 : 0; } } @@ -920,11 +1004,6 @@ mesh::CutMesh visualization_mesh(const HOMeshPart& part, { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - if (part.bg->num_level_sets != 1) - { - throw std::runtime_error( - "HOMeshPart output currently supports exactly one level set"); - } mesh::CutMesh out; out._gdim = part.bg->mesh->gdim; diff --git a/cpp/src/prism_midpoint_split.h b/cpp/src/prism_midpoint_split.h index b29cb27..7f01da8 100644 --- a/cpp/src/prism_midpoint_split.h +++ b/cpp/src/prism_midpoint_split.h @@ -65,6 +65,35 @@ family classify_family(std::span prism_tokens); /// suggested by the tetra-prism analysis. PrismRoleAnalysis analyze_tetra_derived_prism(std::span prism_tokens); +inline int parent_tetrahedron_edge_token(int token_a, int token_b) +{ + if (is_root_token(token_a) || is_root_token(token_b)) + { + throw std::runtime_error( + "parent_tetrahedron_edge_token: midpoint endpoints must be original vertices"); + } + + const int a = token_a - 100; + const int b = token_b - 100; + if (a < 0 || a >= 4 || b < 0 || b >= 4) + { + throw std::runtime_error( + "parent_tetrahedron_edge_token: invalid original-vertex token"); + } + + constexpr std::array parent_edges = {{ + {0, 1}, {1, 2}, {2, 0}, {0, 3}, {1, 3}, {2, 3}, + }}; + for (std::size_t e = 0; e < parent_edges.size(); ++e) + { + const LocalEdge edge = parent_edges[e]; + if ((edge.a == a && edge.b == b) || (edge.a == b && edge.b == a)) + return static_cast(e); + } + + throw std::runtime_error("parent_tetrahedron_edge_token: parent edge not found"); +} + template struct MidpointInsertionResult { @@ -112,7 +141,9 @@ MidpointInsertionResult create_default_midpoints( for (const LocalEdge edge : result.analysis.midpoint_edges) { - result.added_vertex_tokens.push_back(next_token_base++); + result.added_vertex_tokens.push_back(parent_tetrahedron_edge_token( + prism_tokens[static_cast(edge.a)], + prism_tokens[static_cast(edge.b)])); result.added_vertex_edges.push_back(edge); for (int d = 0; d < coord_dim; ++d) { @@ -121,6 +152,7 @@ MidpointInsertionResult create_default_midpoints( result.added_vertex_coords.push_back(T(0.5) * (xa + xb)); } } + (void) next_token_base; return result; } @@ -327,19 +359,30 @@ MidpointInsertionResult split_tetra_derived_prism( std::vector> canonical_tets; if (result.analysis.split_family == family::roots3) { - // Build the three original-vertex corner tetrahedra first. The - // remaining central cell has vertices {3,4,5,6,7,8} and is an - // octahedron, not a prism. Split it into four tetrahedra around the - // root-midpoint diagonal (5,6), which yields one central tet of the - // preferred midpoint-root-root-root form. + // Build the three original-vertex corner tetrahedra first. + // + // The remaining central cell has vertices {3,4,5,6,7,8}: + // - {3,4,5} are the root vertices (one on each tetra edge) + // - {6,7,8} are midpoints on the non-root triangle edges + // + // This remaining polyhedron is a triangular antiprism with boundary + // triangles: + // - top: {3,4,5} + // - bottom: {6,7,8} + // - sides: {3,4,6}, {4,5,7}, {5,3,8}, {3,6,8}, {4,7,6}, {5,8,7} + // + // A 4-tet decomposition (total 7 tets incl. corners) is required here: + // a 3-tet "prism" split does not cover the antiprism volume. canonical_tets = { std::array{0, 6, 8, 3}, std::array{1, 7, 6, 4}, std::array{2, 8, 7, 5}, - std::array{5, 6, 3, 4}, - std::array{5, 6, 4, 7}, - std::array{5, 6, 7, 8}, - std::array{5, 6, 8, 3}, + // Central triangular antiprism {3,4,5,6,7,8} split into 4 tets. + // This corresponds to inserting the internal diagonal (3,7). + std::array{3, 4, 6, 7}, + std::array{3, 5, 8, 7}, + std::array{3, 4, 5, 7}, + std::array{3, 6, 8, 7}, }; } else diff --git a/cpp/src/quad_midpoint_split.h b/cpp/src/quad_midpoint_split.h index e3833ab..b0f4d8e 100644 --- a/cpp/src/quad_midpoint_split.h +++ b/cpp/src/quad_midpoint_split.h @@ -35,6 +35,35 @@ bool is_root_token(int token); QuadRoleAnalysis analyze_triangle_derived_quadrilateral( std::span quad_tokens); +inline int parent_triangle_edge_token(int token_a, int token_b) +{ + if (is_root_token(token_a) || is_root_token(token_b)) + { + throw std::runtime_error( + "parent_triangle_edge_token: midpoint endpoints must be original vertices"); + } + + const int a = token_a - 100; + const int b = token_b - 100; + if (a < 0 || a >= 3 || b < 0 || b >= 3) + { + throw std::runtime_error( + "parent_triangle_edge_token: invalid original-vertex token"); + } + + constexpr std::array parent_edges = {{ + {0, 1}, {1, 2}, {2, 0}, + }}; + for (std::size_t e = 0; e < parent_edges.size(); ++e) + { + const LocalEdge edge = parent_edges[e]; + if ((edge.a == a && edge.b == b) || (edge.a == b && edge.b == a)) + return static_cast(e); + } + + throw std::runtime_error("parent_triangle_edge_token: parent edge not found"); +} + template struct MidpointInsertionResult { @@ -70,8 +99,12 @@ MidpointInsertionResult split_triangle_derived_quadrilateral( MidpointInsertionResult result; result.analysis = analyze_triangle_derived_quadrilateral(quad_tokens); - result.added_vertex_tokens.push_back(next_token_base++); result.added_vertex_edges.push_back(result.analysis.midpoint_edge); + const LocalEdge midpoint_edge = result.analysis.midpoint_edge; + result.added_vertex_tokens.push_back(parent_triangle_edge_token( + quad_tokens[static_cast(midpoint_edge.a)], + quad_tokens[static_cast(midpoint_edge.b)])); + (void) next_token_base; for (int d = 0; d < coord_dim; ++d) { @@ -101,9 +134,9 @@ MidpointInsertionResult split_triangle_derived_quadrilateral( }; const std::array, 3> canonical_triangles = {{ - {2, 0, 4}, - {4, 0, 1}, - {3, 4, 1}, + {2, 1, 4}, + {4, 1, 0}, + {3, 4, 0}, }}; result.triangles.reserve(canonical_triangles.size()); diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp index cb03aaa..40a6cbb 100644 --- a/cpp/src/refine_cell.cpp +++ b/cpp/src/refine_cell.cpp @@ -352,6 +352,11 @@ void apply_topology_update_preserve_certification( old_num_cells, old_cell_ids_for_new_cells); clear_topology_caches(adapt_cell); + + const int nls = std::max(adapt_cell.cell_cert_tag_num_level_sets, + adapt_cell.edge_root_tag_num_level_sets); + recompute_active_level_set_masks(adapt_cell, nls); + rebuild_zero_entity_inventory(adapt_cell); } diff --git a/cpp/src/selection_expr.cpp b/cpp/src/selection_expr.cpp index 286de3b..ebd0bab 100644 --- a/cpp/src/selection_expr.cpp +++ b/cpp/src/selection_expr.cpp @@ -190,7 +190,7 @@ int infer_selection_dim(const SelectionExpr& expr, int tdim) for (const auto& clause : expr.clauses) if (clause.relation == Relation::EqualTo) ++n_eq; - return tdim - n_eq; + return std::max(0, tdim - n_eq); } } // namespace cutcells diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 9d45eb7..1f1d8c5 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -21,6 +21,28 @@ def _load_cpp_module(): if matching: candidates = matching + # If we're running from a source tree, prefer an up-to-date in-tree build. + # The repo keeps `cutcells/wrapper.cpp` next to this file; if the extension + # is older than the wrapper source, it's almost certainly stale (and tends + # to miss newer bindings like HOMeshPart.visualization_mesh, etc.). + wrapper_cpp = _Path(__file__).with_name("wrapper.cpp") + if wrapper_cpp.exists(): + try: + wrapper_ts = wrapper_cpp.stat().st_mtime + candidates = [ + c for c in candidates + if c.exists() and c.stat().st_mtime >= wrapper_ts + ] + except OSError: + pass + + # Choose the newest candidate (by mtime) to avoid picking an old leftover. + if candidates: + try: + candidates = sorted(candidates, key=lambda p: p.stat().st_mtime, reverse=True) + except OSError: + pass + lib_candidates = [ build_dir / "cutcells_cpp" / "src" / "libcutcells.dylib", _Path(__file__).resolve().parents[2] @@ -34,20 +56,116 @@ def _load_cpp_module(): _ctypes.CDLL(str(lib), mode=_ctypes.RTLD_GLOBAL) break - spec = _importlib_util.spec_from_file_location( - "cutcells._cutcellscpp", candidates[0] - ) - if spec is None or spec.loader is None: - raise ImportError(f"Could not load extension from {candidates[0]}") + if candidates: + try: + spec = _importlib_util.spec_from_file_location( + "cutcells._cutcellscpp", candidates[0] + ) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load extension from {candidates[0]}") + + module = _importlib_util.module_from_spec(spec) + _sys.modules["cutcells._cutcellscpp"] = module + spec.loader.exec_module(module) + + # Basic API sanity check for common dev/test usage. If this fails, + # fall back to the installed extension module. + if not ( + hasattr(module, "HOMeshPart_float64") + and hasattr(module.HOMeshPart_float64, "visualization_mesh") + ): + raise ImportError(f"Stale in-tree extension loaded from {candidates[0]}") + + return module + except Exception: + _sys.modules.pop("cutcells._cutcellscpp", None) + + # If the extension was installed into the package directory (normal wheel + # install), prefer loading it from right next to this __init__.py. + # The sys.path scan below intentionally skips this package dir to avoid + # re-import loops in a source-tree checkout, so without this branch an + # installed package cannot load its own extension. + this_pkg_dir = _Path(__file__).resolve().parent + ext_suffix = _sysconfig.get_config_var("EXT_SUFFIX") + local_sos = [] + try: + for so in this_pkg_dir.glob("_cutcellscpp*.so"): + if ext_suffix and not so.name.endswith(ext_suffix): + continue + local_sos.append(so) + except Exception: + local_sos = [] + + if local_sos: + local_sos = sorted(local_sos, key=lambda p: p.stat().st_mtime, reverse=True) + so_path = local_sos[0] + + lib_candidates = [ + this_pkg_dir / "libcutcells.dylib", + this_pkg_dir.parent / "libcutcells.dylib", + this_pkg_dir.parent / "lib" / "libcutcells.dylib", + ] + for lib in lib_candidates: + try: + if lib.exists(): + _ctypes.CDLL(str(lib), mode=_ctypes.RTLD_GLOBAL) + break + except Exception: + pass + spec = _importlib_util.spec_from_file_location("cutcells._cutcellscpp", so_path) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load installed extension from {so_path}") module = _importlib_util.module_from_spec(spec) _sys.modules["cutcells._cutcellscpp"] = module spec.loader.exec_module(module) return module - from . import _cutcellscpp as module + # Fall back to an installed extension (e.g. site-packages) when running from + # a source tree without an up-to-date in-tree build. + installed = [] + for entry in list(_sys.path): + try: + pkg_dir = _Path(entry) / "cutcells" + if not pkg_dir.exists() or pkg_dir.resolve() == this_pkg_dir: + continue + for so in pkg_dir.glob("_cutcellscpp*.so"): + if ext_suffix and not so.name.endswith(ext_suffix): + continue + installed.append(so) + except Exception: + continue + + if installed: + installed = sorted(installed, key=lambda p: p.stat().st_mtime, reverse=True) + so_path = installed[0] + + # Try to preload the companion dylib if it was packaged separately. + lib_candidates = [ + so_path.parent / "libcutcells.dylib", + so_path.parent.parent / "libcutcells.dylib", + so_path.parent.parent / "lib" / "libcutcells.dylib", + ] + for lib in lib_candidates: + try: + if lib.exists(): + _ctypes.CDLL(str(lib), mode=_ctypes.RTLD_GLOBAL) + break + except Exception: + pass + + spec = _importlib_util.spec_from_file_location("cutcells._cutcellscpp", so_path) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load installed extension from {so_path}") + module = _importlib_util.module_from_spec(spec) + _sys.modules["cutcells._cutcellscpp"] = module + spec.loader.exec_module(module) + return module - return module + raise ImportError( + "Could not load cutcells extension: no in-tree build found and no installed " + "cutcells/_cutcellscpp*.so found on sys.path." + ) _cutcellscpp = _load_cpp_module() @@ -83,6 +201,7 @@ def _load_cpp_module(): create_level_set_mesh_data = _cutcellscpp.create_level_set_mesh_data create_level_set_function = _cutcellscpp.create_level_set_function create_level_set = _cutcellscpp.create_level_set +interpolate_level_set = _cutcellscpp.interpolate_level_set make_adapt_cell = _cutcellscpp.make_adapt_cell build_edges = _cutcellscpp.build_edges make_cell_level_set = _cutcellscpp.make_cell_level_set @@ -119,9 +238,12 @@ def _load_cpp_module(): HOMeshPart = _cutcellscpp.HOMeshPart from .mesh_utils import ( + cutmesh_to_pyvista, mesh_from_pyvista, rectangle_triangle_mesh, rectangle_quad_mesh, + safe_part_name, + structured_triangle_mesh_view, box_tetrahedron_mesh, box_hex_mesh, ) diff --git a/python/cutcells/mesh_utils.py b/python/cutcells/mesh_utils.py index c7b508f..abd0b3d 100644 --- a/python/cutcells/mesh_utils.py +++ b/python/cutcells/mesh_utils.py @@ -70,6 +70,65 @@ def mesh_from_pyvista(grid, *, tdim=None): ) +def safe_part_name(expr): + """Return a filesystem-safe compact name for a level-set selection.""" + return ( + str(expr) + .replace(" ", "") + .replace("<", "lt") + .replace(">", "gt") + .replace("=", "eq") + ) + + +def cutmesh_to_pyvista(cut_mesh): + """Convert a cutcells CutMesh to a pyvista.UnstructuredGrid.""" + if pv is None: + raise ImportError("pyvista is required for cutmesh_to_pyvista") + + points = np.asarray(cut_mesh.vertex_coords, dtype=np.float64) + if points.ndim == 2 and points.shape[1] == 2: + points = np.column_stack([points, np.zeros(points.shape[0])]) + + return pv.UnstructuredGrid( + np.asarray(cut_mesh.cells, dtype=np.int64), + np.asarray(cut_mesh.vtk_types, dtype=np.uint8), + points, + ) + + +def structured_triangle_mesh_view(x0, y0, x1, y1, nx, ny): + """Pure NumPy structured triangular MeshView on [x0,x1] x [y0,y1]. + + Each quadrilateral grid cell is split along the bottom-left to top-right + diagonal. This helper does not require pyvista. + """ + from . import MeshView + + x = np.linspace(x0, x1, num=nx) + y = np.linspace(y0, y1, num=ny) + xx, yy = np.meshgrid(x, y) + coordinates = np.column_stack([xx.ravel(), yy.ravel()]).astype(np.float64) + + connectivity = [] + cell_types = [] + for j in range(ny - 1): + for i in range(nx - 1): + v0 = j * nx + i + v1 = v0 + 1 + v3 = v0 + nx + v2 = v3 + 1 + connectivity.extend([v0, v1, v2]) + connectivity.extend([v0, v2, v3]) + cell_types.extend([5, 5]) # VTK_TRIANGLE + + connectivity = np.asarray(connectivity, dtype=np.int32) + offsets = np.arange(0, connectivity.size + 1, 3, dtype=np.int32) + cell_types = np.asarray(cell_types, dtype=np.int32) + + return MeshView(coordinates, connectivity, offsets, cell_types, tdim=2) + + # --------------------------------------------------------------------------- # 2D structured meshes # --------------------------------------------------------------------------- diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 4710e17..4d3aa94 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -721,24 +721,22 @@ inline bool part_mode_is_cut_only(std::string_view mode) template cutcells::mesh::CutMesh part_visualization_mesh( const cutcells::HOMeshPart& part, - std::string_view mode, - bool triangulate) + std::string_view mode) { const bool cut_only = part_mode_is_cut_only(mode); return cutcells::output::visualization_mesh( - part, /*include_uncut_cells=*/!cut_only, triangulate); + part, /*include_uncut_cells=*/!cut_only, /*triangulate=*/false); } template cutcells::quadrature::QuadratureRules part_quadrature( const cutcells::HOMeshPart& part, int order, - std::string_view mode, - bool triangulate) + std::string_view mode) { const bool cut_only = part_mode_is_cut_only(mode); return cutcells::output::quadrature_rules( - part, order, /*include_uncut_cells=*/!cut_only, triangulate); + part, order, /*include_uncut_cells=*/!cut_only, /*triangulate=*/false); } template @@ -1207,6 +1205,52 @@ void declare_meshview_and_levelset(nb::module_& m, const std::string& suffix) nb::arg("name") = "phi", "Interpolate a batched callable phi(X) at higher-order level-set dof coordinates."); + m.def( + "interpolate_level_set", + [](const MeshViewT& mesh, nb::callable phi, int degree, + const std::string& name) + { + LevelSetMeshDataT mesh_data; + { + nb::gil_scoped_release release; + mesh_data = cutcells::create_level_set_mesh_data(mesh, degree, T(-1)); + } + + auto mesh_data_ptr = std::make_shared(std::move(mesh_data)); + const std::size_t num_dofs = static_cast(mesh_data_ptr->num_dofs()); + const std::size_t gdim = static_cast(mesh_data_ptr->gdim); + + // Expose as (num_dofs, gdim), i.e. one point per row (matches tests + docs). + // Keep `mesh_data_ptr` alive via `owner` in case the callback holds onto X. + nb::object owner = nb::cast(mesh_data_ptr); + nb::ndarray X( + mesh_data_ptr->dof_coordinates.data(), + {num_dofs, gdim}, + owner); + + // Intentional single batched callback invocation. + nb::object values_obj = phi(X); + auto values = nb::cast>(values_obj); + if (static_cast(values.size()) != num_dofs) + { + throw std::runtime_error( + "interpolate_level_set: callback must return a 1D array with length num_dofs"); + } + + return cutcells::create_level_set_function( + std::move(mesh_data_ptr), + std::span( + values.data(), + static_cast(values.size())), + name); + }, + nb::arg("mesh"), + nb::arg("phi"), + nb::arg("degree"), + nb::arg("name") = "phi", + "Interpolate a batched callable phi(X) on higher-order level-set DOF coordinates.\n" + "X is passed as an ndarray of shape (num_dofs, gdim)."); + // ---- cut_mesh_view ---- // Cuts a MeshView using a LevelSetFunction, returning a CutMesh. // Nodal level-set values are taken from ls.nodal_values if available, @@ -1771,6 +1815,15 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::handle()); }, nb::rv_policy::reference_internal) + .def_prop_ro("active_level_set_mask", + [](const HOCutResult& self) { + return nb::ndarray( + self.cut_cells.active_level_set_mask.data(), + {self.cut_cells.active_level_set_mask.size()}, + nb::handle()); + }, + nb::rv_policy::reference_internal, + "Per cut-cell bitmask of level sets intersecting the background cell.") .def_prop_ro("cell_domains", [](const HOCutResult& self) { const int* data = reinterpret_cast( @@ -1841,38 +1894,33 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::rv_policy::reference_internal) .def( "visualization_mesh", - [](const PartT& self, const std::string& mode, bool triangulate) { + [](const PartT& self, const std::string& mode) { nb::gil_scoped_release release; - return part_visualization_mesh(self, mode, triangulate); + return part_visualization_mesh(self, mode); }, nb::arg("mode") = "full", - nb::arg("triangulate") = false, - "Return a straight visualization mesh for a one-level-set HOMeshPart.\n" - "This bridge currently supports only single-level-set selections.") + "Return a straight visualization mesh for an HOMeshPart.") .def( "quadrature", - [](const PartT& self, int order, const std::string& mode, bool triangulate) { + [](const PartT& self, int order, const std::string& mode) { nb::gil_scoped_release release; - return part_quadrature(self, order, mode, triangulate); + return part_quadrature(self, order, mode); }, nb::arg("order") = 3, nb::arg("mode") = "full", - nb::arg("triangulate") = false, "Return straight quadrature rules for a one-level-set HOMeshPart.\n" "This bridge currently supports only single-level-set selections.") .def( "write_vtu", [](const PartT& self, const std::string& filename, - const std::string& mode, - bool triangulate) { + const std::string& mode) { nb::gil_scoped_release release; - auto vis = part_visualization_mesh(self, mode, triangulate); + auto vis = part_visualization_mesh(self, mode); io::write_vtk(filename, vis); }, nb::arg("filename"), nb::arg("mode") = "full", - nb::arg("triangulate") = false, "Write a straight visualization VTU file for a one-level-set HOMeshPart."); // --- ho_cut() factory --- @@ -1940,6 +1988,7 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::class_(m, adapt_name.c_str(), "Adaptive local cell topology") .def(nb::init<>()) .def_prop_ro("tdim", [](const AdaptCellT& self) { return self.tdim; }) + .def_prop_ro("active_level_set_mask", [](const AdaptCellT& self) { return self.active_level_set_mask; }) .def("num_vertices", &AdaptCellT::n_vertices) .def("num_edges", [](const AdaptCellT& self) { return self.n_entities(1); }) .def("num_cells", [](const AdaptCellT& self) { return self.n_entities(self.tdim); }) @@ -1964,6 +2013,99 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_mask_per_vertex", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_mask_per_vertex.data(), + {self.zero_mask_per_vertex.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "negative_mask_per_vertex", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.negative_mask_per_vertex.data(), + {self.negative_mask_per_vertex.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_active_level_set_mask", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.cell_active_level_set_mask.data(), + {self.cell_active_level_set_mask.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "num_zero_entities", + [](const AdaptCellT& self) { return self.n_zero_entities(); }) + .def_prop_ro( + "zero_entity_dim", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_dim.data(), + {self.zero_entity_dim.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_entity_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_id.data(), + {self.zero_entity_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_entity_zero_mask", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_zero_mask.data(), + {self.zero_entity_zero_mask.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_entity_is_owned", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_is_owned.data(), + {self.zero_entity_is_owned.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_entity_parent_dim", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_parent_dim.data(), + {self.zero_entity_parent_dim.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "zero_entity_parent_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.zero_entity_parent_id.data(), + {self.zero_entity_parent_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) .def_prop_ro( "edge_connectivity", [](const AdaptCellT& self) @@ -2271,19 +2413,20 @@ void declare_certification(nb::module_& m, const std::string& suffix) m.def( "process_ready_to_cut_cells", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, - T zero_tol, T sign_tol, int edge_max_depth) + T zero_tol, T sign_tol, int edge_max_depth, bool triangulate_cut_parts) { nb::gil_scoped_release release; cutcells::process_ready_to_cut_cells( adapt_cell, ls_cell, level_set_id, - zero_tol, sign_tol, edge_max_depth); + zero_tol, sign_tol, edge_max_depth, triangulate_cut_parts); }, nb::arg("adapt_cell"), nb::arg("level_set_cell"), nb::arg("level_set_id"), nb::arg("zero_tol") = T(1e-12), nb::arg("sign_tol") = T(1e-12), - nb::arg("edge_max_depth") = 20); + nb::arg("edge_max_depth") = 20, + nb::arg("triangulate_cut_parts") = false); m.def( "refine_green_on_multiple_root_edges", @@ -2308,12 +2451,14 @@ void declare_certification(nb::module_& m, const std::string& suffix) m.def( "certify_refine_and_process_ready_cells", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, - int max_iterations, T zero_tol, T sign_tol, int edge_max_depth) + int max_iterations, T zero_tol, T sign_tol, int edge_max_depth, + bool triangulate_cut_parts) { nb::gil_scoped_release release; cutcells::certify_refine_and_process_ready_cells( adapt_cell, ls_cell, level_set_id, - max_iterations, zero_tol, sign_tol, edge_max_depth); + max_iterations, zero_tol, sign_tol, edge_max_depth, + triangulate_cut_parts); }, nb::arg("adapt_cell"), nb::arg("level_set_cell"), @@ -2321,7 +2466,8 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("max_iterations") = 8, nb::arg("zero_tol") = T(1e-12), nb::arg("sign_tol") = T(1e-12), - nb::arg("edge_max_depth") = 20); + nb::arg("edge_max_depth") = 20, + nb::arg("triangulate_cut_parts") = false); m.def( "certify_and_refine", diff --git a/python/demo/demo_meshview_ho_cut.py b/python/demo/demo_meshview_ho_cut.py index d4c6773..10fff2b 100644 --- a/python/demo/demo_meshview_ho_cut.py +++ b/python/demo/demo_meshview_ho_cut.py @@ -123,9 +123,9 @@ def main() -> None: print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") print("Building straight hybrid visualization meshes from HOMeshPart ...") - inside_full = negative.visualization_mesh(mode="full", triangulate=False) - inside_cut = negative.visualization_mesh(mode="cut_only", triangulate=False) - interface_mesh = interface.visualization_mesh(mode="cut_only", triangulate=False) + inside_full = negative.visualization_mesh(mode="full") + inside_cut = negative.visualization_mesh(mode="cut_only") + interface_mesh = interface.visualization_mesh(mode="cut_only") print(f" inside full cells={len(np.asarray(inside_full.types))}") print(f" inside cut-only cells={len(np.asarray(inside_cut.types))}") @@ -135,17 +135,14 @@ def main() -> None: q_inside_full = negative.quadrature( order=args.quadrature_order, mode="full", - triangulate=False, ) q_inside_cut = negative.quadrature( order=args.quadrature_order, mode="cut_only", - triangulate=False, ) q_interface = interface.quadrature( order=args.quadrature_order, mode="cut_only", - triangulate=False, ) print(f" inside full quadrature sum = {q_inside_full.weights.sum():.12g}") @@ -156,9 +153,9 @@ def main() -> None: inside_cut_path = args.output_dir / "phi_negative_cut_only_straight.vtu" interface_path = args.output_dir / "phi_interface_straight.vtu" - negative.write_vtu(str(inside_full_path), mode="full", triangulate=False) - negative.write_vtu(str(inside_cut_path), mode="cut_only", triangulate=False) - interface.write_vtu(str(interface_path), mode="cut_only", triangulate=False) + negative.write_vtu(str(inside_full_path), mode="full") + negative.write_vtu(str(inside_cut_path), mode="cut_only") + interface.write_vtu(str(interface_path), mode="cut_only") print(f" wrote {inside_full_path}") print(f" wrote {inside_cut_path}") diff --git a/python/demo/demo_meshview_ho_cut_triangulated.py b/python/demo/demo_meshview_ho_cut_triangulated.py index e6fbe1a..ab4c96b 100644 --- a/python/demo/demo_meshview_ho_cut_triangulated.py +++ b/python/demo/demo_meshview_ho_cut_triangulated.py @@ -419,12 +419,12 @@ def main() -> None: print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") - print("Building triangulated straight visualization meshes from HOMeshPart ...") - inside_full = negative.visualization_mesh(mode="full", triangulate=True) - inside_cut = negative.visualization_mesh(mode="cut_only", triangulate=True) - outside_full = positive.visualization_mesh(mode="full", triangulate=True) - outside_cut = positive.visualization_mesh(mode="cut_only", triangulate=True) - interface_mesh = interface.visualization_mesh(mode="cut_only", triangulate=True) + print("Building straight visualization meshes from HOMeshPart ...") + inside_full = negative.visualization_mesh(mode="full") + inside_cut = negative.visualization_mesh(mode="cut_only") + outside_full = positive.visualization_mesh(mode="full") + outside_cut = positive.visualization_mesh(mode="cut_only") + interface_mesh = interface.visualization_mesh(mode="cut_only") print(f" inside full cells={len(np.asarray(inside_full.types))}") print(f" inside cut-only cells={len(np.asarray(inside_cut.types))}") @@ -432,12 +432,12 @@ def main() -> None: print(f" outside full cells={len(np.asarray(outside_full.types))}") print(f" outside cut-only cells={len(np.asarray(outside_cut.types))}") - print("Building non-triangulated reference meshes for the new-edge check ...") - inside_full_base = negative.visualization_mesh(mode="full", triangulate=False) - inside_cut_base = negative.visualization_mesh(mode="cut_only", triangulate=False) - outside_full_base = positive.visualization_mesh(mode="full", triangulate=False) - outside_cut_base = positive.visualization_mesh(mode="cut_only", triangulate=False) - interface_base = interface.visualization_mesh(mode="cut_only", triangulate=False) + print("Building reference meshes for the new-edge check ...") + inside_full_base = negative.visualization_mesh(mode="full") + inside_cut_base = negative.visualization_mesh(mode="cut_only") + outside_full_base = positive.visualization_mesh(mode="full") + outside_cut_base = positive.visualization_mesh(mode="cut_only") + interface_base = interface.visualization_mesh(mode="cut_only") print("Checking whether triangulation-created edges carry level-set roots ...") edge_checks = { @@ -482,7 +482,7 @@ def main() -> None: ] for part, path, mode in mesh_outputs: - part.write_vtu(str(path), mode=mode, triangulate=True) + part.write_vtu(str(path), mode=mode) print(f" wrote {path}") if args.no_plot: diff --git a/python/tests/test_ho_part_straight_output.py b/python/tests/test_ho_part_straight_output.py index 06ddf12..6f37a48 100644 --- a/python/tests/test_ho_part_straight_output.py +++ b/python/tests/test_ho_part_straight_output.py @@ -71,6 +71,40 @@ def _edge_counts(cut_mesh): return counts +def test_triangle_lut_triangulated_quad_connects_uncut_edge_vertices_to_adjacent_roots(): + vertex_coordinates = np.array( + [ + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + ], + dtype=np.float64, + ) + level_set_values = np.array([-0.7, 0.3, 0.3], dtype=np.float64) + + cut = cutcells.cut( + cutcells.CellType.triangle, + vertex_coordinates, + 2, + level_set_values, + "phi>0", + True, + ) + + parent_tokens = np.asarray(cut.vertex_parent_entity, dtype=np.int32) + connectivity = np.asarray(cut.connectivity, dtype=np.int32).reshape(-1, 3) + token_tris = {tuple(sorted(int(parent_tokens[v]) for v in tri)) for tri in connectivity} + + assert token_tris == { + tuple(sorted((101, 0, 1))), + tuple(sorted((1, 0, 2))), + tuple(sorted((102, 1, 2))), + } + + def test_homeshpart_straight_output_bridge(tmp_path: Path): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( @@ -87,30 +121,20 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): vis_cut = negative.visualization_mesh(mode="cut_only") vis_full = negative.visualization_mesh(mode="full") vis_interface = interface.visualization_mesh(mode="cut_only") - vis_cut_triangulated = negative.visualization_mesh(mode="cut_only", triangulate=True) - vis_interface_triangulated = interface.visualization_mesh( - mode="cut_only", triangulate=True - ) assert len(np.asarray(vis_cut.types)) > 0 assert len(np.asarray(vis_full.types)) >= len(np.asarray(vis_cut.types)) assert len(np.asarray(vis_interface.types)) > 0 assert np.unique(np.asarray(vis_cut.vtk_types)).tolist() == [13] assert np.unique(np.asarray(vis_interface.vtk_types)).tolist() == [9] - assert np.unique(np.asarray(vis_cut_triangulated.vtk_types)).tolist() == [10] - assert np.unique(np.asarray(vis_interface_triangulated.vtk_types)).tolist() == [5] q_cut = negative.quadrature(order=3, mode="cut_only") q_full = negative.quadrature(order=3, mode="full") q_interface = interface.quadrature(order=3, mode="cut_only") - q_cut_triangulated = negative.quadrature( - order=3, mode="cut_only", triangulate=True - ) assert q_cut.weights.sum() > 0.0 assert q_full.weights.sum() >= q_cut.weights.sum() assert q_interface.weights.sum() > 0.0 - assert np.isclose(q_cut_triangulated.weights.sum(), q_cut.weights.sum()) negative_path = tmp_path / "negative_full.vtu" interface_path = tmp_path / "interface.vtu" @@ -123,7 +147,7 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): assert "Name=\"types\" format=\"ascii\">9 " in interface_path.read_text() -def test_triangulated_interface_preserves_quad_boundary_without_bowtie(): +def test_interface_output_reflects_adaptcell_quad_leaf(): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( mesh, @@ -135,25 +159,17 @@ def test_triangulated_interface_preserves_quad_boundary_without_bowtie(): result = cutcells.cut(mesh, ls) interface = result["phi = 0"] - vis_interface = interface.visualization_mesh(mode="cut_only", triangulate=False) - vis_interface_triangulated = interface.visualization_mesh( - mode="cut_only", triangulate=True - ) + vis_interface = interface.visualization_mesh(mode="cut_only") quad_edges = _edge_counts(vis_interface) - tri_edges = _edge_counts(vis_interface_triangulated) quad_boundary = {edge for edge, count in quad_edges.items() if count == 1} - tri_boundary = {edge for edge, count in tri_edges.items() if count == 1} - tri_interior = {edge for edge, count in tri_edges.items() if count == 2} assert np.asarray(vis_interface.vtk_types).tolist() == [9] - assert np.asarray(vis_interface_triangulated.vtk_types).tolist() == [5, 5] - assert tri_boundary == quad_boundary - assert len(tri_interior) == 1 + assert len(quad_boundary) == 4 -def test_triangle_volume_output_uses_quad_midpoint_split(): +def test_triangle_volume_output_reflects_adaptcell_quad_leaf(): mesh = _single_triangle_mesh() ls = cutcells.create_level_set( mesh, @@ -165,16 +181,9 @@ def test_triangle_volume_output_uses_quad_midpoint_split(): result = cutcells.cut(mesh, ls) negative = result["phi < 0"] - vis_base = negative.visualization_mesh(mode="cut_only", triangulate=False) - vis_triangulated = negative.visualization_mesh(mode="cut_only", triangulate=True) - q_base = negative.quadrature(order=3, mode="cut_only", triangulate=False) - q_triangulated = negative.quadrature(order=3, mode="cut_only", triangulate=True) - - tri_points = np.asarray(vis_triangulated.vertex_coords, dtype=np.float64) + vis_base = negative.visualization_mesh(mode="cut_only") + q_base = negative.quadrature(order=3, mode="cut_only") assert np.asarray(vis_base.vtk_types).tolist() == [9] - assert np.asarray(vis_triangulated.vtk_types).tolist() == [5, 5, 5] assert np.asarray(vis_base.vertex_coords).shape[0] == 4 - assert tri_points.shape[0] == 5 - assert np.any(np.all(np.isclose(tri_points, np.array([1.0, 0.5])), axis=1)) - assert np.isclose(q_triangulated.weights.sum(), q_base.weights.sum()) + assert q_base.weights.sum() > 0.0 diff --git a/python/tests/test_mesh_utils.py b/python/tests/test_mesh_utils.py index 9f57f3b..45924ad 100644 --- a/python/tests/test_mesh_utils.py +++ b/python/tests/test_mesh_utils.py @@ -2,6 +2,18 @@ import cutcells +def test_structured_triangle_mesh_view(): + mesh = cutcells.structured_triangle_mesh_view(-1, -1, 1, 1, 5, 4) + assert mesh.tdim == 2 + assert mesh.gdim == 2 + assert mesh.num_nodes() == 20 + assert mesh.num_cells() == 24 + + +def test_safe_part_name(): + assert cutcells.safe_part_name("phi_left = 0 and phi_right < 0") == "phi_lefteq0andphi_rightlt0" + + def test_rectangle_triangle(): grid = cutcells.rectangle_triangle_mesh(-1, -1, 1, 1, 10, 10) assert grid.n_points == 100 diff --git a/python/tests/test_tetra_triangulation_analysis.py b/python/tests/test_tetra_triangulation_analysis.py index d1a65cb..c6d9e88 100644 --- a/python/tests/test_tetra_triangulation_analysis.py +++ b/python/tests/test_tetra_triangulation_analysis.py @@ -14,7 +14,7 @@ def test_tetra_parent_summary_excludes_zero_only_edges_from_interior_root_count( assert summary["new_edges_with_interior_roots"] == 0 assert summary["new_edges_with_zero_tag"] == 18 assert summary["new_edges_with_zero_endpoint"] == 122 - assert summary["total_new_simplices"] == 108 + assert summary["total_new_simplices"] == 116 assert summary["negative_new_simplices"] == 0 assert summary["degenerate_new_simplices"] == 0 @@ -40,7 +40,7 @@ def test_tetra_parent_prism_volume_cases_have_only_endpoint_roots_and_positive_t for summary in (negative_summary, positive_summary): assert summary["cases"] == 14 - assert summary["source_type_counts"] == {"prism": 48} + assert summary["source_type_counts"] == {"prism": 52} assert summary["new_edges_with_interior_roots"] == 0 assert summary["new_edges_with_zero_tag"] == 6 assert summary["new_edges_with_zero_endpoint"] == 58 From d9806b9a689b1f267dd9443090b81e992f813450 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Fri, 24 Apr 2026 21:19:10 +0200 Subject: [PATCH 13/23] multiple levelsets 2d working --- cpp/src/cell_certification.cpp | 228 +++++++++++++++++++++++++++++---- cpp/src/cell_certification.h | 10 +- 2 files changed, 208 insertions(+), 30 deletions(-) diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index 65abf81..4f619a6 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include namespace cutcells @@ -273,6 +275,34 @@ int append_vertex_with_parent_info(AdaptCell& adapt_cell, return new_v; } +template +void inherit_common_edge_sign_masks(AdaptCell& adapt_cell, + int vertex_id, + int edge_id) +{ + if (edge_id < 0) + return; + + auto edge_vertices = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + if (edge_vertices.size() != 2) + return; + + const auto v0 = static_cast(edge_vertices[0]); + const auto v1 = static_cast(edge_vertices[1]); + const std::uint64_t common_zero = + adapt_cell.zero_mask_per_vertex[v0] & adapt_cell.zero_mask_per_vertex[v1]; + const std::uint64_t common_negative = + (adapt_cell.negative_mask_per_vertex[v0] + & adapt_cell.negative_mask_per_vertex[v1]) + & ~common_zero; + + auto& zero_mask = adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)]; + auto& negative_mask = adapt_cell.negative_mask_per_vertex[static_cast(vertex_id)]; + zero_mask |= common_zero; + negative_mask &= ~common_zero; + negative_mask |= common_negative; +} + template void gather_adapt_edge_bernstein(const AdaptCell& adapt_cell, const LevelSetCell& ls_cell, @@ -430,6 +460,25 @@ int ensure_one_root_vertex_on_edge(AdaptCell& adapt_cell, auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; const int v0 = static_cast(verts[0]); const int v1 = static_cast(verts[1]); + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + const T endpoint_tol = std::max(zero_tol, T(64) * std::numeric_limits::epsilon()); + if (std::fabs(root_t) <= endpoint_tol + && (adapt_cell.zero_mask_per_vertex[static_cast(v0)] & bit) != 0) + { + adapt_cell.edge_one_root_param[idx] = T(0); + adapt_cell.edge_one_root_vertex_id[idx] = v0; + adapt_cell.edge_one_root_has_value[idx] = 1; + return v0; + } + if (std::fabs(root_t - T(1)) <= endpoint_tol + && (adapt_cell.zero_mask_per_vertex[static_cast(v1)] & bit) != 0) + { + adapt_cell.edge_one_root_param[idx] = T(1); + adapt_cell.edge_one_root_vertex_id[idx] = v1; + adapt_cell.edge_one_root_has_value[idx] = 1; + return v1; + } + const int tdim = adapt_cell.tdim; std::vector coords(static_cast(tdim), T(0)); for (int d = 0; d < tdim; ++d) @@ -464,6 +513,7 @@ int ensure_one_root_vertex_on_edge(AdaptCell& adapt_cell, std::span(parent_param), edge_id); + inherit_common_edge_sign_masks(adapt_cell, vertex_id, edge_id); set_vertex_sign_for_level_set(adapt_cell, vertex_id, level_set_id, T(0), zero_tol); adapt_cell.edge_one_root_param[idx] = root_t; adapt_cell.edge_one_root_vertex_id[idx] = vertex_id; @@ -514,6 +564,7 @@ void append_ready_cut_part_cells( int parent_dim = parent_tdim; int parent_id = -1; + int old_edge_id_for_token = -1; if (token >= 0 && token < 100) { const int local_edge_id = token; @@ -521,6 +572,7 @@ void append_ready_cut_part_cells( && local_edge_id < static_cast(old_edge_ids_by_local_edge.size())) { const int old_edge_id = old_edge_ids_by_local_edge[static_cast(local_edge_id)]; + old_edge_id_for_token = old_edge_id; int parent_edge_id = -1; if (old_edge_id >= 0 && edge_is_on_single_parent_edge(adapt_cell, old_edge_id, parent_edge_id)) @@ -538,16 +590,23 @@ void append_ready_cut_part_cells( if (token >= 0 && token < 100) { const int local_edge_id = token; + if (old_edge_id_for_token >= 0) + inherit_common_edge_sign_masks(adapt_cell, vertex_id, old_edge_id_for_token); + + const EdgeRootTag edge_tag = + old_edge_id_for_token >= 0 + ? adapt_cell.get_edge_root_tag(level_set_id, old_edge_id_for_token) + : EdgeRootTag::not_classified; const bool is_level_set_root = local_edge_id >= 0 && local_edge_id < static_cast(old_edge_ids_by_local_edge.size()) - && old_edge_ids_by_local_edge[static_cast(local_edge_id)] >= 0 - && adapt_cell.get_edge_root_tag( - level_set_id, - old_edge_ids_by_local_edge[static_cast(local_edge_id)]) - == EdgeRootTag::one_root; + && edge_tag == EdgeRootTag::one_root; + const bool is_level_set_zero_edge = + local_edge_id >= 0 + && local_edge_id < static_cast(old_edge_ids_by_local_edge.size()) + && edge_tag == EdgeRootTag::zero; - if (is_level_set_root) + if (is_level_set_root || is_level_set_zero_edge) { // Straight cut vertices lie on the straight interface by // construction. They should participate in phi=0 @@ -617,49 +676,79 @@ CellCertTag classify_ready_to_cut_topology(const AdaptCell& adapt_cell, const int tdim = adapt_cell.tdim; auto cell_verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; const cell::type subcell_type = adapt_cell.entity_types[tdim][static_cast(cell_id)]; - const int n_edges = adapt_cell.n_entities(1); - int num_one_root_edges = 0; + std::set cut_point_tokens; bool has_multiple_roots = false; bool has_zero_edge = false; + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + std::map local_vertex_by_global; + for (std::size_t lv = 0; lv < cell_verts.size(); ++lv) + { + const int gv = static_cast(cell_verts[lv]); + local_vertex_by_global[gv] = static_cast(lv); + if ((adapt_cell.zero_mask_per_vertex[static_cast(gv)] & bit) != 0) + cut_point_tokens.insert(100 + static_cast(lv)); + } + + const int n_edges = adapt_cell.n_entities(1); for (int e = 0; e < n_edges; ++e) { auto edge_verts = adapt_cell.entity_to_vertex[1][static_cast(e)]; - bool v0_in = false; - bool v1_in = false; - for (auto cv : cell_verts) - { - if (cv == edge_verts[0]) v0_in = true; - if (cv == edge_verts[1]) v1_in = true; - } - if (!v0_in || !v1_in) + auto it0 = local_vertex_by_global.find(static_cast(edge_verts[0])); + auto it1 = local_vertex_by_global.find(static_cast(edge_verts[1])); + if (it0 == local_vertex_by_global.end() || it1 == local_vertex_by_global.end()) continue; const EdgeRootTag etag = adapt_cell.get_edge_root_tag(level_set_id, e); - if (etag == EdgeRootTag::one_root) - ++num_one_root_edges; - else if (etag == EdgeRootTag::multiple_roots) + if (etag == EdgeRootTag::multiple_roots) + { has_multiple_roots = true; + } else if (etag == EdgeRootTag::zero) + { has_zero_edge = true; + } + else if (etag == EdgeRootTag::one_root) + { + const bool v0_zero = + (adapt_cell.zero_mask_per_vertex[static_cast(edge_verts[0])] & bit) != 0; + const bool v1_zero = + (adapt_cell.zero_mask_per_vertex[static_cast(edge_verts[1])] & bit) != 0; + + if (v0_zero && !v1_zero) + cut_point_tokens.insert(100 + it0->second); + else if (v1_zero && !v0_zero) + cut_point_tokens.insert(100 + it1->second); + else if (v0_zero && v1_zero) + has_zero_edge = true; + else + { + const int local_edge_id = basix_edge_id_for_vertices( + subcell_type, it0->second, it1->second); + cut_point_tokens.insert(local_edge_id); + } + } } - if (has_multiple_roots || has_zero_edge) + if (has_multiple_roots) return CellCertTag::cut; - if (subcell_type == cell::type::triangle && num_one_root_edges == 2) + if (subcell_type == cell::type::triangle && cut_point_tokens.size() == 2) return CellCertTag::ready_to_cut; if (subcell_type == cell::type::tetrahedron - && (num_one_root_edges == 3 || num_one_root_edges == 4)) + && (cut_point_tokens.size() == 3 || cut_point_tokens.size() == 4)) { return CellCertTag::ready_to_cut; } - if (num_one_root_edges > 0) + if (!cut_point_tokens.empty()) return CellCertTag::cut; + if (has_zero_edge) + return CellCertTag::not_classified; + return CellCertTag::not_classified; } @@ -1309,20 +1398,35 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, const std::vector ls_values = gather_leaf_cell_vertex_level_set_values(adapt_cell, ls_cell, c); + const std::uint64_t current_level_set_bit = std::uint64_t(1) << level_set_id; + std::vector current_level_set_zero(old_cell_vertices.size(), false); bool has_strict_negative = false; bool has_strict_positive = false; - for (const T value : ls_values) + bool has_zero = false; + for (std::size_t j = 0; j < ls_values.size(); ++j) { + const T value = ls_values[j]; + const int gv = old_cell_vertices[j]; + current_level_set_zero[j] = + (adapt_cell.zero_mask_per_vertex[static_cast(gv)] + & current_level_set_bit) != 0; has_strict_negative = has_strict_negative || value < -zero_tol; has_strict_positive = has_strict_positive || value > zero_tol; + has_zero = has_zero || current_level_set_zero[j]; } if (!(has_strict_negative && has_strict_positive)) { std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); old_cell_ids_for_new_cells.push_back(c); - explicit_current_ls_tags.push_back( - has_strict_negative ? CellCertTag::negative : CellCertTag::positive); + CellCertTag copied_tag = CellCertTag::zero; + if (has_strict_negative) + copied_tag = CellCertTag::negative; + else if (has_strict_positive) + copied_tag = CellCertTag::positive; + else if (!has_zero) + copied_tag = CellCertTag::not_classified; + explicit_current_ls_tags.push_back(copied_tag); continue; } @@ -1371,6 +1475,78 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, if (leaf_cell_type == cell::type::triangle) { + if (has_zero) + { + auto append_zero_vertex_triangle_side = + [&](bool negative_side, CellCertTag side_tag) + { + std::vector tokens; + auto append_token = [&](int token) + { + if (tokens.empty() || tokens.back() != token) + tokens.push_back(token); + }; + + for (int lv = 0; lv < 3; ++lv) + { + const int next = (lv + 1) % 3; + const T vi = ls_values[static_cast(lv)]; + const T vj = ls_values[static_cast(next)]; + const bool zero_i = + current_level_set_zero[static_cast(lv)]; + const bool inside_i = negative_side + ? (zero_i || vi < -zero_tol) + : (zero_i || vi > zero_tol); + if (inside_i) + append_token(100 + lv); + + const bool strict_cross = + (vi < -zero_tol && vj > zero_tol) + || (vi > zero_tol && vj < -zero_tol); + if (strict_cross) + { + const int local_edge = + basix_edge_id_for_vertices(leaf_cell_type, lv, next); + append_token(local_edge); + } + } + + if (tokens.size() > 1 && tokens.front() == tokens.back()) + tokens.pop_back(); + if (tokens.size() < 3) + return; + if (tokens.size() != 3) + { + throw std::runtime_error( + "process_ready_to_cut_cells: zero-vertex triangle clipping " + "expected a triangle"); + } + + std::vector mapped(tokens.size(), -1); + for (std::size_t j = 0; j < tokens.size(); ++j) + { + auto token_it = token_to_vertex.find(tokens[j]); + if (token_it == token_to_vertex.end()) + { + throw std::runtime_error( + "process_ready_to_cut_cells: missing zero-vertex " + "triangle token " + std::to_string(tokens[j])); + } + mapped[j] = token_it->second; + } + + append_top_cell_local( + new_types, new_cells, cell::type::triangle, + std::span(mapped)); + old_cell_ids_for_new_cells.push_back(-1); + explicit_current_ls_tags.push_back(side_tag); + }; + + append_zero_vertex_triangle_side(true, CellCertTag::negative); + append_zero_vertex_triangle_side(false, CellCertTag::positive); + continue; + } + cell::CutCell negative_part; cell::CutCell positive_part; cell::triangle::cut( diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index 7b46a28..a2423d5 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -50,11 +50,13 @@ void restrict_subcell_bernstein_exact(cell::type parent_cell_type, /// /// Logic: /// A. If the incident edge pattern is a directly cuttable simplex case: -/// - triangle with exactly 2 one_root edges and no multiple_roots/zero -/// - tetrahedron with exactly 3 or 4 one_root edges and no multiple_roots/zero +/// - triangle with exactly 2 one_root edges and no multiple_roots +/// - tetrahedron with exactly 3 or 4 one_root edges and no multiple_roots /// → ready_to_cut. -/// B. Else if any incident edge has tag one_root, multiple_roots, or zero → cut. -/// B. Otherwise, restrict the parent Bernstein to the subcell and check +/// B. Else if any incident edge has tag one_root or multiple_roots → cut. +/// Zero edges alone fall through so later level sets can still classify +/// or refine cells whose interface is inherited from an earlier cut. +/// C. Otherwise, restrict the parent Bernstein to the subcell and check /// the sign hull: /// - all positive → positive /// - all negative → negative From 058164487472d9c5818fc4c6ab7468e78368cc7a Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Mon, 27 Apr 2026 17:19:16 +0200 Subject: [PATCH 14/23] work on non-triangulated mapping of 3d and 2d geometries --- cpp/src/CMakeLists.txt | 2 + cpp/src/adapt_cell.cpp | 135 +- cpp/src/curving.cpp | 3217 +++++++++++++++++++++++++++++++ cpp/src/curving.h | 170 ++ cpp/src/ho_cut_mesh.cpp | 41 +- cpp/src/ho_cut_mesh.h | 11 +- cpp/src/ho_mesh_part_output.cpp | 2178 ++++++++++++++++++++- cpp/src/ho_mesh_part_output.h | 33 +- cpp/src/level_set_cell.cpp | 19 + cpp/src/level_set_cell.h | 4 +- cpp/src/write_vtk.cpp | 141 +- cpp/src/write_vtk.h | 42 + python/cutcells/__init__.py | 18 +- python/cutcells/wrapper.cpp | 304 ++- 14 files changed, 6194 insertions(+), 121 deletions(-) create mode 100644 cpp/src/curving.cpp create mode 100644 cpp/src/curving.h diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 3b6f073..5ee1c3e 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -35,6 +35,7 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.h ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.h + ${CMAKE_CURRENT_SOURCE_DIR}/curving.h ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.h ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.h ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.h @@ -66,6 +67,7 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/curving.cpp ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.cpp diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp index a6705e9..c637d9f 100644 --- a/cpp/src/adapt_cell.cpp +++ b/cpp/src/adapt_cell.cpp @@ -15,6 +15,136 @@ namespace cutcells { +namespace +{ + +template +T squared_distance_to_segment(std::span p, + std::span a, + std::span b, + int dim) +{ + T ab2 = T(0); + T ap_ab = T(0); + for (int d = 0; d < dim; ++d) + { + const T ab = b[static_cast(d)] - a[static_cast(d)]; + const T ap = p[static_cast(d)] - a[static_cast(d)]; + ab2 += ab * ab; + ap_ab += ap * ab; + } + const T t = (ab2 > T(0)) ? std::clamp(ap_ab / ab2, T(0), T(1)) : T(0); + T dist2 = T(0); + for (int d = 0; d < dim; ++d) + { + const T x = a[static_cast(d)] + + t * (b[static_cast(d)] - a[static_cast(d)]); + const T r = p[static_cast(d)] - x; + dist2 += r * r; + } + return dist2; +} + +template +bool point_in_face_span(std::span p, + const std::vector& ref_vertices, + int tdim, + std::span face_vertices, + T tol) +{ + if (tdim != 3 || face_vertices.size() < 3) + return false; + + const T* a = ref_vertices.data() + static_cast(face_vertices[0] * tdim); + const T* b = ref_vertices.data() + static_cast(face_vertices[1] * tdim); + const T* c = ref_vertices.data() + static_cast(face_vertices[2] * tdim); + std::array u = {}; + std::array v = {}; + std::array w = {}; + for (int d = 0; d < 3; ++d) + { + u[static_cast(d)] = b[d] - a[d]; + v[static_cast(d)] = c[d] - a[d]; + w[static_cast(d)] = p[static_cast(d)] - a[d]; + } + const std::array n = { + u[1] * v[2] - u[2] * v[1], + u[2] * v[0] - u[0] * v[2], + u[0] * v[1] - u[1] * v[0]}; + const T n2 = n[0] * n[0] + n[1] * n[1] + n[2] * n[2]; + if (n2 <= tol * tol) + return false; + const T signed_dist = + (n[0] * w[0] + n[1] * w[1] + n[2] * w[2]) / std::sqrt(n2); + return std::fabs(signed_dist) <= tol; +} + +template +std::pair +infer_zero_entity_parent_host(const AdaptCell& ac, + std::span entity_vertices) +{ + const T tol = T(256) * std::numeric_limits::epsilon(); + const int tdim = ac.tdim; + const auto ref_vertices = cell::reference_vertices(ac.parent_cell_type); + + if (tdim >= 1) + { + auto parent_edges = cell::edges(ac.parent_cell_type); + for (int e = 0; e < static_cast(parent_edges.size()); ++e) + { + const auto edge = parent_edges[static_cast(e)]; + std::span a( + ref_vertices.data() + static_cast(edge[0] * tdim), + static_cast(tdim)); + std::span b( + ref_vertices.data() + static_cast(edge[1] * tdim), + static_cast(tdim)); + + bool all_on_edge = true; + for (const auto v : entity_vertices) + { + std::span p( + ac.vertex_coords.data() + static_cast(v * tdim), + static_cast(tdim)); + if (squared_distance_to_segment(p, a, b, tdim) > tol * tol) + { + all_on_edge = false; + break; + } + } + if (all_on_edge) + return {std::int8_t(1), std::int32_t(e)}; + } + } + + if (tdim == 3) + { + const int nfaces = cell::num_faces(ac.parent_cell_type); + for (int f = 0; f < nfaces; ++f) + { + auto face_vertices = cell::face_vertices(ac.parent_cell_type, f); + bool all_on_face = true; + for (const auto v : entity_vertices) + { + std::span p( + ac.vertex_coords.data() + static_cast(v * tdim), + static_cast(tdim)); + if (!point_in_face_span(p, ref_vertices, tdim, face_vertices, tol)) + { + all_on_face = false; + break; + } + } + if (all_on_face) + return {std::int8_t(2), std::int32_t(f)}; + } + } + + return {static_cast(tdim), static_cast(ac.parent_cell_id)}; +} + +} // namespace template void build_faces(AdaptCell& ac) @@ -316,8 +446,9 @@ void rebuild_zero_entity_inventory(AdaptCell& ac) ac.zero_entity_id.push_back(static_cast(e)); ac.zero_entity_zero_mask.push_back(mask); ac.zero_entity_is_owned.push_back(std::uint8_t(1)); - ac.zero_entity_parent_dim.push_back(std::int8_t(-1)); - ac.zero_entity_parent_id.push_back(std::int32_t(-1)); + const auto host = infer_zero_entity_parent_host(ac, verts); + ac.zero_entity_parent_dim.push_back(host.first); + ac.zero_entity_parent_id.push_back(host.second); } } diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp new file mode 100644 index 0000000..cde749b --- /dev/null +++ b/cpp/src/curving.cpp @@ -0,0 +1,3217 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#include "curving.h" + +#include "cell_topology.h" +#include "edge_root.h" +#include "mapping.h" +#include "reference_cell.h" + +#include +#include +#include +#include +#include +#include + +namespace cutcells::curving +{ +namespace +{ + +template +bool solve_dense_small(std::vector A, std::vector b, int n, std::vector& x); + +template +struct BoundaryEdgeState +{ + std::array vertices = {-1, -1}; + const CurvedZeroEntityState* state = nullptr; + bool use_curved_state = true; +}; + +struct BoundaryEdgeRef +{ + int local_zero_entity_id = -1; + bool use_curved_state = true; +}; + +template +struct ProjectionStats +{ + int iterations = 0; + CurvingStatus status = CurvingStatus::failed; + CurvingFailureCode failure_code = CurvingFailureCode::projection_failed; + T residual = std::numeric_limits::infinity(); + std::uint32_t active_face_mask = 0; + int closest_face_id = -1; + int safe_subspace_dim = -1; + CurvingProjectionMode projection_mode = CurvingProjectionMode::none; + int retry_count = 0; +}; + +template +void append_node_stats(CurvedZeroEntityState& state, + const ProjectionStats& stats) +{ + state.node_iterations.push_back(static_cast(stats.iterations)); + state.node_status.push_back(static_cast(stats.status)); + state.node_failure_code.push_back(static_cast(stats.failure_code)); + state.node_residual.push_back(stats.residual); + state.node_active_face_mask.push_back(stats.active_face_mask); + state.node_closest_face_id.push_back(static_cast(stats.closest_face_id)); + state.node_safe_subspace_dim.push_back(static_cast(stats.safe_subspace_dim)); + state.node_projection_mode.push_back(static_cast(stats.projection_mode)); + state.node_retry_count.push_back(static_cast(stats.retry_count)); +} + +template +ProjectionStats accepted_node_stats(CurvingFailureCode code, T residual = T(0)) +{ + ProjectionStats stats; + stats.iterations = 0; + stats.status = CurvingStatus::curved; + stats.failure_code = code; + stats.residual = residual; + return stats; +} + +inline bool same_unordered_vertices(std::span a, std::span b) +{ + if (a.size() != b.size()) + return false; + for (const int av : a) + { + bool found = false; + for (const int bv : b) + found = found || (av == bv); + if (!found) + return false; + } + return true; +} + +template +std::vector zero_entity_vertices(const AdaptCell& ac, int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + std::vector out; + + if (zdim == 0) + { + out.resize(static_cast(ac.tdim)); + for (int d = 0; d < ac.tdim; ++d) + out[static_cast(d)] = + ac.vertex_coords[static_cast(zid * ac.tdim + d)]; + return out; + } + + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + out.resize(static_cast(verts.size() * ac.tdim)); + for (std::size_t i = 0; i < verts.size(); ++i) + { + const int v = static_cast(verts[i]); + for (int d = 0; d < ac.tdim; ++d) + out[i * static_cast(ac.tdim) + static_cast(d)] = + ac.vertex_coords[static_cast(v * ac.tdim + d)]; + } + return out; +} + +template +std::vector zero_entity_vertex_ids(const AdaptCell& ac, int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim == 0) + return {zid}; + + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + std::vector out; + out.reserve(verts.size()); + for (const auto v : verts) + out.push_back(static_cast(v)); + return out; +} + +template +std::pair legendre_value_and_derivative(int order, T x) +{ + if (order == 0) + return {T(1), T(0)}; + + T pm2 = T(1); + T pm1 = x; + for (int n = 2; n <= order; ++n) + { + const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); + pm2 = pm1; + pm1 = p; + } + + const T denom = T(1) - x * x; + if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) + return {pm1, T(0)}; + const T derivative = T(order) * (pm2 - x * pm1) / denom; + return {pm1, derivative}; +} + +template +std::vector gll_parameters(int order) +{ + order = std::max(order, 1); + std::vector params(static_cast(order + 1), T(0)); + params.front() = T(0); + params.back() = T(1); + if (order == 1) + return params; + + const T pi = std::acos(T(-1)); + const T eps = T(128) * std::numeric_limits::epsilon(); + for (int i = 1; i < order; ++i) + { + T x = -std::cos(pi * T(i) / T(order)); + for (int iter = 0; iter < 32; ++iter) + { + const auto [p, dp] = legendre_value_and_derivative(order, x); + const T denom = T(1) - x * x; + if (std::abs(denom) <= eps) + break; + const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; + if (std::abs(d2p) <= eps) + break; + const T step = dp / d2p; + x -= step; + x = std::clamp(x, -T(1) + eps, T(1) - eps); + if (std::abs(step) <= eps) + break; + } + params[static_cast(i)] = T(0.5) * (x + T(1)); + } + return params; +} + +template +std::vector interpolation_parameters(int order, NodeFamily family) +{ + order = std::max(order, 1); + std::vector params(static_cast(order + 1), T(0)); + + if (family == NodeFamily::gll) + return gll_parameters(order); + + // The current reference Lagrange point generator is equispaced. Keep + // lagrange distinct at the API/cache level so a later Basix-backed node + // family can be added without changing cache semantics. + for (int i = 0; i <= order; ++i) + params[static_cast(i)] = T(i) / T(order); + return params; +} + +template +std::vector> triangle_interpolation_barycentric_nodes( + int order, + NodeFamily family); + +template +void append_edge_seed_nodes(const AdaptCell& ac, + int local_zero_entity_id, + const CurvingOptions& options, + std::vector& seeds) +{ + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[1][static_cast(zid)]; + if (verts.size() != 2) + throw std::runtime_error("curving: zero edge does not have two vertices"); + + const auto params = interpolation_parameters(options.geometry_order, options.node_family); + seeds.clear(); + seeds.reserve(params.size() * static_cast(ac.tdim)); + for (const T s : params) + { + for (int d = 0; d < ac.tdim; ++d) + { + const T x0 = ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; + const T x1 = ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)]; + seeds.push_back((T(1) - s) * x0 + s * x1); + } + } +} + +template +void append_face_seed_nodes(const AdaptCell& ac, + int local_zero_entity_id, + const CurvingOptions& options, + std::vector& seeds) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2) + throw std::runtime_error("curving: expected a zero face"); + + auto verts = ac.entity_to_vertex[2][static_cast(zid)]; + const auto entity_type = ac.entity_types[2][static_cast(zid)]; + const int order = std::max(options.geometry_order, 1); + seeds.clear(); + + auto append_combo = [&](std::span weights) + { + for (int d = 0; d < ac.tdim; ++d) + { + T x = T(0); + for (std::size_t v = 0; v < weights.size(); ++v) + { + x += weights[v] * ac.vertex_coords[ + static_cast(verts[v] * ac.tdim + d)]; + } + seeds.push_back(x); + } + }; + + if (entity_type == cell::type::triangle) + { + const auto nodes = + triangle_interpolation_barycentric_nodes(order, options.node_family); + for (const auto& w : nodes) + append_combo(std::span(w.data(), 3)); + return; + } + + if (entity_type == cell::type::quadrilateral) + { + const auto params = interpolation_parameters(order, options.node_family); + std::array w = {}; + for (const T v : params) + { + for (const T u : params) + { + w = {(T(1) - u) * (T(1) - v), + u * (T(1) - v), + (T(1) - u) * v, + u * v}; + append_combo(std::span(w.data(), 4)); + } + } + return; + } + + throw std::runtime_error("curving: unsupported zero-face type"); +} + +template +T lagrange_basis_1d(int i, std::span params, T x) +{ + T value = T(1); + const T xi = params[static_cast(i)]; + for (int j = 0; j < static_cast(params.size()); ++j) + { + if (j == i) + continue; + value *= (x - params[static_cast(j)]) + / (xi - params[static_cast(j)]); + } + return value; +} + +template +T warp_factor(int order, T r) +{ + if (order <= 1) + return T(0); + + std::vector equispaced(static_cast(order + 1), T(0)); + std::vector gll = gll_parameters(order); + for (int i = 0; i <= order; ++i) + { + equispaced[static_cast(i)] = -T(1) + T(2 * i) / T(order); + gll[static_cast(i)] = T(2) * gll[static_cast(i)] - T(1); + } + + T warp = T(0); + for (int i = 0; i <= order; ++i) + { + const T Li = lagrange_basis_1d( + i, std::span(equispaced.data(), equispaced.size()), r); + warp += Li * (gll[static_cast(i)] + - equispaced[static_cast(i)]); + } + + const T edge_factor = T(1) - r * r; + if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) + warp /= edge_factor; + return warp; +} + +template +std::array equilateral_to_reference_barycentric(T x, T y) +{ + const T sqrt3 = std::sqrt(T(3)); + std::array w = {}; + w[2] = (sqrt3 * y + T(1)) / T(3); + w[1] = (x + T(1) - w[2]) / T(2); + w[0] = T(1) - w[1] - w[2]; + + T sum = T(0); + for (T& wi : w) + { + if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) + wi = T(0); + if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) + wi = T(1); + wi = std::clamp(wi, T(0), T(1)); + sum += wi; + } + if (sum > T(0)) + for (T& wi : w) + wi /= sum; + return w; +} + +template +std::vector> triangle_interpolation_barycentric_nodes( + int order, + NodeFamily family) +{ + order = std::max(order, 1); + std::vector> nodes; + nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); + + if (family != NodeFamily::gll) + { + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + nodes.push_back({T(1) - u - v, u, v}); + } + } + return nodes; + } + + constexpr std::array alpha_opt = { + T(0.0), T(0.0), T(1.4152), T(0.1001), + T(0.2751), T(0.9800), T(1.0999), T(1.2832), + T(1.3648), T(1.4773), T(1.4959), T(1.5743), + T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; + const T alpha = (order < static_cast(alpha_opt.size())) + ? alpha_opt[static_cast(order)] + : T(5) / T(3); + const T sqrt3 = std::sqrt(T(3)); + const T cos120 = -T(0.5); + const T sin120 = sqrt3 / T(2); + const T cos240 = -T(0.5); + const T sin240 = -sqrt3 / T(2); + + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + const T lambda0 = T(1) - u - v; + const T lambda1 = u; + const T lambda2 = v; + + T x = -lambda0 + lambda1; + T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; + + const T L1 = lambda2; + const T L2 = lambda0; + const T L3 = lambda1; + const T warp1 = T(4) * L2 * L3 * warp_factor(order, L3 - L2) + * (T(1) + (alpha * L1) * (alpha * L1)); + const T warp2 = T(4) * L1 * L3 * warp_factor(order, L1 - L3) + * (T(1) + (alpha * L2) * (alpha * L2)); + const T warp3 = T(4) * L1 * L2 * warp_factor(order, L2 - L1) + * (T(1) + (alpha * L3) * (alpha * L3)); + + x += warp1 + cos120 * warp2 + cos240 * warp3; + y += sin120 * warp2 + sin240 * warp3; + nodes.push_back(equilateral_to_reference_barycentric(x, y)); + } + } + return nodes; +} + +template +const BoundaryEdgeState* find_boundary_edge_state( + std::span> boundary_edges, + int v0, + int v1) +{ + std::array query = {v0, v1}; + for (const auto& edge : boundary_edges) + { + if (same_unordered_vertices( + std::span(query.data(), query.size()), + std::span(edge.vertices.data(), edge.vertices.size()))) + { + return &edge; + } + } + return nullptr; +} + +template +std::vector eval_edge_state_at(const BoundaryEdgeState& edge, + const CurvingOptions& options, + int from_vertex, + int to_vertex, + T t_from_to) +{ + if (edge.state == nullptr || edge.state->status != CurvingStatus::curved) + throw std::runtime_error("curving: missing accepted curved boundary edge"); + + const int order = std::max(options.geometry_order, 1); + const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); + const bool same_orientation = + edge.vertices[0] == from_vertex && edge.vertices[1] == to_vertex; + const bool reverse_orientation = + edge.vertices[0] == to_vertex && edge.vertices[1] == from_vertex; + if (!same_orientation && !reverse_orientation) + throw std::runtime_error("curving: boundary edge orientation mismatch"); + + const T t = same_orientation ? t_from_to : T(1) - t_from_to; + const auto params = interpolation_parameters(order, options.node_family); + std::vector out(static_cast(tdim), T(0)); + for (int i = 0; i <= order; ++i) + { + const T Li = lagrange_basis_1d( + i, std::span(params.data(), params.size()), t); + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] += Li * edge.state->ref_nodes[ + static_cast(i * tdim + d)]; + } + return out; +} + +template +void append_domain_inequality(std::vector>& ineq, + std::array a, + T b) +{ + ineq.push_back({a[0], a[1], a[2], b}); +} + +template +std::vector> reference_domain_inequalities(cell::type cell_type) +{ + std::vector> ineq; + switch (cell_type) + { + case cell::type::interval: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, { T(1), T(0), T(0)}, T(1)); + break; + case cell::type::triangle: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); + append_domain_inequality(ineq, {T(1), T(1), T(0)}, T(1)); + break; + case cell::type::tetrahedron: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); + append_domain_inequality(ineq, {T(1), T(1), T(1)}, T(1)); + break; + case cell::type::quadrilateral: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, { T(1), T(0), T(0)}, T(1)); + append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), T(1), T(0)}, T(1)); + break; + case cell::type::hexahedron: + for (int d = 0; d < 3; ++d) + { + std::array lo = {T(0), T(0), T(0)}; + std::array hi = {T(0), T(0), T(0)}; + lo[static_cast(d)] = -T(1); + hi[static_cast(d)] = T(1); + append_domain_inequality(ineq, lo, T(0)); + append_domain_inequality(ineq, hi, T(1)); + } + break; + case cell::type::prism: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); + append_domain_inequality(ineq, {T(1), T(1), T(0)}, T(1)); + append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); + append_domain_inequality(ineq, {T(0), T(0), T(1)}, T(1)); + break; + case cell::type::pyramid: + append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); + append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); + append_domain_inequality(ineq, {T(1), T(0), T(1)}, T(1)); + append_domain_inequality(ineq, {T(0), T(1), T(1)}, T(1)); + append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); + append_domain_inequality(ineq, {T(0), T(0), T(1)}, T(1)); + break; + default: + break; + } + return ineq; +} + +template +bool host_parameter_interval(const AdaptCell& ac, + std::span x0, + std::span d, + T tol, + T& lo, + T& hi) +{ + lo = -std::numeric_limits::infinity(); + hi = std::numeric_limits::infinity(); + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + for (const auto& row : ineq) + { + T ax = T(0); + T ad = T(0); + for (int k = 0; k < ac.tdim; ++k) + { + ax += row[static_cast(k)] * x0[static_cast(k)]; + ad += row[static_cast(k)] * d[static_cast(k)]; + } + const T rhs = row[3] + tol - ax; + if (std::fabs(ad) <= T(64) * std::numeric_limits::epsilon()) + { + if (rhs < T(0)) + return false; + continue; + } + const T bound = rhs / ad; + if (ad > T(0)) + hi = std::min(hi, bound); + else + lo = std::max(lo, bound); + } + return lo <= hi; +} + +template +T vec_dot(std::span a, std::span b) +{ + T out = T(0); + for (std::size_t i = 0; i < a.size(); ++i) + out += a[i] * b[i]; + return out; +} + +template +T vec_norm(std::span a) +{ + return std::sqrt(vec_dot(a, a)); +} + +template +T face_normal_norm(const std::array& row, int tdim) +{ + T n2 = T(0); + for (int d = 0; d < tdim; ++d) + n2 += row[static_cast(d)] * row[static_cast(d)]; + return std::sqrt(n2); +} + +template +T face_value(const std::array& row, std::span x, int tdim) +{ + T ax = T(0); + for (int d = 0; d < tdim; ++d) + ax += row[static_cast(d)] * x[static_cast(d)]; + return ax; +} + +template +T normalized_face_slack(const std::array& row, + std::span x, + int tdim) +{ + const T n = face_normal_norm(row, tdim); + if (n <= T(0)) + return std::numeric_limits::infinity(); + return (row[3] - face_value(row, x, tdim)) / n; +} + +template +bool vertex_satisfies_face(const std::array& row, + std::span ref_vertices, + int vertex, + int tdim) +{ + std::array x = {T(0), T(0), T(0)}; + for (int d = 0; d < tdim; ++d) + x[static_cast(d)] = + ref_vertices[static_cast(vertex * tdim + d)]; + const T n = face_normal_norm(row, tdim); + const T tol = T(64) * std::numeric_limits::epsilon() * std::max(T(1), n); + return std::fabs(face_value(row, std::span(x.data(), static_cast(tdim)), tdim) - row[3]) <= tol; +} + +template +int face_inequality_index_from_vertices(cell::type cell_type, + std::span vertices, + int tdim) +{ + const auto ineq = reference_domain_inequalities(cell_type); + const auto ref_vertices = cell::reference_vertices(cell_type); + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + bool all_on_face = true; + for (const int v : vertices) + { + all_on_face = all_on_face + && vertex_satisfies_face( + ineq[static_cast(i)], + std::span(ref_vertices.data(), ref_vertices.size()), + v, + tdim); + } + if (all_on_face) + return i; + } + return -1; +} + +template +int parent_face_inequality_index(cell::type cell_type, int parent_face_id, int tdim) +{ + if (parent_face_id < 0 || parent_face_id >= cell::num_faces(cell_type)) + return -1; + const auto fv = cell::face_vertices(cell_type, parent_face_id); + return face_inequality_index_from_vertices(cell_type, fv, tdim); +} + +template +std::uint32_t structural_active_face_mask(const AdaptCell& ac, + int local_zero_entity_id) +{ + if (ac.tdim != 3) + return 0; + + const int parent_dim = + ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; + const int parent_id = + ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; + + std::uint32_t mask = 0; + if (parent_dim == 2) + { + const int row = parent_face_inequality_index( + ac.parent_cell_type, parent_id, ac.tdim); + if (row >= 0) + mask |= std::uint32_t(1) << row; + return mask; + } + + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + const auto ref_vertices = cell::reference_vertices(ac.parent_cell_type); + if (parent_dim == 1) + { + const auto edges = cell::edges(ac.parent_cell_type); + if (parent_id < 0 || parent_id >= static_cast(edges.size())) + return mask; + const auto edge = edges[static_cast(parent_id)]; + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + if (vertex_satisfies_face( + ineq[static_cast(i)], + std::span(ref_vertices.data(), ref_vertices.size()), + edge[0], + ac.tdim) + && vertex_satisfies_face( + ineq[static_cast(i)], + std::span(ref_vertices.data(), ref_vertices.size()), + edge[1], + ac.tdim)) + { + mask |= std::uint32_t(1) << i; + } + } + } + else if (parent_dim == 0) + { + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + if (vertex_satisfies_face( + ineq[static_cast(i)], + std::span(ref_vertices.data(), ref_vertices.size()), + parent_id, + ac.tdim)) + { + mask |= std::uint32_t(1) << i; + } + } + } + return mask; +} + +template +std::uint32_t add_near_active_faces(const AdaptCell& ac, + std::span x, + std::uint32_t mask, + T tol) +{ + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + const T slack = normalized_face_slack( + ineq[static_cast(i)], x, ac.tdim); + if (slack <= tol) + mask |= std::uint32_t(1) << i; + } + return mask; +} + +template +int closest_inactive_face(const AdaptCell& ac, + std::span x, + std::uint32_t active_mask) +{ + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + int best = -1; + T best_slack = std::numeric_limits::infinity(); + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + if ((active_mask & (std::uint32_t(1) << i)) != 0) + continue; + const T slack = normalized_face_slack( + ineq[static_cast(i)], x, ac.tdim); + if (slack < best_slack) + { + best_slack = slack; + best = i; + } + } + return best; +} + +template +std::uint32_t add_closest_faces(const AdaptCell& ac, + std::span x, + std::uint32_t active_mask, + int& closest_face_id) +{ + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + closest_face_id = closest_inactive_face(ac, x, active_mask); + if (closest_face_id < 0) + return active_mask; + + const T best_slack = normalized_face_slack( + ineq[static_cast(closest_face_id)], x, ac.tdim); + const T tie_tol = std::max(T(128) * std::numeric_limits::epsilon(), + T(10) * ac.tdim * std::numeric_limits::epsilon()); + std::uint32_t out = active_mask; + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + if ((active_mask & (std::uint32_t(1) << i)) != 0) + continue; + const T slack = normalized_face_slack( + ineq[static_cast(i)], x, ac.tdim); + if (std::fabs(slack - best_slack) <= tie_tol * std::max(T(1), std::fabs(best_slack))) + out |= std::uint32_t(1) << i; + } + return out; +} + +template +std::vector> orthonormal_active_normals(const AdaptCell& ac, + std::uint32_t active_mask) +{ + const auto ineq = reference_domain_inequalities(ac.parent_cell_type); + std::vector> normals; + for (int i = 0; i < static_cast(ineq.size()); ++i) + { + if ((active_mask & (std::uint32_t(1) << i)) == 0) + continue; + std::vector q(static_cast(ac.tdim), T(0)); + for (int d = 0; d < ac.tdim; ++d) + q[static_cast(d)] = + ineq[static_cast(i)][static_cast(d)]; + for (const auto& prev : normals) + { + const T c = vec_dot( + std::span(q.data(), q.size()), + std::span(prev.data(), prev.size())); + for (int d = 0; d < ac.tdim; ++d) + q[static_cast(d)] -= c * prev[static_cast(d)]; + } + const T n = vec_norm(std::span(q.data(), q.size())); + if (n <= T(64) * std::numeric_limits::epsilon()) + continue; + for (T& x : q) + x /= n; + normals.push_back(q); + } + return normals; +} + +template +std::vector project_to_active_face_space(const AdaptCell& ac, + std::span direction, + std::uint32_t active_mask) +{ + std::vector out(direction.begin(), direction.end()); + const auto normals = orthonormal_active_normals(ac, active_mask); + for (const auto& n : normals) + { + const T c = vec_dot( + std::span(out.data(), out.size()), + std::span(n.data(), n.size())); + for (int d = 0; d < ac.tdim; ++d) + out[static_cast(d)] -= c * n[static_cast(d)]; + } + return out; +} + +template +std::vector> active_nullspace_basis(const AdaptCell& ac, + std::uint32_t active_mask) +{ + std::vector> basis; + const auto normals = orthonormal_active_normals(ac, active_mask); + for (int axis = 0; axis < ac.tdim; ++axis) + { + std::vector q(static_cast(ac.tdim), T(0)); + q[static_cast(axis)] = T(1); + for (const auto& n : normals) + { + const T c = vec_dot( + std::span(q.data(), q.size()), + std::span(n.data(), n.size())); + for (int d = 0; d < ac.tdim; ++d) + q[static_cast(d)] -= c * n[static_cast(d)]; + } + for (const auto& prev : basis) + { + const T c = vec_dot( + std::span(q.data(), q.size()), + std::span(prev.data(), prev.size())); + for (int d = 0; d < ac.tdim; ++d) + q[static_cast(d)] -= c * prev[static_cast(d)]; + } + const T n = vec_norm(std::span(q.data(), q.size())); + if (n <= T(64) * std::numeric_limits::epsilon()) + continue; + for (T& x : q) + x /= n; + basis.push_back(q); + } + return basis; +} + +template +int active_subspace_dim(const AdaptCell& ac, std::uint32_t active_mask) +{ + return static_cast(active_nullspace_basis(ac, active_mask).size()); +} + +template +void append_unique_direction(std::vector>& directions, + std::vector dir, + T tol) +{ + const T n = vec_norm(std::span(dir.data(), dir.size())); + if (n <= tol) + return; + for (T& x : dir) + x /= n; + + for (const auto& existing : directions) + { + const T c = std::fabs(vec_dot( + std::span(dir.data(), dir.size()), + std::span(existing.data(), existing.size()))); + if (c >= T(1) - T(64) * std::numeric_limits::epsilon()) + return; + } + directions.push_back(std::move(dir)); +} + +template +std::vector zero_entity_edge_tangent(const AdaptCell& ac, + int local_zero_entity_id) +{ + std::vector tangent(static_cast(ac.tdim), T(0)); + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 1) + return tangent; + + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[1][static_cast(zid)]; + if (verts.size() != 2) + return tangent; + for (int d = 0; d < ac.tdim; ++d) + tangent[static_cast(d)] = + ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)] + - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; + return tangent; +} + +template +std::vector zero_entity_face_normal(const AdaptCell& ac, + int local_zero_entity_id) +{ + std::vector normal(static_cast(ac.tdim), T(0)); + if (ac.tdim != 3) + return normal; + + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 2) + return normal; + + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[2][static_cast(zid)]; + if (verts.size() < 3) + return normal; + + std::array e0 = {T(0), T(0), T(0)}; + std::array e1 = {T(0), T(0), T(0)}; + for (int d = 0; d < 3; ++d) + { + e0[static_cast(d)] = + ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)] + - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; + e1[static_cast(d)] = + ac.vertex_coords[static_cast(verts[2] * ac.tdim + d)] + - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; + } + normal[0] = e0[1] * e1[2] - e0[2] * e1[1]; + normal[1] = e0[2] * e1[0] - e0[0] * e1[2]; + normal[2] = e0[0] * e1[1] - e0[1] * e1[0]; + return normal; +} + +template +std::vector remove_component(std::vector direction, + std::span tangent) +{ + const T tt = vec_dot(tangent, tangent); + if (tt <= T(0)) + return direction; + const T c = vec_dot( + std::span(direction.data(), direction.size()), tangent) / tt; + for (std::size_t i = 0; i < direction.size(); ++i) + direction[i] -= c * tangent[i]; + return direction; +} + +template +std::vector physical_normal_reference_direction( + const LevelSetCell& ls_cell, + std::span grad_ref) +{ + std::vector out(grad_ref.begin(), grad_ref.end()); + const int tdim = ls_cell.tdim; + const int gdim = ls_cell.gdim; + if (tdim <= 0 || tdim > 3 || gdim <= 0 + || static_cast(grad_ref.size()) != tdim + || ls_cell.parent_vertex_coords.empty()) + { + return out; + } + + const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); + std::vector gram(static_cast(tdim * tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + const int va = cols[static_cast(a)]; + if (va < 0) + return out; + for (int b = 0; b < tdim; ++b) + { + const int vb = cols[static_cast(b)]; + if (vb < 0) + return out; + T value = T(0); + for (int r = 0; r < gdim; ++r) + { + const T ja = ls_cell.parent_vertex_coords[ + static_cast(va * gdim + r)] + - ls_cell.parent_vertex_coords[ + static_cast(r)]; + const T jb = ls_cell.parent_vertex_coords[ + static_cast(vb * gdim + r)] + - ls_cell.parent_vertex_coords[ + static_cast(r)]; + value += ja * jb; + } + gram[static_cast(a * tdim + b)] = value; + } + } + + std::vector rhs(grad_ref.begin(), grad_ref.end()); + std::vector metric_direction; + if (solve_dense_small(std::move(gram), std::move(rhs), tdim, metric_direction)) + return metric_direction; + return out; +} + +template +bool affine_jacobian(const LevelSetCell& ls_cell, + std::vector& jacobian) +{ + const int tdim = ls_cell.tdim; + const int gdim = ls_cell.gdim; + if (tdim <= 0 || tdim > 3 || gdim <= 0 + || ls_cell.parent_vertex_coords.empty()) + { + return false; + } + + const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); + jacobian.assign(static_cast(gdim * tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + const int va = cols[static_cast(a)]; + if (va < 0) + return false; + for (int r = 0; r < gdim; ++r) + { + jacobian[static_cast(r * tdim + a)] = + ls_cell.parent_vertex_coords[ + static_cast(va * gdim + r)] + - ls_cell.parent_vertex_coords[static_cast(r)]; + } + } + return true; +} + +template +std::vector pull_back_physical_direction(std::span jacobian, + int gdim, + int tdim, + std::span direction_phys, + std::span fallback_ref) +{ + std::vector gram(static_cast(tdim * tdim), T(0)); + std::vector rhs(static_cast(tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + for (int r = 0; r < gdim; ++r) + { + rhs[static_cast(a)] += + jacobian[static_cast(r * tdim + a)] + * direction_phys[static_cast(r)]; + } + for (int b = 0; b < tdim; ++b) + { + T value = T(0); + for (int r = 0; r < gdim; ++r) + { + value += jacobian[static_cast(r * tdim + a)] + * jacobian[static_cast(r * tdim + b)]; + } + gram[static_cast(a * tdim + b)] = value; + } + } + + std::vector out; + if (solve_dense_small(std::move(gram), std::move(rhs), tdim, out)) + return out; + return std::vector(fallback_ref.begin(), fallback_ref.end()); +} + +template +std::vector solve_physical_gradient(std::span jacobian, + int gdim, + int tdim, + std::span grad_ref, + std::span fallback_ref) +{ + if (gdim == tdim) + { + std::vector jt(static_cast(tdim * tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + for (int r = 0; r < gdim; ++r) + { + jt[static_cast(a * tdim + r)] = + jacobian[static_cast(r * tdim + a)]; + } + } + std::vector rhs(grad_ref.begin(), grad_ref.end()); + std::vector grad_phys; + if (solve_dense_small(std::move(jt), std::move(rhs), tdim, grad_phys)) + return grad_phys; + } + + std::vector out(static_cast(gdim), T(0)); + for (int r = 0; r < gdim; ++r) + { + for (int a = 0; a < tdim; ++a) + { + out[static_cast(r)] += + jacobian[static_cast(r * tdim + a)] + * fallback_ref[static_cast(a)]; + } + } + return out; +} + +template +std::vector level_set_physical_point(const LevelSetCell& ls_cell, + std::span ref) +{ + return cell::push_forward_affine_map( + ls_cell.cell_type, + ls_cell.parent_vertex_coords, + ls_cell.gdim, + ref); +} + +template +T curving_level_set_value(const LevelSetCell& ls_cell, + std::span ref) +{ + if (ls_cell.global_level_set != nullptr + && ls_cell.global_level_set->has_value() + && !ls_cell.parent_vertex_coords.empty()) + { + const auto x = level_set_physical_point(ls_cell, ref); + return ls_cell.global_level_set->value(x.data(), ls_cell.cell_id); + } + return ls_cell.value(ref); +} + +template +void curving_level_set_grad(const LevelSetCell& ls_cell, + std::span ref, + std::span grad_ref) +{ + if (ls_cell.global_level_set != nullptr + && ls_cell.global_level_set->has_gradient() + && !ls_cell.parent_vertex_coords.empty()) + { + std::vector jacobian; + if (affine_jacobian(ls_cell, jacobian)) + { + const auto x = level_set_physical_point(ls_cell, ref); + std::vector grad_phys(static_cast(ls_cell.gdim), T(0)); + ls_cell.global_level_set->grad(x.data(), ls_cell.cell_id, grad_phys.data()); + for (int a = 0; a < ls_cell.tdim; ++a) + { + T value = T(0); + for (int r = 0; r < ls_cell.gdim; ++r) + value += jacobian[static_cast(r * ls_cell.tdim + a)] + * grad_phys[static_cast(r)]; + grad_ref[static_cast(a)] = value; + } + return; + } + } + ls_cell.grad(ref, grad_ref); +} + +template +std::vector physical_host_gradient_reference_direction( + const LevelSetCell& ls_cell, + const AdaptCell& ac, + int local_zero_entity_id, + std::span grad_ref, + std::span fallback_ref) +{ + std::vector jacobian; + if (!affine_jacobian(ls_cell, jacobian)) + return std::vector(fallback_ref.begin(), fallback_ref.end()); + + const int tdim = ls_cell.tdim; + const int gdim = ls_cell.gdim; + auto direction_phys = solve_physical_gradient( + std::span(jacobian.data(), jacobian.size()), + gdim, + tdim, + grad_ref, + fallback_ref); + + const int parent_dim = + ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; + const int parent_id = + ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; + if (parent_dim == 2 && gdim == 3) + { + auto face = cell::face_vertices(ac.parent_cell_type, parent_id); + if (face.size() >= 3) + { + std::array e0 = {T(0), T(0), T(0)}; + std::array e1 = {T(0), T(0), T(0)}; + for (int d = 0; d < 3; ++d) + { + e0[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(face[1] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(face[0] * gdim + d)]; + e1[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(face[2] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(face[0] * gdim + d)]; + } + std::array normal = { + e0[1] * e1[2] - e0[2] * e1[1], + e0[2] * e1[0] - e0[0] * e1[2], + e0[0] * e1[1] - e0[1] * e1[0]}; + const T nn = normal[0] * normal[0] + + normal[1] * normal[1] + + normal[2] * normal[2]; + if (nn > T(0)) + { + const T c = (direction_phys[0] * normal[0] + + direction_phys[1] * normal[1] + + direction_phys[2] * normal[2]) / nn; + for (int d = 0; d < 3; ++d) + direction_phys[static_cast(d)] -= c * normal[d]; + } + } + } + else if (parent_dim == 1) + { + auto edges = cell::edges(ac.parent_cell_type); + if (parent_id >= 0 && parent_id < static_cast(edges.size())) + { + const auto edge = edges[static_cast(parent_id)]; + std::vector tangent(static_cast(gdim), T(0)); + for (int d = 0; d < gdim; ++d) + { + tangent[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(edge[1] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(edge[0] * gdim + d)]; + } + const T tt = vec_dot( + std::span(tangent.data(), tangent.size()), + std::span(tangent.data(), tangent.size())); + if (tt > T(0)) + { + const T c = vec_dot( + std::span(direction_phys.data(), direction_phys.size()), + std::span(tangent.data(), tangent.size())) / tt; + for (int d = 0; d < gdim; ++d) + direction_phys[static_cast(d)] = + c * tangent[static_cast(d)]; + } + } + } + else if (parent_dim == 0) + { + return std::vector(static_cast(tdim), T(0)); + } + + return pull_back_physical_direction( + std::span(jacobian.data(), jacobian.size()), + gdim, + tdim, + std::span(direction_phys.data(), direction_phys.size()), + fallback_ref); +} + +template +std::vector> scalar_candidate_directions(const AdaptCell& ac, + int local_zero_entity_id, + std::span seed, + std::span grad, + std::span metric_grad, + std::span host_metric_grad, + std::uint32_t active_mask, + const CurvingOptions& options) +{ + std::vector> directions; + const T tol = std::max(options.ftol, T(64) * std::numeric_limits::epsilon()); + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const std::uint32_t host_mask = + structural_active_face_mask(ac, local_zero_entity_id); + + // First try the local closest-point direction for the implicit geometry, + // restricted only by the structural parent host entity. Extra near-face + // constraints can overrestrict seeds on warped cut quads and create large + // tangential motion, so those are kept for fallback directions below. + append_unique_direction( + directions, std::vector(host_metric_grad.begin(), host_metric_grad.end()), tol); + + if (active_mask != host_mask) + { + append_unique_direction( + directions, project_to_active_face_space( + ac, metric_grad, active_mask), tol); + } + + if (zdim == 1) + { + const auto tangent = zero_entity_edge_tangent(ac, local_zero_entity_id); + auto d = project_to_active_face_space(ac, metric_grad, active_mask); + d = remove_component( + std::move(d), std::span(tangent.data(), tangent.size())); + append_unique_direction(directions, std::move(d), tol); + } + else if (zdim == 2) + { + auto normal = zero_entity_face_normal(ac, local_zero_entity_id); + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(normal.data(), normal.size()), active_mask), + tol); + } + + append_unique_direction( + directions, project_to_active_face_space(ac, grad, active_mask), tol); + + const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); + const int nv = cell::get_num_vertices(ac.parent_cell_type); + for (int v = 0; v < nv; ++v) + { + std::vector ray(static_cast(ac.tdim), T(0)); + for (int d = 0; d < ac.tdim; ++d) + ray[static_cast(d)] = + seed[static_cast(d)] + - parent_vertices[static_cast(v * ac.tdim + d)]; + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(ray.data(), ray.size()), active_mask), + tol); + } + + const auto basis = active_nullspace_basis(ac, active_mask); + for (const auto& b : basis) + append_unique_direction(directions, b, tol); + + return directions; +} + +template +bool try_scalar_line_search(const AdaptCell& ac, + std::span seed, + std::span> directions, + const CurvingOptions& options, + Eval&& eval_on_point, + std::vector& out, + ProjectionStats& stats) +{ + const int tdim = ac.tdim; + for (const auto& unit : directions) + { + T lo = T(0), hi = T(0); + if (!host_parameter_interval( + ac, seed, std::span(unit.data(), unit.size()), + options.domain_tol, lo, hi)) + { + stats.failure_code = CurvingFailureCode::no_host_interval; + continue; + } + lo = std::max(lo, -T(2)); + hi = std::min(hi, T(2)); + if (!(lo <= T(0) && T(0) <= hi)) + continue; + + auto eval_line = [&](T t) -> T + { + std::array x = {T(0), T(0), T(0)}; + for (int d = 0; d < tdim; ++d) + x[static_cast(d)] = + seed[static_cast(d)] + + t * unit[static_cast(d)]; + return eval_on_point(std::span(x.data(), static_cast(tdim))); + }; + + const int samples = 48; + T best_a = T(0), best_b = T(0); + T best_dist = std::numeric_limits::infinity(); + bool have_bracket = false; + T t_prev = lo; + T f_prev = eval_line(t_prev); + for (int i = 1; i <= samples; ++i) + { + const T t_cur = lo + (hi - lo) * T(i) / T(samples); + const T f_cur = eval_line(t_cur); + if (std::fabs(f_cur) <= options.ftol) + { + out.assign(seed.begin(), seed.end()); + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] += t_cur * unit[static_cast(d)]; + stats.iterations = i; + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = std::fabs(f_cur); + return true; + } + if (f_prev * f_cur <= T(0)) + { + const T dist = std::min(std::fabs(t_prev), std::fabs(t_cur)); + if (dist < best_dist) + { + best_a = t_prev; + best_b = t_cur; + best_dist = dist; + have_bracket = true; + } + } + t_prev = t_cur; + f_prev = f_cur; + } + if (!have_bracket) + { + stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; + continue; + } + + const T fa = eval_line(best_a); + const T fb = eval_line(best_b); + int iterations = 0; + bool converged = false; + const T root_t = cell::edge_root::brent_solve( + eval_line, best_a, best_b, fa, fb, + options.max_iter, options.xtol, options.ftol, + &iterations, &converged); + stats.iterations = iterations; + const T residual = std::fabs(eval_line(root_t)); + stats.residual = residual; + if (!converged && residual > options.ftol) + { + stats.failure_code = CurvingFailureCode::brent_failed; + continue; + } + + out.assign(seed.begin(), seed.end()); + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] += root_t * unit[static_cast(d)]; + const bool inside = cell::edge_root::is_inside_reference_domain( + std::span(out.data(), out.size()), + ac.parent_cell_type, + options.domain_tol); + if (!inside) + { + stats.status = CurvingStatus::failed; + stats.failure_code = CurvingFailureCode::outside_host_domain; + return false; + } + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + return true; + } + + stats.status = CurvingStatus::failed; + return false; +} + +template +bool constrained_scalar_newton(const AdaptCell& ac, + std::span seed, + std::uint32_t active_mask, + const CurvingOptions& options, + Eval&& eval_on_point, + Grad&& grad_on_point, + std::vector& out, + ProjectionStats& stats) +{ + const int tdim = ac.tdim; + out.assign(seed.begin(), seed.end()); + stats.projection_mode = CurvingProjectionMode::constrained_newton; + stats.active_face_mask = active_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); + + if (stats.safe_subspace_dim <= 0) + { + stats.failure_code = CurvingFailureCode::constrained_newton_failed; + return false; + } + + std::vector grad(static_cast(tdim), T(0)); + for (int iter = 0; iter < options.max_iter; ++iter) + { + const T f = eval_on_point(std::span(out.data(), out.size())); + if (std::fabs(f) <= options.ftol) + { + stats.iterations = iter; + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = std::fabs(f); + return true; + } + + grad_on_point(std::span(out.data(), out.size()), + std::span(grad.data(), grad.size())); + auto pg = project_to_active_face_space( + ac, std::span(grad.data(), grad.size()), active_mask); + const T gg = vec_dot( + std::span(pg.data(), pg.size()), + std::span(pg.data(), pg.size())); + if (gg <= options.ftol * options.ftol) + { + stats.iterations = iter; + stats.failure_code = CurvingFailureCode::singular_gradient_system; + stats.residual = std::fabs(f); + return false; + } + + std::vector delta(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + delta[static_cast(d)] = + -f * pg[static_cast(d)] / gg; + + T lo = T(0), hi = T(0); + if (!host_parameter_interval( + ac, + std::span(out.data(), out.size()), + std::span(delta.data(), delta.size()), + options.domain_tol, + lo, + hi)) + { + stats.failure_code = CurvingFailureCode::no_host_interval; + stats.residual = std::fabs(f); + return false; + } + + T step = std::min(T(1), hi); + if (!(step > T(0))) + { + stats.failure_code = CurvingFailureCode::line_search_failed; + stats.residual = std::fabs(f); + return false; + } + + const T merit = T(0.5) * f * f; + bool accepted = false; + std::vector candidate(static_cast(tdim), T(0)); + for (int ls = 0; ls < 16; ++ls) + { + for (int d = 0; d < tdim; ++d) + candidate[static_cast(d)] = + out[static_cast(d)] + + step * delta[static_cast(d)]; + if (!cell::edge_root::is_inside_reference_domain( + std::span(candidate.data(), candidate.size()), + ac.parent_cell_type, + options.domain_tol)) + { + step *= T(0.5); + continue; + } + const T cand_f = eval_on_point( + std::span(candidate.data(), candidate.size())); + const T cand_merit = T(0.5) * cand_f * cand_f; + if (cand_merit < merit) + { + out = candidate; + accepted = true; + break; + } + step *= T(0.5); + } + if (!accepted) + { + stats.iterations = iter + 1; + stats.failure_code = CurvingFailureCode::line_search_failed; + stats.residual = std::fabs(f); + return false; + } + } + + const T f = eval_on_point(std::span(out.data(), out.size())); + stats.iterations = options.max_iter; + stats.residual = std::fabs(f); + if (stats.residual <= options.ftol) + { + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + return true; + } + stats.failure_code = CurvingFailureCode::constrained_newton_failed; + return false; +} + +template +bool solve_dense_small(std::vector A, std::vector b, int n, std::vector& x) +{ + x.assign(static_cast(n), T(0)); + if (n <= 0 || n > 3) + return false; + + for (int k = 0; k < n; ++k) + { + int pivot = k; + T best = std::fabs(A[static_cast(k * n + k)]); + for (int r = k + 1; r < n; ++r) + { + const T val = std::fabs(A[static_cast(r * n + k)]); + if (val > best) + { + best = val; + pivot = r; + } + } + if (best <= T(64) * std::numeric_limits::epsilon()) + return false; + if (pivot != k) + { + for (int c = k; c < n; ++c) + std::swap(A[static_cast(k * n + c)], + A[static_cast(pivot * n + c)]); + std::swap(b[static_cast(k)], b[static_cast(pivot)]); + } + + const T diag = A[static_cast(k * n + k)]; + for (int c = k; c < n; ++c) + A[static_cast(k * n + c)] /= diag; + b[static_cast(k)] /= diag; + + for (int r = 0; r < n; ++r) + { + if (r == k) + continue; + const T factor = A[static_cast(r * n + k)]; + if (factor == T(0)) + continue; + for (int c = k; c < n; ++c) + A[static_cast(r * n + c)] -= factor * A[static_cast(k * n + c)]; + b[static_cast(r)] -= factor * b[static_cast(k)]; + } + } + + x = std::move(b); + return true; +} + +template +std::vector projected_direction_to_host(const AdaptCell& ac, + int local_zero_entity_id, + std::span direction) +{ + const int tdim = ac.tdim; + std::vector out(direction.begin(), direction.end()); + const int parent_dim = ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; + const int parent_id = ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; + if (parent_dim < 0 || parent_dim >= tdim) + return out; + + std::vector ref_vertices = cell::reference_vertices(ac.parent_cell_type); + std::vector> basis; + + auto add_basis_from_vertices = [&](int v0, int v1) + { + std::array b = {T(0), T(0), T(0)}; + for (int d = 0; d < tdim; ++d) + b[static_cast(d)] = + ref_vertices[static_cast(v1 * tdim + d)] + - ref_vertices[static_cast(v0 * tdim + d)]; + basis.push_back(b); + }; + + if (parent_dim == 1) + { + auto edges = cell::edges(ac.parent_cell_type); + if (parent_id >= 0 && parent_id < static_cast(edges.size())) + add_basis_from_vertices(edges[static_cast(parent_id)][0], + edges[static_cast(parent_id)][1]); + } + else if (parent_dim == 2 && ac.tdim == 3) + { + auto fv = cell::face_vertices(ac.parent_cell_type, parent_id); + add_basis_from_vertices(fv[0], fv[1]); + add_basis_from_vertices(fv[0], fv[2]); + } + + if (basis.empty()) + return out; + + std::fill(out.begin(), out.end(), T(0)); + if (basis.size() == 1) + { + T bd = T(0); + T bb = T(0); + for (int d = 0; d < tdim; ++d) + { + bd += basis[0][static_cast(d)] * direction[static_cast(d)]; + bb += basis[0][static_cast(d)] * basis[0][static_cast(d)]; + } + if (bb > T(0)) + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] = (bd / bb) * basis[0][static_cast(d)]; + return out; + } + + T g00 = T(0), g01 = T(0), g11 = T(0), r0 = T(0), r1 = T(0); + for (int d = 0; d < tdim; ++d) + { + g00 += basis[0][static_cast(d)] * basis[0][static_cast(d)]; + g01 += basis[0][static_cast(d)] * basis[1][static_cast(d)]; + g11 += basis[1][static_cast(d)] * basis[1][static_cast(d)]; + r0 += basis[0][static_cast(d)] * direction[static_cast(d)]; + r1 += basis[1][static_cast(d)] * direction[static_cast(d)]; + } + const T det = g00 * g11 - g01 * g01; + if (std::fabs(det) <= T(64) * std::numeric_limits::epsilon()) + return out; + const T a = ( r0 * g11 - r1 * g01) / det; + const T b = (-r0 * g01 + r1 * g00) / det; + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] = + a * basis[0][static_cast(d)] + + b * basis[1][static_cast(d)]; + return out; +} + +template +std::vector active_level_sets(std::uint64_t mask) +{ + std::vector ids; + for (int i = 0; i < 64; ++i) + if ((mask & (std::uint64_t(1) << i)) != 0) + ids.push_back(i); + return ids; +} + +template +const LevelSetCell& find_level_set_cell(std::span> cells, + std::span offsets, + int cut_cell_id, + int level_set_id) +{ + for (int i = offsets[static_cast(cut_cell_id)]; + i < offsets[static_cast(cut_cell_id + 1)]; ++i) + { + if (cells[static_cast(i)].level_set_id == level_set_id) + return cells[static_cast(i)]; + } + throw std::runtime_error("curving: missing LevelSetCell for zero entity"); +} + +template +bool scalar_project(const AdaptCell& ac, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span seed, + const CurvingOptions& options, + std::vector& out, + ProjectionStats& stats) +{ + const int tdim = ac.tdim; + stats = {}; + stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; + const T seed_value = curving_level_set_value(ls_cell, seed); + if (std::fabs(seed_value) <= options.ftol) + { + out.assign(seed.begin(), seed.end()); + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = std::fabs(seed_value); + stats.active_face_mask = add_near_active_faces( + ac, + seed, + structural_active_face_mask(ac, local_zero_entity_id), + options.active_face_tol); + stats.safe_subspace_dim = active_subspace_dim(ac, stats.active_face_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + return true; + } + std::vector grad(static_cast(tdim), T(0)); + curving_level_set_grad( + ls_cell, seed, std::span(grad.data(), grad.size())); + const auto metric_grad = physical_normal_reference_direction( + ls_cell, std::span(grad.data(), grad.size())); + const auto host_metric_grad = physical_host_gradient_reference_direction( + ls_cell, + ac, + local_zero_entity_id, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size())); + + auto eval_on_point = [&](std::span x) -> T + { + return curving_level_set_value(ls_cell, x); + }; + auto grad_on_point = [&](std::span x, std::span out_grad) + { + ls_cell.grad(x, out_grad); + }; + + std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); + active_mask = add_near_active_faces(ac, seed, active_mask, options.active_face_tol); + + stats.active_face_mask = active_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + + auto directions = scalar_candidate_directions( + ac, + local_zero_entity_id, + seed, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size()), + std::span(host_metric_grad.data(), host_metric_grad.size()), + active_mask, + options); + if (try_scalar_line_search( + ac, seed, + std::span>(directions.data(), directions.size()), + options, + eval_on_point, + out, + stats)) + { + stats.active_face_mask = active_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + return true; + } + + int closest_face_id = -1; + const std::uint32_t retry_mask = + add_closest_faces(ac, seed, active_mask, closest_face_id); + if (retry_mask != active_mask) + { + ProjectionStats retry_stats = stats; + retry_stats.active_face_mask = retry_mask; + retry_stats.closest_face_id = closest_face_id; + retry_stats.safe_subspace_dim = active_subspace_dim(ac, retry_mask); + retry_stats.projection_mode = CurvingProjectionMode::closest_face_retry; + retry_stats.retry_count = 1; + + directions = scalar_candidate_directions( + ac, + local_zero_entity_id, + seed, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size()), + std::span(host_metric_grad.data(), host_metric_grad.size()), + retry_mask, + options); + if (try_scalar_line_search( + ac, seed, + std::span>(directions.data(), directions.size()), + options, + eval_on_point, + out, + retry_stats)) + { + retry_stats.active_face_mask = retry_mask; + retry_stats.closest_face_id = closest_face_id; + retry_stats.safe_subspace_dim = active_subspace_dim(ac, retry_mask); + retry_stats.projection_mode = CurvingProjectionMode::closest_face_retry; + retry_stats.retry_count = 1; + stats = retry_stats; + return true; + } + stats = retry_stats; + stats.failure_code = CurvingFailureCode::closest_face_retry_failed; + } + + std::array newton_masks = { + (retry_mask != active_mask) ? retry_mask : active_mask, + active_mask + }; + const int newton_attempts = (newton_masks[0] == newton_masks[1]) ? 1 : 2; + ProjectionStats best_newton_stats = stats; + for (int attempt = 0; attempt < newton_attempts; ++attempt) + { + ProjectionStats newton_stats = stats; + const std::uint32_t newton_mask = newton_masks[static_cast(attempt)]; + newton_stats.active_face_mask = newton_mask; + newton_stats.closest_face_id = (newton_mask == retry_mask) ? closest_face_id : -1; + newton_stats.retry_count = (retry_mask != active_mask) ? 2 + attempt : 1 + attempt; + if (constrained_scalar_newton( + ac, + seed, + newton_mask, + options, + eval_on_point, + grad_on_point, + out, + newton_stats)) + { + stats = newton_stats; + return true; + } + best_newton_stats = newton_stats; + } + + stats = best_newton_stats; + stats.status = CurvingStatus::failed; + if (stats.failure_code == CurvingFailureCode::none) + stats.failure_code = CurvingFailureCode::constrained_newton_failed; + return false; +} + +template +bool fixed_ray_scalar_project(const AdaptCell& ac, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span seed, + const CurvingOptions& options, + std::vector& out, + ProjectionStats& stats) +{ + const int tdim = ac.tdim; + stats = {}; + const std::uint32_t host_mask = + structural_active_face_mask(ac, local_zero_entity_id); + stats.active_face_mask = host_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; + + const T seed_value = curving_level_set_value(ls_cell, seed); + if (std::fabs(seed_value) <= options.ftol) + { + out.assign(seed.begin(), seed.end()); + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = std::fabs(seed_value); + return true; + } + + std::vector grad(static_cast(tdim), T(0)); + curving_level_set_grad( + ls_cell, seed, std::span(grad.data(), grad.size())); + const auto metric_grad = physical_normal_reference_direction( + ls_cell, std::span(grad.data(), grad.size())); + auto direction = physical_host_gradient_reference_direction( + ls_cell, + ac, + local_zero_entity_id, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size())); + direction = project_to_active_face_space( + ac, std::span(direction.data(), direction.size()), host_mask); + + std::vector> directions; + const T direction_tol = + std::max(options.ftol, T(64) * std::numeric_limits::epsilon()); + append_unique_direction(directions, direction, direction_tol); + + auto normal = zero_entity_face_normal(ac, local_zero_entity_id); + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(normal.data(), normal.size()), host_mask), + direction_tol); + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(grad.data(), grad.size()), host_mask), + direction_tol); + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(metric_grad.data(), metric_grad.size()), host_mask), + direction_tol); + + const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); + const int nv = cell::get_num_vertices(ac.parent_cell_type); + for (int v = 0; v < nv; ++v) + { + std::vector ray(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + ray[static_cast(d)] = + seed[static_cast(d)] + - parent_vertices[static_cast(v * tdim + d)]; + append_unique_direction( + directions, + project_to_active_face_space( + ac, std::span(ray.data(), ray.size()), host_mask), + direction_tol); + } + + const auto basis = active_nullspace_basis(ac, host_mask); + for (const auto& b : basis) + append_unique_direction(directions, b, direction_tol); + + if (directions.empty()) + { + stats.failure_code = CurvingFailureCode::singular_gradient_system; + return false; + } + + auto eval_on_point = [&](std::span x) -> T + { + return curving_level_set_value(ls_cell, x); + }; + + bool found = false; + T best_step2 = std::numeric_limits::infinity(); + std::vector best_point; + ProjectionStats best_stats = stats; + for (const auto& unit : directions) + { + std::vector candidate; + ProjectionStats candidate_stats = stats; + if (!try_scalar_line_search( + ac, + seed, + std::span>(&unit, 1), + options, + eval_on_point, + candidate, + candidate_stats)) + { + if (!found) + best_stats = candidate_stats; + continue; + } + + T step2 = T(0); + for (int d = 0; d < tdim; ++d) + { + const T delta = candidate[static_cast(d)] + - seed[static_cast(d)]; + step2 += delta * delta; + } + if (!found || step2 < best_step2) + { + found = true; + best_step2 = step2; + best_point = std::move(candidate); + best_stats = candidate_stats; + } + } + + if (found) + { + out = std::move(best_point); + stats = best_stats; + } + else + { + stats = best_stats; + } + stats.active_face_mask = host_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + return found; +} + +template +bool vector_project(const AdaptCell& ac, + int local_zero_entity_id, + std::span* const> ls_cells, + std::span seed, + const CurvingOptions& options, + std::vector& out, + ProjectionStats& stats) +{ + const int tdim = ac.tdim; + const int m = static_cast(ls_cells.size()); + stats = {}; + stats.failure_code = CurvingFailureCode::projection_failed; + out.assign(seed.begin(), seed.end()); + if (m <= 0 || m > tdim) + { + stats.failure_code = CurvingFailureCode::invalid_constraint_count; + return false; + } + + std::vector values(static_cast(m), T(0)); + std::vector grads(static_cast(m * tdim), T(0)); + std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); + active_mask = add_near_active_faces(ac, seed, active_mask, options.active_face_tol); + int closest_face_id = -1; + int retry_count = 0; + + for (int iter = 0; iter < options.max_iter; ++iter) + { + T norm_f = T(0); + for (int i = 0; i < m; ++i) + { + values[static_cast(i)] = + ls_cells[static_cast(i)]->value( + std::span(out.data(), out.size())); + norm_f += values[static_cast(i)] * values[static_cast(i)]; + ls_cells[static_cast(i)]->grad( + std::span(out.data(), out.size()), + std::span(grads.data() + static_cast(i * tdim), + static_cast(tdim))); + } + norm_f = std::sqrt(norm_f); + if (norm_f <= options.ftol) + { + stats.iterations = iter; + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return true; + } + + const auto basis = active_nullspace_basis(ac, active_mask); + const int q = static_cast(basis.size()); + if (q < m) + { + const std::uint32_t retry_mask = + add_closest_faces( + ac, + std::span(out.data(), out.size()), + active_mask, + closest_face_id); + if (retry_mask != active_mask) + { + active_mask = retry_mask; + ++retry_count; + continue; + } + stats.iterations = iter; + stats.failure_code = CurvingFailureCode::singular_gradient_system; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = q; + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return false; + } + + std::vector jq(static_cast(m * q), T(0)); + for (int i = 0; i < m; ++i) + { + std::span gi( + grads.data() + static_cast(i * tdim), + static_cast(tdim)); + for (int k = 0; k < q; ++k) + jq[static_cast(i * q + k)] = + vec_dot(gi, std::span(basis[static_cast(k)].data(), + basis[static_cast(k)].size())); + } + + std::vector normal_matrix(static_cast(m * m), T(0)); + for (int i = 0; i < m; ++i) + { + for (int j = 0; j < m; ++j) + { + T a = T(0); + for (int k = 0; k < q; ++k) + a += jq[static_cast(i * q + k)] + * jq[static_cast(j * q + k)]; + normal_matrix[static_cast(i * m + j)] = a; + } + } + std::vector rhs(static_cast(m), T(0)); + for (int i = 0; i < m; ++i) + rhs[static_cast(i)] = -values[static_cast(i)]; + + std::vector lambda; + if (!solve_dense_small(normal_matrix, rhs, m, lambda)) + { + const std::uint32_t retry_mask = + add_closest_faces( + ac, + std::span(out.data(), out.size()), + active_mask, + closest_face_id); + if (retry_mask != active_mask) + { + active_mask = retry_mask; + ++retry_count; + continue; + } + stats.iterations = iter; + stats.failure_code = CurvingFailureCode::singular_gradient_system; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = q; + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return false; + } + + std::vector delta(static_cast(tdim), T(0)); + for (int k = 0; k < q; ++k) + { + T dy = T(0); + for (int i = 0; i < m; ++i) + dy += jq[static_cast(i * q + k)] + * lambda[static_cast(i)]; + for (int d = 0; d < tdim; ++d) + delta[static_cast(d)] += + dy * basis[static_cast(k)][static_cast(d)]; + } + + if (vec_norm(std::span(delta.data(), delta.size())) <= options.xtol) + { + stats.iterations = iter; + stats.failure_code = CurvingFailureCode::singular_gradient_system; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = q; + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return false; + } + + T lo = T(0), hi = T(0); + if (!host_parameter_interval( + ac, + std::span(out.data(), out.size()), + std::span(delta.data(), delta.size()), + options.domain_tol, + lo, + hi)) + { + stats.iterations = iter; + stats.failure_code = CurvingFailureCode::no_host_interval; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = q; + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return false; + } + + T step = T(1); + if (hi > T(0) && hi < step) + step = hi; + bool accepted = false; + std::vector candidate(static_cast(tdim), T(0)); + const T merit = T(0.5) * norm_f * norm_f; + for (int ls = 0; ls < 12; ++ls) + { + for (int d = 0; d < tdim; ++d) + candidate[static_cast(d)] = + out[static_cast(d)] + step * delta[static_cast(d)]; + if (!cell::edge_root::is_inside_reference_domain( + std::span(candidate.data(), candidate.size()), + ac.parent_cell_type, + options.domain_tol)) + { + step *= T(0.5); + continue; + } + T cand_norm = T(0); + for (int i = 0; i < m; ++i) + { + const T f = ls_cells[static_cast(i)]->value( + std::span(candidate.data(), candidate.size())); + cand_norm += f * f; + } + const T cand_merit = T(0.5) * cand_norm; + if (cand_merit < merit) + { + out = candidate; + accepted = true; + break; + } + step *= T(0.5); + } + if (!accepted) + { + const std::uint32_t retry_mask = + add_closest_faces( + ac, + std::span(out.data(), out.size()), + active_mask, + closest_face_id); + if (retry_mask != active_mask) + { + active_mask = retry_mask; + ++retry_count; + continue; + } + stats.iterations = iter + 1; + stats.failure_code = CurvingFailureCode::line_search_failed; + stats.residual = norm_f; + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = q; + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + return false; + } + } + + T final_norm = T(0); + for (int i = 0; i < m; ++i) + { + const T f = ls_cells[static_cast(i)]->value( + std::span(out.data(), out.size())); + final_norm += f * f; + } + stats.iterations = options.max_iter; + stats.residual = std::sqrt(final_norm); + stats.active_face_mask = active_mask; + stats.closest_face_id = closest_face_id; + stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); + stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.retry_count = retry_count; + if (stats.residual <= options.ftol) + { + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + return true; + } + stats.status = CurvingStatus::failed; + stats.failure_code = CurvingFailureCode::max_iterations; + return false; +} + +template +bool project_seed_to_zero_entity(const AdaptCell& ac, + int local_zero_entity_id, + std::span* const> active_cells, + std::span seed, + const CurvingOptions& options, + std::vector& projected, + ProjectionStats& stats) +{ + if (active_cells.size() == 1) + { + return scalar_project( + ac, local_zero_entity_id, *active_cells[0], seed, options, projected, stats); + } + return vector_project( + ac, local_zero_entity_id, active_cells, seed, options, projected, stats); +} + +template +bool build_hierarchical_face_nodes( + CurvedZeroEntityState& state, + const AdaptCell& ac, + int local_zero_entity_id, + std::span* const> active_cells, + std::span> boundary_edges, + const CurvingOptions& options) +{ + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + const auto entity_type = ac.entity_types[2][static_cast(zid)]; + auto verts_span = ac.entity_to_vertex[2][static_cast(zid)]; + std::vector verts; + verts.reserve(verts_span.size()); + for (const auto v : verts_span) + verts.push_back(static_cast(v)); + + const int order = std::max(options.geometry_order, 1); + const T eps = T(64) * std::numeric_limits::epsilon(); + std::vector projected; + state.ref_nodes.clear(); + + auto append_vertex = [&](int vertex_id) + { + for (int d = 0; d < ac.tdim; ++d) + state.ref_nodes.push_back( + ac.vertex_coords[static_cast(vertex_id * ac.tdim + d)]); + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::exact_vertex)); + }; + + auto straight_seed = [&](std::span weights) + { + std::array seed = {T(0), T(0), T(0)}; + for (std::size_t v = 0; v < weights.size(); ++v) + { + for (int d = 0; d < ac.tdim; ++d) + { + seed[static_cast(d)] += weights[v] * ac.vertex_coords[ + static_cast(verts[v] * ac.tdim + d)]; + } + } + return seed; + }; + + auto project_face_interior_seed = [&](std::span seed, + std::vector& candidate, + ProjectionStats& stats) + { + candidate.clear(); + stats = {}; + if (active_cells.size() != 1) + { + stats.failure_code = CurvingFailureCode::invalid_constraint_count; + stats.projection_mode = CurvingProjectionMode::safe_line; + stats.active_face_mask = + structural_active_face_mask(ac, local_zero_entity_id); + stats.safe_subspace_dim = active_subspace_dim( + ac, stats.active_face_mask); + return false; + } + return fixed_ray_scalar_project( + ac, + local_zero_entity_id, + *active_cells[0], + seed, + options, + candidate, + stats); + }; + + auto append_straight_face_interior_seed = [&](std::span weights) + { + const auto seed = straight_seed(weights); + ProjectionStats stats; + if (!project_face_interior_seed( + std::span(seed.data(), static_cast(ac.tdim)), + projected, + stats)) + { + append_node_stats(state, stats); + return false; + } + state.ref_nodes.insert(state.ref_nodes.end(), projected.begin(), projected.end()); + append_node_stats(state, stats); + return true; + }; + + auto append_straight_edge_node = [&](int v0, int v1, T t) + { + for (int d = 0; d < ac.tdim; ++d) + { + const T x0 = ac.vertex_coords[static_cast(v0 * ac.tdim + d)]; + const T x1 = ac.vertex_coords[static_cast(v1 * ac.tdim + d)]; + state.ref_nodes.push_back((T(1) - t) * x0 + t * x1); + } + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + }; + + auto eval_curved_edge = [&](int v0, int v1, T t, std::vector& x) -> bool + { + const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); + if (edge == nullptr || !edge->use_curved_state) + return false; + x = eval_edge_state_at(*edge, options, v0, v1, t); + return true; + }; + + auto add_scaled = [&](std::array& out, + T scale, + std::span x) + { + for (int d = 0; d < ac.tdim; ++d) + out[static_cast(d)] += scale * x[static_cast(d)]; + }; + + auto sub_scaled_vertex = [&](std::array& out, T scale, int vertex_id) + { + for (int d = 0; d < ac.tdim; ++d) + out[static_cast(d)] -= scale * ac.vertex_coords[ + static_cast(vertex_id * ac.tdim + d)]; + }; + + auto append_transfinite_or_straight_seed = + [&](std::array transfinite_seed, + std::span straight_weights) -> bool + { + // Strict face-interior nodes are lifted by a scalar solve along one + // frozen ray through the transfinite seed. This preserves the face + // parameter location; the generic scalar projector can otherwise + // select fallback directions that satisfy phi=0 but drift tangentially. + ProjectionStats transfinite_stats; + std::vector transfinite_projected; + if (project_face_interior_seed( + std::span( + transfinite_seed.data(), static_cast(ac.tdim)), + transfinite_projected, + transfinite_stats)) + { + state.ref_nodes.insert( + state.ref_nodes.end(), + transfinite_projected.begin(), + transfinite_projected.end()); + append_node_stats(state, transfinite_stats); + return true; + } + + const auto fallback_seed = straight_seed(straight_weights); + ProjectionStats fallback_stats; + std::vector fallback_projected; + if (!project_face_interior_seed( + std::span( + fallback_seed.data(), static_cast(ac.tdim)), + fallback_projected, + fallback_stats)) + { + append_node_stats(state, fallback_stats); + return false; + } + state.ref_nodes.insert( + state.ref_nodes.end(), + fallback_projected.begin(), + fallback_projected.end()); + append_node_stats(state, fallback_stats); + return true; + }; + + auto append_triangle_interior_node = [&](std::array w) -> bool + { + const int v0 = verts[0]; + const int v1 = verts[1]; + const int v2 = verts[2]; + std::vector c01; + std::vector c12; + std::vector c02; + + const T s01 = w[0] + w[1]; + const T s12 = w[1] + w[2]; + const T s02 = w[0] + w[2]; + if (s01 <= eps || s12 <= eps || s02 <= eps + || !eval_curved_edge(v0, v1, w[1] / s01, c01) + || !eval_curved_edge(v1, v2, w[2] / s12, c12) + || !eval_curved_edge(v0, v2, w[2] / s02, c02)) + { + return append_straight_face_interior_seed( + std::span(w.data(), w.size())); + } + + std::array seed = {T(0), T(0), T(0)}; + add_scaled(seed, s01, std::span(c01.data(), c01.size())); + add_scaled(seed, s12, std::span(c12.data(), c12.size())); + add_scaled(seed, s02, std::span(c02.data(), c02.size())); + sub_scaled_vertex(seed, w[0], v0); + sub_scaled_vertex(seed, w[1], v1); + sub_scaled_vertex(seed, w[2], v2); + return append_transfinite_or_straight_seed( + seed, std::span(w.data(), w.size())); + }; + + auto append_quad_interior_node = [&](T u, T v) -> bool + { + std::array w = {(T(1) - u) * (T(1) - v), + u * (T(1) - v), + (T(1) - u) * v, + u * v}; + std::vector bottom; + std::vector top; + std::vector left; + std::vector right; + if (!eval_curved_edge(verts[0], verts[1], u, bottom) + || !eval_curved_edge(verts[2], verts[3], u, top) + || !eval_curved_edge(verts[0], verts[2], v, left) + || !eval_curved_edge(verts[1], verts[3], v, right)) + { + return append_straight_face_interior_seed( + std::span(w.data(), w.size())); + } + + std::array seed = {T(0), T(0), T(0)}; + add_scaled(seed, T(1) - v, std::span(bottom.data(), bottom.size())); + add_scaled(seed, v, std::span(top.data(), top.size())); + add_scaled(seed, T(1) - u, std::span(left.data(), left.size())); + add_scaled(seed, u, std::span(right.data(), right.size())); + sub_scaled_vertex(seed, w[0], verts[0]); + sub_scaled_vertex(seed, w[1], verts[1]); + sub_scaled_vertex(seed, w[2], verts[2]); + sub_scaled_vertex(seed, w[3], verts[3]); + return append_transfinite_or_straight_seed( + seed, std::span(w.data(), w.size())); + }; + + auto append_triangle_node = [&](std::array w) -> bool + { + int zero_count = 0; + int one_index = -1; + int edge_a = -1; + int edge_b = -1; + for (int k = 0; k < 3; ++k) + { + if (std::abs(w[static_cast(k)]) <= eps) + ++zero_count; + if (std::abs(w[static_cast(k)] - T(1)) <= eps) + one_index = k; + else if (w[static_cast(k)] > eps) + { + if (edge_a < 0) + edge_a = k; + else + edge_b = k; + } + } + + if (one_index >= 0) + { + append_vertex(verts[static_cast(one_index)]); + return true; + } + if (zero_count == 1 && edge_a >= 0 && edge_b >= 0) + { + const int v0 = verts[static_cast(edge_a)]; + const int v1 = verts[static_cast(edge_b)]; + const T denom = w[static_cast(edge_a)] + + w[static_cast(edge_b)]; + const T t = w[static_cast(edge_b)] / denom; + const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); + if (edge == nullptr) + { + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::missing_boundary_edge; + append_node_stats(state, stats); + return false; + } + if (!edge->use_curved_state) + { + append_straight_edge_node(v0, v1, t); + return true; + } + const auto x = eval_edge_state_at(*edge, options, v0, v1, t); + state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + return true; + } + return append_triangle_interior_node(w); + }; + + if (entity_type == cell::type::triangle) + { + const auto nodes = + triangle_interpolation_barycentric_nodes(order, options.node_family); + for (const auto& w : nodes) + { + if (!append_triangle_node(w)) + { + state.failure_reason = "missing or failed hierarchical curved face boundary edge"; + return false; + } + } + return true; + } + + if (entity_type == cell::type::quadrilateral) + { + const auto params = interpolation_parameters(order, options.node_family); + for (const T v : params) + { + for (const T u : params) + { + const bool on_u0 = std::abs(u) <= eps; + const bool on_u1 = std::abs(u - T(1)) <= eps; + const bool on_v0 = std::abs(v) <= eps; + const bool on_v1 = std::abs(v - T(1)) <= eps; + + if ((on_u0 || on_u1) && (on_v0 || on_v1)) + { + int vertex = -1; + if (on_u0 && on_v0) + { + vertex = verts[0]; + } + else if (on_u1 && on_v0) + { + vertex = verts[1]; + } + else if (on_u0 && on_v1) + { + vertex = verts[2]; + } + else + { + vertex = verts[3]; + } + append_vertex(vertex); + continue; + } + + if (on_v0 || on_v1 || on_u0 || on_u1) + { + int v0 = -1; + int v1 = -1; + T t = T(0); + if (on_v0) + { + v0 = verts[0]; v1 = verts[1]; t = u; + } + else if (on_v1) + { + v0 = verts[2]; v1 = verts[3]; t = u; + } + else if (on_u0) + { + v0 = verts[0]; v1 = verts[2]; t = v; + } + else + { + v0 = verts[1]; v1 = verts[3]; t = v; + } + const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); + if (edge == nullptr) + { + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::missing_boundary_edge; + append_node_stats(state, stats); + state.failure_reason = "missing or failed hierarchical curved face boundary edge"; + return false; + } + if (!edge->use_curved_state) + { + append_straight_edge_node(v0, v1, t); + continue; + } + const auto x = eval_edge_state_at(*edge, options, v0, v1, t); + state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + continue; + } + + if (!append_quad_interior_node(u, v)) + { + state.failure_reason = "projection failed inside parent host domain"; + return false; + } + } + } + return true; + } + + state.failure_reason = "curving: unsupported zero-face type"; + return false; +} + +template +void build_curved_state(CurvedZeroEntityState& state, + const AdaptCell& ac, + std::span> level_set_cells, + std::span ls_offsets, + int cut_cell_id, + int local_zero_entity_id, + std::span> boundary_edges, + const CurvingOptions& options) +{ + state.status = CurvingStatus::in_progress; + state.failure_reason.clear(); + state.geometry_order = options.geometry_order; + state.node_family = options.node_family; + state.zero_entity_version = ac.zero_entity_version; + state.zero_mask = ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; + state.ref_nodes.clear(); + state.node_iterations.clear(); + state.node_status.clear(); + state.node_failure_code.clear(); + state.node_residual.clear(); + state.node_active_face_mask.clear(); + state.node_closest_face_id.clear(); + state.node_safe_subspace_dim.clear(); + state.node_projection_mode.clear(); + state.node_retry_count.clear(); + + std::vector seeds; + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim == 0) + { + state.ref_nodes = zero_entity_vertices(ac, local_zero_entity_id); + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::exact_vertex)); + state.status = CurvingStatus::curved; + return; + } + if (zdim == 1) + append_edge_seed_nodes(ac, local_zero_entity_id, options, seeds); + else if (zdim == 2) + { + // Face states are built hierarchically below after active constraints + // are available: vertices are kept exact, boundary nodes come from + // already curved zero edges, and only strict face-interior nodes are + // projected. + } + else + { + state.status = CurvingStatus::failed; + state.failure_reason = "unsupported zero-entity dimension"; + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::unsupported_entity; + append_node_stats(state, stats); + return; + } + + const std::uint64_t effective_zero_mask = state.zero_mask & ac.active_level_set_mask; + const auto ls_ids = active_level_sets(effective_zero_mask); + if (ls_ids.empty()) + { + if (zdim == 2) + append_face_seed_nodes(ac, local_zero_entity_id, options, seeds); + state.ref_nodes = seeds; + const int n_nodes = static_cast( + seeds.size() / static_cast(std::max(ac.tdim, 1))); + for (int i = 0; i < n_nodes; ++i) + append_node_stats( + state, accepted_node_stats(CurvingFailureCode::none)); + state.status = CurvingStatus::curved; + return; + } + + std::vector*> active_cells; + active_cells.reserve(ls_ids.size()); + try + { + for (const int ls_id : ls_ids) + active_cells.push_back(&find_level_set_cell( + level_set_cells, ls_offsets, cut_cell_id, ls_id)); + } + catch (const std::exception& e) + { + state.status = CurvingStatus::failed; + state.failure_reason = e.what(); + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::missing_level_set_cell; + append_node_stats(state, stats); + return; + } + + if (zdim == 2) + { + if (!build_hierarchical_face_nodes( + state, + ac, + local_zero_entity_id, + std::span* const>( + active_cells.data(), active_cells.size()), + boundary_edges, + options)) + { + state.status = CurvingStatus::failed; + if (state.failure_reason.empty()) + state.failure_reason = "hierarchical zero-face curving failed"; + state.ref_nodes.clear(); + return; + } + state.status = CurvingStatus::curved; + return; + } + + const int nseeds = static_cast(seeds.size() / static_cast(ac.tdim)); + state.ref_nodes.reserve(seeds.size()); + std::vector projected; + for (int i = 0; i < nseeds; ++i) + { + std::span seed( + seeds.data() + static_cast(i * ac.tdim), + static_cast(ac.tdim)); + + ProjectionStats stats; + const bool ok = project_seed_to_zero_entity( + ac, + local_zero_entity_id, + std::span* const>( + active_cells.data(), active_cells.size()), + seed, + options, + projected, + stats); + append_node_stats(state, stats); + + if (!ok) + { + state.status = CurvingStatus::failed; + state.failure_reason = "projection failed inside parent host domain"; + state.ref_nodes.clear(); + return; + } + state.ref_nodes.insert(state.ref_nodes.end(), projected.begin(), projected.end()); + } + + state.status = CurvingStatus::curved; +} + +} // namespace + +NodeFamily node_family_from_string(std::string_view name) +{ + if (name == "gll" || name == "lobatto" + || name == "fekete" || name == "warp_blend" || name == "warp-blend") + return NodeFamily::gll; + if (name == "equispaced") + return NodeFamily::equispaced; + if (name == "lagrange") + return NodeFamily::lagrange; + throw std::invalid_argument("unknown curving node family"); +} + +std::string_view node_family_name(NodeFamily family) +{ + switch (family) + { + case NodeFamily::gll: return "gll"; + case NodeFamily::equispaced: return "equispaced"; + case NodeFamily::lagrange: return "lagrange"; + } + return "unknown"; +} + +template +void rebuild_identity(CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells) +{ + curving.clear(); + curving.num_cut_cells = static_cast(adapt_cells.size()); + curving.local_to_canonical_offsets.reserve(adapt_cells.size() + 1); + curving.local_to_canonical_offsets.push_back(0); + + for (int c = 0; c < static_cast(adapt_cells.size()); ++c) + { + const auto& ac = adapt_cells[static_cast(c)]; + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + CurvingIdentity id; + id.cut_cell_id = c; + id.local_zero_entity_id = z; + id.dim = ac.zero_entity_dim[static_cast(z)]; + id.parent_dim = ac.zero_entity_parent_dim[static_cast(z)]; + id.parent_id = ac.zero_entity_parent_id[static_cast(z)]; + if (id.parent_dim == ac.tdim && id.parent_id < 0) + id.parent_id = static_cast(parent_cell_ids[static_cast(c)]); + id.zero_mask = ac.zero_entity_zero_mask[static_cast(z)]; + curving.identities.push_back(id); + curving.states.emplace_back(); + curving.local_to_canonical.push_back( + static_cast(curving.identities.size()) - 1); + } + curving.local_to_canonical_offsets.push_back( + static_cast(curving.local_to_canonical.size())); + } + + curving.identity_valid = true; +} + +template +std::vector face_boundary_zero_edges(const AdaptCell& ac, + int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 2) + return {}; + + const std::uint64_t face_mask = + ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + const auto face_type = ac.entity_types[2][static_cast(zid)]; + auto face_verts_span = ac.entity_to_vertex[2][static_cast(zid)]; + std::vector face_verts; + face_verts.reserve(face_verts_span.size()); + for (const auto v : face_verts_span) + face_verts.push_back(static_cast(v)); + + std::vector edge_ids; + for (const auto& edge : cell::edges(face_type)) + { + std::array target = { + face_verts[static_cast(edge[0])], + face_verts[static_cast(edge[1])] + }; + int match = -1; + bool use_curved_state = true; + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (ac.zero_entity_dim[static_cast(z)] != 1) + continue; + const auto edge_mask = ac.zero_entity_zero_mask[static_cast(z)]; + if ((edge_mask & face_mask) != face_mask) + continue; + const auto edge_verts = zero_entity_vertex_ids(ac, z); + if (same_unordered_vertices( + std::span(target.data(), target.size()), + std::span(edge_verts.data(), edge_verts.size()))) + { + match = z; + use_curved_state = true; + break; + } + } + if (match < 0) + return {}; + edge_ids.push_back({match, use_curved_state}); + } + return edge_ids; +} + +template +const CurvedZeroEntityState& ensure_curved( + CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells, + std::span> level_set_cells, + std::span ls_offsets, + int cut_cell_id, + int local_zero_entity_id, + const CurvingOptions& options) +{ + if (!curving.identity_valid + || curving.num_cut_cells != static_cast(adapt_cells.size())) + { + rebuild_identity(curving, parent_cell_ids, adapt_cells); + } + if (cut_cell_id < 0 || cut_cell_id >= static_cast(adapt_cells.size())) + throw std::out_of_range("curving: cut_cell_id out of range"); + + const int begin = curving.local_to_canonical_offsets[static_cast(cut_cell_id)]; + const int end = curving.local_to_canonical_offsets[static_cast(cut_cell_id + 1)]; + if (local_zero_entity_id < 0 || begin + local_zero_entity_id >= end) + throw std::out_of_range("curving: local_zero_entity_id out of range"); + + const int canonical = curving.local_to_canonical[static_cast(begin + local_zero_entity_id)]; + auto& state = curving.states[static_cast(canonical)]; + const auto& ac = adapt_cells[static_cast(cut_cell_id)]; + const bool valid = + state.status == CurvingStatus::curved + && state.geometry_order == options.geometry_order + && state.node_family == options.node_family + && state.zero_entity_version == ac.zero_entity_version; + const bool failed = + state.status == CurvingStatus::failed + && state.geometry_order == options.geometry_order + && state.node_family == options.node_family + && state.zero_entity_version == ac.zero_entity_version; + if (!valid && !failed) + { + std::vector> boundary_edges; + if (ac.zero_entity_dim[static_cast(local_zero_entity_id)] == 2) + { + const auto boundary_edge_refs = + face_boundary_zero_edges(ac, local_zero_entity_id); + if (boundary_edge_refs.empty()) + { + state.status = CurvingStatus::failed; + state.failure_reason = "hierarchical zero-face curving requires boundary zero edges"; + state.geometry_order = options.geometry_order; + state.node_family = options.node_family; + state.zero_entity_version = ac.zero_entity_version; + state.zero_mask = ac.zero_entity_zero_mask[ + static_cast(local_zero_entity_id)]; + state.ref_nodes.clear(); + state.node_iterations.clear(); + state.node_status.clear(); + state.node_failure_code.clear(); + state.node_residual.clear(); + state.node_active_face_mask.clear(); + state.node_closest_face_id.clear(); + state.node_safe_subspace_dim.clear(); + state.node_projection_mode.clear(); + state.node_retry_count.clear(); + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::missing_boundary_edge; + append_node_stats(state, stats); + return state; + } + + boundary_edges.reserve(boundary_edge_refs.size()); + for (const auto& edge_ref : boundary_edge_refs) + { + const CurvedZeroEntityState* edge_state = nullptr; + if (edge_ref.use_curved_state) + { + edge_state = &ensure_curved( + curving, + parent_cell_ids, + adapt_cells, + level_set_cells, + ls_offsets, + cut_cell_id, + edge_ref.local_zero_entity_id, + options); + if (edge_state->status != CurvingStatus::curved) + { + state.status = CurvingStatus::failed; + state.failure_reason = "hierarchical zero-face boundary edge curving failed"; + state.geometry_order = options.geometry_order; + state.node_family = options.node_family; + state.zero_entity_version = ac.zero_entity_version; + state.zero_mask = ac.zero_entity_zero_mask[ + static_cast(local_zero_entity_id)]; + state.ref_nodes.clear(); + state.node_iterations.clear(); + state.node_status.clear(); + state.node_failure_code.clear(); + state.node_residual.clear(); + state.node_active_face_mask.clear(); + state.node_closest_face_id.clear(); + state.node_safe_subspace_dim.clear(); + state.node_projection_mode.clear(); + state.node_retry_count.clear(); + ProjectionStats stats; + stats.failure_code = CurvingFailureCode::boundary_edge_failed; + append_node_stats(state, stats); + return state; + } + } + const auto edge_verts = + zero_entity_vertex_ids(ac, edge_ref.local_zero_entity_id); + BoundaryEdgeState boundary; + boundary.vertices = {edge_verts[0], edge_verts[1]}; + boundary.state = edge_state; + boundary.use_curved_state = edge_ref.use_curved_state; + boundary_edges.push_back(boundary); + } + } + + build_curved_state( + state, ac, level_set_cells, ls_offsets, + cut_cell_id, local_zero_entity_id, + std::span>( + boundary_edges.data(), boundary_edges.size()), + options); + } + return state; +} + +template +void ensure_all_curved(CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells, + std::span> level_set_cells, + std::span ls_offsets, + const CurvingOptions& options) +{ + if (!curving.identity_valid + || curving.num_cut_cells != static_cast(adapt_cells.size())) + { + rebuild_identity(curving, parent_cell_ids, adapt_cells); + } + for (int c = 0; c < static_cast(adapt_cells.size()); ++c) + { + for (int z = 0; z < adapt_cells[static_cast(c)].n_zero_entities(); ++z) + (void)ensure_curved( + curving, parent_cell_ids, adapt_cells, level_set_cells, ls_offsets, + c, z, options); + } +} + +template void rebuild_identity(CurvingData&, std::span, std::span>); +template void rebuild_identity(CurvingData&, std::span, std::span>); +template void rebuild_identity(CurvingData&, std::span, std::span>); +template void rebuild_identity(CurvingData&, std::span, std::span>); + +template const CurvedZeroEntityState& ensure_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, int, int, + const CurvingOptions&); +template const CurvedZeroEntityState& ensure_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, int, int, + const CurvingOptions&); +template const CurvedZeroEntityState& ensure_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, int, int, + const CurvingOptions&); +template const CurvedZeroEntityState& ensure_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, int, int, + const CurvingOptions&); + +template void ensure_all_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, + const CurvingOptions&); +template void ensure_all_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, + const CurvingOptions&); +template void ensure_all_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, + const CurvingOptions&); +template void ensure_all_curved( + CurvingData&, std::span, std::span>, + std::span>, std::span, + const CurvingOptions&); + +} // namespace cutcells::curving diff --git a/cpp/src/curving.h b/cpp/src/curving.h new file mode 100644 index 0000000..90d59f1 --- /dev/null +++ b/cpp/src/curving.h @@ -0,0 +1,170 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include + +#include "adapt_cell.h" +#include "level_set_cell.h" + +namespace cutcells::curving +{ + +enum class NodeFamily : std::uint8_t +{ + gll = 0, + equispaced = 1, + lagrange = 2 +}; + +enum class CurvingStatus : std::uint8_t +{ + not_built = 0, + in_progress = 1, + curved = 2, + failed = 3 +}; + +enum class CurvingFailureCode : std::uint8_t +{ + none = 0, + exact_vertex = 1, + boundary_from_edge = 2, + invalid_constraint_count = 3, + missing_level_set_cell = 4, + empty_zero_mask = 5, + unsupported_entity = 6, + no_host_interval = 7, + no_sign_changing_bracket = 8, + brent_failed = 9, + outside_host_domain = 10, + singular_gradient_system = 11, + line_search_failed = 12, + max_iterations = 13, + missing_boundary_edge = 14, + boundary_edge_failed = 15, + projection_failed = 16, + closest_face_retry_failed = 17, + constrained_newton_failed = 18 +}; + +enum class CurvingProjectionMode : std::uint8_t +{ + none = 0, + safe_line = 1, + closest_face_retry = 2, + constrained_newton = 3, + vector_newton = 4 +}; + +template +struct CurvingOptions +{ + int geometry_order = 2; + NodeFamily node_family = NodeFamily::gll; + int max_iter = 32; + T xtol = T(1e-12); + T ftol = T(1e-12); + T domain_tol = T(1e-10); + T active_face_tol = T(1e-9); + int max_subdivision_depth = 3; +}; + +template +struct CurvedZeroEntityState +{ + CurvingStatus status = CurvingStatus::not_built; + int geometry_order = -1; + NodeFamily node_family = NodeFamily::gll; + std::uint32_t zero_entity_version = 0; + std::uint64_t zero_mask = 0; + std::string failure_reason; + + // Full curved interpolation nodes, including endpoints/boundary nodes. + // Coordinates are in the parent background cell reference frame. + std::vector ref_nodes; + + // Per intended interpolation node, including nodes that failed before the + // entity as a whole was accepted. Successful vertices/boundary nodes have + // zero iterations and a non-error failure code documenting their origin. + std::vector node_iterations; + std::vector node_status; + std::vector node_failure_code; + std::vector node_residual; + std::vector node_active_face_mask; + std::vector node_closest_face_id; + std::vector node_safe_subspace_dim; + std::vector node_projection_mode; + std::vector node_retry_count; +}; + +struct CurvingIdentity +{ + int cut_cell_id = -1; + int local_zero_entity_id = -1; + int dim = -1; + std::int8_t parent_dim = -1; + std::int32_t parent_id = -1; + std::uint64_t zero_mask = 0; +}; + +template +struct CurvingData +{ + std::vector identities; + std::vector> states; + + // CSR-like local lookup: local_to_canonical_offsets[k]..[k+1] belongs to + // cut cell k and stores canonical ids for local zero entity ids. + std::vector local_to_canonical_offsets; + std::vector local_to_canonical; + + int num_cut_cells = 0; + bool identity_valid = false; + + void clear() + { + identities.clear(); + states.clear(); + local_to_canonical_offsets.clear(); + local_to_canonical.clear(); + num_cut_cells = 0; + identity_valid = false; + } +}; + +NodeFamily node_family_from_string(std::string_view name); +std::string_view node_family_name(NodeFamily family); + +template +void rebuild_identity(CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells); + +template +const CurvedZeroEntityState& ensure_curved( + CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells, + std::span> level_set_cells, + std::span ls_offsets, + int cut_cell_id, + int local_zero_entity_id, + const CurvingOptions& options); + +template +void ensure_all_curved(CurvingData& curving, + std::span parent_cell_ids, + std::span> adapt_cells, + std::span> level_set_cells, + std::span ls_offsets, + const CurvingOptions& options); + +} // namespace cutcells::curving diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index 15f4ae7..bddb568 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -289,7 +289,9 @@ void refresh_adapt_cell_semantics( template std::pair, BackgroundMeshData> -cut(const MeshView& mesh, const LevelSetFunction& ls) +cut(const MeshView& mesh, + const LevelSetFunction& ls, + bool triangulate_cut_parts) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -345,7 +347,7 @@ cut(const MeshView& mesh, const LevelSetFunction& ls) certify_refine_and_process_ready_cells( ac, hc.level_set_cells.back(), /*level_set_id=*/0, /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20, - /*triangulate_cut_parts=*/false); + triangulate_cut_parts); { const std::array processed_ids = {0}; const auto* processed_cell = &hc.level_set_cells.back(); @@ -380,7 +382,8 @@ cut(const MeshView& mesh, const LevelSetFunction& ls) template std::pair, BackgroundMeshData> cut(const MeshView& mesh, - const std::vector>& level_sets) + const std::vector>& level_sets, + bool triangulate_cut_parts) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -447,6 +450,7 @@ cut(const MeshView& mesh, if (dom == cell::domain::intersected) { any_intersected = true; + ls_cell.level_set_id = li; intersected_ls_indices.push_back(li); intersected_ls_cells.push_back(std::move(ls_cell)); } @@ -461,8 +465,9 @@ cut(const MeshView& mesh, all_level_set_cells.reserve(static_cast(nls)); for (int li = 0; li < nls; ++li) { - all_level_set_cells.push_back( - make_cell_level_set(level_sets[static_cast(li)], ci)); + auto ls_cell = make_cell_level_set(level_sets[static_cast(li)], ci); + ls_cell.level_set_id = li; + all_level_set_cells.push_back(std::move(ls_cell)); } const int cut_idx = hc.num_cut_cells(); @@ -473,10 +478,6 @@ cut(const MeshView& mesh, // Process intersecting level sets recursively (input order). // - // Multi-level-set cutting must leave the AdaptCell leaf mesh as - // simplexes after each cut; otherwise a later level set may be asked to - // cut quad/prism leaves, which the ready-to-cut certification path does - // not support. std::uint64_t cell_active_mask = 0; for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) { @@ -484,7 +485,7 @@ cut(const MeshView& mesh, certify_refine_and_process_ready_cells( ac, intersected_ls_cells[k], li, /*max_iterations=*/8, T(1e-12), T(1e-12), - /*edge_max_depth=*/20, /*triangulate_cut_parts=*/true); + /*edge_max_depth=*/20, triangulate_cut_parts); // New vertices created while processing level set li must be // reclassified for all already-processed level sets. @@ -511,7 +512,9 @@ cut(const MeshView& mesh, T(1e-12)); rebuild_zero_entity_inventory(ac); - // Persist per-cell LevelSetCell blocks in HOCutCells CSR storage. + // Persist only level sets actively changing sign in this parent cell. + // Non-active level sets may still touch a vertex/face, but they are not + // curving constraints for this cut-cell. for (auto& ls_cell : intersected_ls_cells) hc.level_set_cells.push_back(std::move(ls_cell)); hc.ls_offsets.push_back( @@ -641,29 +644,29 @@ HOMeshPart select_part(const HOCutCells& cut_cells, // cut() single LS template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&); +cut(const MeshView&, const LevelSetFunction&, bool); // cut() multi LS template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&); +cut(const MeshView&, const std::vector>&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&); +cut(const MeshView&, const std::vector>&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&); +cut(const MeshView&, const std::vector>&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&); +cut(const MeshView&, const std::vector>&, bool); // select_part() template HOMeshPart diff --git a/cpp/src/ho_cut_mesh.h b/cpp/src/ho_cut_mesh.h index dfee0a9..cf14297 100644 --- a/cpp/src/ho_cut_mesh.h +++ b/cpp/src/ho_cut_mesh.h @@ -13,6 +13,7 @@ #include "adapt_cell.h" #include "cell_flags.h" +#include "curving.h" #include "level_set.h" #include "level_set_cell.h" #include "mesh_view.h" @@ -88,6 +89,10 @@ struct HOCutCells /// Size = num_cut_cells. std::vector active_level_set_mask; + /// Central curved zero-entity identity/cache data. + /// Mutable because HOMeshPart is a const view but curving is a lazy cache. + mutable curving::CurvingData curving; + /// Number of intersected cells. int num_cut_cells() const { @@ -142,7 +147,8 @@ struct HOMeshPart template std::pair, BackgroundMeshData> cut(const MeshView& mesh, - const LevelSetFunction& ls); + const LevelSetFunction& ls, + bool triangulate_cut_parts = false); /// Build HOCutCells and BackgroundMeshData from a mesh and multiple level sets. /// @@ -152,7 +158,8 @@ cut(const MeshView& mesh, template std::pair, BackgroundMeshData> cut(const MeshView& mesh, - const std::vector>& level_sets); + const std::vector>& level_sets, + bool triangulate_cut_parts = true); // ===================================================================== // select_part() — builds HOMeshPart diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index c91929a..c8d8325 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -12,10 +12,18 @@ #include "quad_midpoint_split.h" #include "prism_midpoint_split.h" #include "triangulation.h" +#include "write_vtk.h" #include +#include +#include +#include +#include +#include +#include #include #include +#include #include namespace cutcells::output @@ -27,6 +35,50 @@ struct SelectedEntity { cell::type type = cell::type::point; std::vector vertices; + int zero_entity_index = -1; +}; + +constexpr int vtk_lagrange_curve = 68; +constexpr int vtk_lagrange_triangle = 69; +constexpr int vtk_lagrange_quadrilateral = 70; +constexpr int vtk_lagrange_tetrahedron = 71; +constexpr int vtk_lagrange_wedge = 73; + +template +struct CurvedBoundaryEdge +{ + std::array local_vertices = {-1, -1}; + std::array local_global_vertices = {-1, -1}; + std::array zero_vertices = {-1, -1}; + const curving::CurvedZeroEntityState* state = nullptr; +}; + +template +struct CurvedBoundaryFace +{ + std::array local_vertices = {-1, -1, -1, -1}; + int num_local_vertices = 0; + std::array zero_vertices = {-1, -1, -1, -1}; + int num_zero_vertices = 0; + cell::type zero_face_type = cell::type::triangle; + const curving::CurvedZeroEntityState* state = nullptr; +}; + +template +struct LocalCurvedSimplexMap +{ + cell::type simplex_type = cell::type::point; + int dim = 0; + int parent_tdim = 0; + int gdim = 0; + cell::type parent_cell_type = cell::type::point; + int geometry_order = 1; + curving::NodeFamily node_family = curving::NodeFamily::lagrange; + std::vector vertices; + std::vector ref_vertex_coords; + std::vector parent_physical_coords; + std::vector> curved_edges; + std::vector> curved_faces; }; inline bool is_simplex(cell::type cell_type) @@ -51,6 +103,291 @@ inline cell::type simplex_type_for_dim(int dim) } } +inline bool supports_curved_lagrange_cell(cell::type cell_type) +{ + return is_simplex(cell_type) + || cell_type == cell::type::quadrilateral + || cell_type == cell::type::prism; +} + +template +std::pair legendre_value_and_derivative(int order, T x) +{ + if (order == 0) + return {T(1), T(0)}; + + T pm2 = T(1); + T pm1 = x; + for (int n = 2; n <= order; ++n) + { + const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); + pm2 = pm1; + pm1 = p; + } + + const T denom = T(1) - x * x; + if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) + return {pm1, T(0)}; + const T derivative = T(order) * (pm2 - x * pm1) / denom; + return {pm1, derivative}; +} + +template +std::vector gll_parameters(int order) +{ + order = std::max(order, 1); + std::vector params(static_cast(order + 1), T(0)); + params.front() = T(0); + params.back() = T(1); + if (order == 1) + return params; + + const T pi = std::acos(T(-1)); + const T eps = T(128) * std::numeric_limits::epsilon(); + for (int i = 1; i < order; ++i) + { + T x = -std::cos(pi * T(i) / T(order)); + for (int iter = 0; iter < 32; ++iter) + { + const auto [p, dp] = legendre_value_and_derivative(order, x); + const T denom = T(1) - x * x; + if (std::abs(denom) <= eps) + break; + const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; + if (std::abs(d2p) <= eps) + break; + const T step = dp / d2p; + x -= step; + x = std::clamp(x, -T(1) + eps, T(1) - eps); + if (std::abs(step) <= eps) + break; + } + params[static_cast(i)] = T(0.5) * (x + T(1)); + } + return params; +} + +template +std::vector interpolation_parameters(int order, curving::NodeFamily family) +{ + order = std::max(order, 1); + std::vector params(static_cast(order + 1), T(0)); + if (family == curving::NodeFamily::gll) + return gll_parameters(order); + + for (int i = 0; i <= order; ++i) + params[static_cast(i)] = T(i) / T(order); + return params; +} + +inline curving::NodeFamily construction_node_family(int geometry_order, + curving::NodeFamily output_family) +{ + (void)geometry_order; + return output_family; +} + +template +std::vector> lagrange_simplex_nodes_basix(cell::type simplex_type, int order) +{ + order = std::max(order, 1); + std::vector> nodes; + if (simplex_type == cell::type::interval) + { + nodes.push_back({T(0)}); + nodes.push_back({T(1)}); + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order)}); + return nodes; + } + + if (simplex_type == cell::type::triangle) + { + nodes.push_back({T(0), T(0)}); + nodes.push_back({T(1), T(0)}); + nodes.push_back({T(0), T(1)}); + const auto edges = cell::edges(cell::type::triangle); + const auto verts = cell::reference_vertices(cell::type::triangle); + for (const auto& edge : edges) + { + for (int i = 1; i < order; ++i) + { + const T s = T(i) / T(order); + std::vector x(2, T(0)); + for (int d = 0; d < 2; ++d) + { + const T x0 = verts[static_cast(edge[0] * 2 + d)]; + const T x1 = verts[static_cast(edge[1] * 2 + d)]; + x[static_cast(d)] = (T(1) - s) * x0 + s * x1; + } + nodes.push_back(std::move(x)); + } + } + for (int j = 1; j < order; ++j) + for (int i = 1; i < order - j; ++i) + nodes.push_back({T(i) / T(order), T(j) / T(order)}); + return nodes; + } + + if (simplex_type == cell::type::tetrahedron) + { + nodes.push_back({T(0), T(0), T(0)}); + nodes.push_back({T(1), T(0), T(0)}); + nodes.push_back({T(0), T(1), T(0)}); + nodes.push_back({T(0), T(0), T(1)}); + const auto edges = cell::edges(cell::type::tetrahedron); + const auto verts = cell::reference_vertices(cell::type::tetrahedron); + for (const auto& edge : edges) + { + for (int i = 1; i < order; ++i) + { + const T s = T(i) / T(order); + std::vector x(3, T(0)); + for (int d = 0; d < 3; ++d) + { + const T x0 = verts[static_cast(edge[0] * 3 + d)]; + const T x1 = verts[static_cast(edge[1] * 3 + d)]; + x[static_cast(d)] = (T(1) - s) * x0 + s * x1; + } + nodes.push_back(std::move(x)); + } + } + for (int f = 0; f < 4; ++f) + { + const auto fv = cell::face_vertices(cell::type::tetrahedron, f); + for (int j = 1; j < order; ++j) + { + for (int i = 1; i < order - j; ++i) + { + const T w0 = T(1) - T(i + j) / T(order); + const T w1 = T(i) / T(order); + const T w2 = T(j) / T(order); + std::vector x(3, T(0)); + for (int d = 0; d < 3; ++d) + { + x[static_cast(d)] = + w0 * verts[static_cast(fv[0] * 3 + d)] + + w1 * verts[static_cast(fv[1] * 3 + d)] + + w2 * verts[static_cast(fv[2] * 3 + d)]; + } + nodes.push_back(std::move(x)); + } + } + } + for (int k = 1; k < order; ++k) + for (int j = 1; j < order - k; ++j) + for (int i = 1; i < order - j - k; ++i) + nodes.push_back({T(i) / T(order), T(j) / T(order), T(k) / T(order)}); + return nodes; + } + + throw std::runtime_error("lagrange_simplex_nodes_basix: unsupported cell type"); +} + +template +std::vector> lagrange_quadrilateral_nodes_vtk(int order) +{ + order = std::max(order, 1); + std::vector> nodes; + nodes.reserve(static_cast((order + 1) * (order + 1))); + + nodes.push_back({T(0), T(0)}); + nodes.push_back({T(1), T(0)}); + nodes.push_back({T(1), T(1)}); + nodes.push_back({T(0), T(1)}); + + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order), T(0)}); + for (int j = 1; j < order; ++j) + nodes.push_back({T(1), T(j) / T(order)}); + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order), T(1)}); + for (int j = 1; j < order; ++j) + nodes.push_back({T(0), T(j) / T(order)}); + + for (int j = 1; j < order; ++j) + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order), T(j) / T(order)}); + + return nodes; +} + +template +std::vector> lagrange_prism_nodes_vtk(int order) +{ + order = std::max(order, 1); + const int tri_nodes = (order + 1) * (order + 2) / 2; + std::vector> nodes; + nodes.reserve(static_cast(tri_nodes * (order + 1))); + + nodes.push_back({T(0), T(0), T(0)}); + nodes.push_back({T(1), T(0), T(0)}); + nodes.push_back({T(0), T(1), T(0)}); + nodes.push_back({T(0), T(0), T(1)}); + nodes.push_back({T(1), T(0), T(1)}); + nodes.push_back({T(0), T(1), T(1)}); + + auto append_triangle_edges = [&](T z) + { + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order), T(0), z}); + for (int r = 1; r < order; ++r) + nodes.push_back({T(order - r) / T(order), T(r) / T(order), z}); + for (int r = 1; r < order; ++r) + nodes.push_back({T(0), T(order - r) / T(order), z}); + }; + + append_triangle_edges(T(0)); + append_triangle_edges(T(1)); + + for (const auto base : {std::array{T(0), T(0)}, + std::array{T(1), T(0)}, + std::array{T(0), T(1)}}) + { + for (int k = 1; k < order; ++k) + nodes.push_back({base[0], base[1], T(k) / T(order)}); + } + + auto append_triangle_interior = [&](T z) + { + for (int j = 1; j < order; ++j) + for (int i = 1; i < order - j; ++i) + nodes.push_back({T(i) / T(order), T(j) / T(order), z}); + }; + + append_triangle_interior(T(0)); + append_triangle_interior(T(1)); + + for (int k = 1; k < order; ++k) + for (int i = 1; i < order; ++i) + nodes.push_back({T(i) / T(order), T(0), T(k) / T(order)}); + for (int k = 1; k < order; ++k) + for (int r = 1; r < order; ++r) + nodes.push_back({T(order - r) / T(order), T(r) / T(order), T(k) / T(order)}); + for (int k = 1; k < order; ++k) + for (int r = 1; r < order; ++r) + nodes.push_back({T(0), T(order - r) / T(order), T(k) / T(order)}); + + for (int k = 1; k < order; ++k) + for (int j = 1; j < order; ++j) + for (int i = 1; i < order - j; ++i) + nodes.push_back({T(i) / T(order), T(j) / T(order), T(k) / T(order)}); + + return nodes; +} + +template +std::vector> lagrange_cell_nodes(cell::type cell_type, int order) +{ + if (is_simplex(cell_type)) + return lagrange_simplex_nodes_basix(cell_type, order); + if (cell_type == cell::type::quadrilateral) + return lagrange_quadrilateral_nodes_vtk(order); + if (cell_type == cell::type::prism) + return lagrange_prism_nodes_vtk(order); + throw std::runtime_error("lagrange_cell_nodes: unsupported cell type"); +} + template std::vector parent_cell_vertex_coords_vtk(const MeshView& mesh, I cell_id) { @@ -72,6 +409,980 @@ std::vector parent_cell_vertex_coords_vtk(const MeshView& mesh, I cell_ return coords; } +template +std::vector barycentric_from_simplex_point(cell::type simplex_type, + std::span xi) +{ + if (simplex_type == cell::type::interval) + return {T(1) - xi[0], xi[0]}; + if (simplex_type == cell::type::triangle) + return {T(1) - xi[0] - xi[1], xi[0], xi[1]}; + if (simplex_type == cell::type::tetrahedron) + return {T(1) - xi[0] - xi[1] - xi[2], xi[0], xi[1], xi[2]}; + throw std::runtime_error("barycentric_from_simplex_point: unsupported simplex"); +} + +template +std::vector cell_vertex_shape_weights(cell::type cell_type, + std::span xi) +{ + if (cell_type == cell::type::interval) + return {T(1) - xi[0], xi[0]}; + if (cell_type == cell::type::triangle) + return {T(1) - xi[0] - xi[1], xi[0], xi[1]}; + if (cell_type == cell::type::quadrilateral) + { + const T u = xi[0]; + const T v = xi[1]; + return {(T(1) - u) * (T(1) - v), + u * (T(1) - v), + (T(1) - u) * v, + u * v}; + } + if (cell_type == cell::type::tetrahedron) + return {T(1) - xi[0] - xi[1] - xi[2], xi[0], xi[1], xi[2]}; + if (cell_type == cell::type::prism) + { + const T u = xi[0]; + const T v = xi[1]; + const T z = xi[2]; + const T w0 = T(1) - u - v; + return {w0 * (T(1) - z), + u * (T(1) - z), + v * (T(1) - z), + w0 * z, + u * z, + v * z}; + } + throw std::runtime_error("cell_vertex_shape_weights: unsupported cell type"); +} + +template +std::vector affine_ref_from_bary(const LocalCurvedSimplexMap& map, + std::span bary) +{ + std::vector x(static_cast(map.parent_tdim), T(0)); + for (std::size_t v = 0; v < bary.size(); ++v) + { + for (int d = 0; d < map.parent_tdim; ++d) + { + x[static_cast(d)] += bary[v] * map.ref_vertex_coords[ + v * static_cast(map.parent_tdim) + static_cast(d)]; + } + } + return x; +} + +template +std::vector straight_ref_from_cell_point(const LocalCurvedSimplexMap& map, + std::span xi) +{ + const auto weights = cell_vertex_shape_weights(map.simplex_type, xi); + std::vector x(static_cast(map.parent_tdim), T(0)); + for (std::size_t v = 0; v < weights.size(); ++v) + { + for (int d = 0; d < map.parent_tdim; ++d) + { + x[static_cast(d)] += weights[v] * map.ref_vertex_coords[ + v * static_cast(map.parent_tdim) + static_cast(d)]; + } + } + return x; +} + +template +std::vector push_parent_ref_to_physical(const LocalCurvedSimplexMap& map, + std::span ref_point) +{ + const auto phys = cell::push_forward_affine_map( + map.parent_cell_type, + map.parent_physical_coords, + map.gdim, + ref_point); + return phys; +} + +inline bool same_unordered(std::span a, std::span b) +{ + if (a.size() != b.size()) + return false; + for (const int av : a) + { + bool found = false; + for (const int bv : b) + found = found || (av == bv); + if (!found) + return false; + } + return true; +} + +template +std::vector zero_entity_vertex_ids(const AdaptCell& ac, int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim == 0) + return {zid}; + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + std::vector out; + out.reserve(verts.size()); + for (const auto v : verts) + out.push_back(static_cast(v)); + return out; +} + +template +T lagrange_basis_1d(int i, std::span params, T x) +{ + T value = T(1); + const T xi = params[static_cast(i)]; + for (int j = 0; j < static_cast(params.size()); ++j) + { + if (j == i) + continue; + value *= (x - params[static_cast(j)]) + / (xi - params[static_cast(j)]); + } + return value; +} + +template +T warp_factor(int order, T r) +{ + if (order <= 1) + return T(0); + + std::vector equispaced(static_cast(order + 1), T(0)); + std::vector gll = gll_parameters(order); + for (int i = 0; i <= order; ++i) + { + equispaced[static_cast(i)] = -T(1) + T(2 * i) / T(order); + gll[static_cast(i)] = T(2) * gll[static_cast(i)] - T(1); + } + + T warp = T(0); + for (int i = 0; i <= order; ++i) + { + const T Li = lagrange_basis_1d( + i, std::span(equispaced.data(), equispaced.size()), r); + warp += Li * (gll[static_cast(i)] + - equispaced[static_cast(i)]); + } + + const T edge_factor = T(1) - r * r; + if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) + warp /= edge_factor; + return warp; +} + +template +std::array equilateral_to_reference_barycentric(T x, T y) +{ + const T sqrt3 = std::sqrt(T(3)); + std::array w = {}; + w[2] = (sqrt3 * y + T(1)) / T(3); + w[1] = (x + T(1) - w[2]) / T(2); + w[0] = T(1) - w[1] - w[2]; + + T sum = T(0); + for (T& wi : w) + { + if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) + wi = T(0); + if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) + wi = T(1); + wi = std::clamp(wi, T(0), T(1)); + sum += wi; + } + if (sum > T(0)) + for (T& wi : w) + wi /= sum; + return w; +} + +template +std::vector> triangle_interpolation_barycentric_nodes( + int order, + curving::NodeFamily family) +{ + order = std::max(order, 1); + std::vector> nodes; + nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); + + if (family != curving::NodeFamily::gll) + { + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + nodes.push_back({T(1) - u - v, u, v}); + } + } + return nodes; + } + + constexpr std::array alpha_opt = { + T(0.0), T(0.0), T(1.4152), T(0.1001), + T(0.2751), T(0.9800), T(1.0999), T(1.2832), + T(1.3648), T(1.4773), T(1.4959), T(1.5743), + T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; + const T alpha = (order < static_cast(alpha_opt.size())) + ? alpha_opt[static_cast(order)] + : T(5) / T(3); + const T sqrt3 = std::sqrt(T(3)); + const T cos120 = -T(0.5); + const T sin120 = sqrt3 / T(2); + const T cos240 = -T(0.5); + const T sin240 = -sqrt3 / T(2); + + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + const T lambda0 = T(1) - u - v; + const T lambda1 = u; + const T lambda2 = v; + + T x = -lambda0 + lambda1; + T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; + + const T L1 = lambda2; + const T L2 = lambda0; + const T L3 = lambda1; + const T warp1 = T(4) * L2 * L3 * warp_factor(order, L3 - L2) + * (T(1) + (alpha * L1) * (alpha * L1)); + const T warp2 = T(4) * L1 * L3 * warp_factor(order, L1 - L3) + * (T(1) + (alpha * L2) * (alpha * L2)); + const T warp3 = T(4) * L1 * L2 * warp_factor(order, L2 - L1) + * (T(1) + (alpha * L3) * (alpha * L3)); + + x += warp1 + cos120 * warp2 + cos240 * warp3; + y += sin120 * warp2 + sin240 * warp3; + nodes.push_back(equilateral_to_reference_barycentric(x, y)); + } + } + return nodes; +} + +template +std::vector triangle_monomials(int order, std::span bary) +{ + const T u = bary[1]; + const T v = bary[2]; + std::vector values; + values.reserve(static_cast((order + 1) * (order + 2) / 2)); + for (int total = 0; total <= order; ++total) + { + for (int j = 0; j <= total; ++j) + { + const int i = total - j; + values.push_back(std::pow(u, i) * std::pow(v, j)); + } + } + return values; +} + +template +bool solve_dense(std::vector A, std::vector b, int n, std::vector& x) +{ + x.assign(static_cast(n), T(0)); + for (int k = 0; k < n; ++k) + { + int pivot = k; + T best = std::abs(A[static_cast(k * n + k)]); + for (int r = k + 1; r < n; ++r) + { + const T value = std::abs(A[static_cast(r * n + k)]); + if (value > best) + { + best = value; + pivot = r; + } + } + if (best <= T(256) * std::numeric_limits::epsilon()) + return false; + if (pivot != k) + { + for (int c = k; c < n; ++c) + std::swap(A[static_cast(k * n + c)], + A[static_cast(pivot * n + c)]); + std::swap(b[static_cast(k)], b[static_cast(pivot)]); + } + + const T diag = A[static_cast(k * n + k)]; + for (int c = k; c < n; ++c) + A[static_cast(k * n + c)] /= diag; + b[static_cast(k)] /= diag; + + for (int r = 0; r < n; ++r) + { + if (r == k) + continue; + const T factor = A[static_cast(r * n + k)]; + if (factor == T(0)) + continue; + for (int c = k; c < n; ++c) + A[static_cast(r * n + c)] -= + factor * A[static_cast(k * n + c)]; + b[static_cast(r)] -= factor * b[static_cast(k)]; + } + } + x = std::move(b); + return true; +} + +template +std::vector triangle_lagrange_basis(int order, + curving::NodeFamily node_family, + std::span bary) +{ + const auto nodes = + triangle_interpolation_barycentric_nodes(order, node_family); + const int n = static_cast(nodes.size()); + std::vector matrix(static_cast(n * n), T(0)); + for (int row = 0; row < n; ++row) + { + const auto mono = triangle_monomials( + order, + std::span(nodes[static_cast(row)].data(), 3)); + for (int col = 0; col < n; ++col) + matrix[static_cast(col * n + row)] = + mono[static_cast(col)]; + } + + const auto rhs = triangle_monomials(order, bary); + std::vector basis; + if (!solve_dense(std::move(matrix), rhs, n, basis)) + throw std::runtime_error("triangle_lagrange_basis: singular interpolation matrix"); + return basis; +} + +template +T simplex_lagrange_factor(int alpha, T lambda, int order) +{ + T value = T(1); + for (int r = 0; r < alpha; ++r) + value *= (T(order) * lambda - T(r)) / T(r + 1); + return value; +} + +template +std::vector eval_curved_edge_ref(const CurvedBoundaryEdge& edge, + int order, + curving::NodeFamily family, + T t_local) +{ + const bool same_orientation = + edge.zero_vertices[0] == edge.local_global_vertices[0] + && edge.zero_vertices[1] == edge.local_global_vertices[1]; + const bool reverse_orientation = + edge.zero_vertices[0] == edge.local_global_vertices[1] + && edge.zero_vertices[1] == edge.local_global_vertices[0]; + if (!same_orientation && !reverse_orientation) + throw std::runtime_error("eval_curved_edge_ref: edge orientation mismatch"); + const T t = same_orientation ? t_local : T(1) - t_local; + const auto params = interpolation_parameters(order, family); + const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); + std::vector x(static_cast(tdim), T(0)); + for (int i = 0; i <= order; ++i) + { + const T Li = lagrange_basis_1d(i, std::span(params.data(), params.size()), t); + for (int d = 0; d < tdim; ++d) + x[static_cast(d)] += Li * edge.state->ref_nodes[ + static_cast(i * tdim + d)]; + } + return x; +} + +template +std::vector eval_curved_face_ref(const CurvedBoundaryFace& face, + int order, + curving::NodeFamily node_family, + std::span coordinates) +{ + const int nodes_per_face = + (face.zero_face_type == cell::type::quadrilateral) + ? (order + 1) * (order + 1) + : (order + 1) * (order + 2) / 2; + const int tdim = static_cast(face.state->ref_nodes.size()) / nodes_per_face; + std::vector x(static_cast(tdim), T(0)); + + if (face.zero_face_type == cell::type::quadrilateral) + { + const T u = coordinates[0]; + const T v = coordinates[1]; + const auto params = interpolation_parameters(order, node_family); + int node = 0; + for (int j = 0; j <= order; ++j) + { + const T Lj = lagrange_basis_1d( + j, std::span(params.data(), params.size()), v); + for (int i = 0; i <= order; ++i) + { + const T Li = lagrange_basis_1d( + i, std::span(params.data(), params.size()), u); + const T L = Li * Lj; + for (int d = 0; d < tdim; ++d) + x[static_cast(d)] += L * face.state->ref_nodes[ + static_cast(node * tdim + d)]; + ++node; + } + } + return x; + } + + const auto basis = triangle_lagrange_basis(order, node_family, coordinates); + for (int node = 0; node < static_cast(basis.size()); ++node) + { + const T L = basis[static_cast(node)]; + for (int d = 0; d < tdim; ++d) + x[static_cast(d)] += L * face.state->ref_nodes[ + static_cast(node * tdim + d)]; + } + return x; +} + +template +bool edge_is_in_any_curved_face(const CurvedBoundaryEdge& edge, + std::span> faces) +{ + for (const auto& face : faces) + { + bool has0 = false; + bool has1 = false; + for (int i = 0; i < face.num_local_vertices; ++i) + { + const int fv = face.local_vertices[static_cast(i)]; + has0 = has0 || (fv == edge.local_vertices[0]); + has1 = has1 || (fv == edge.local_vertices[1]); + } + if (has0 && has1) + return true; + } + return false; +} + +template +std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, + std::span xi) +{ + const auto vertex_weights = cell_vertex_shape_weights(map.simplex_type, xi); + std::vector ref = straight_ref_from_cell_point(map, xi); + constexpr T eps = T(64) * std::numeric_limits::epsilon(); + + if (map.dim == 1) + { + for (const auto& edge : map.curved_edges) + { + const auto curved = eval_curved_edge_ref( + edge, map.geometry_order, map.node_family, xi[0]); + for (int d = 0; d < map.parent_tdim; ++d) + ref[static_cast(d)] = curved[static_cast(d)]; + } + return ref; + } + + for (const auto& face : map.curved_faces) + { + T s = T(0); + std::array local_w = {}; + for (int i = 0; i < face.num_local_vertices; ++i) + { + local_w[static_cast(i)] = + vertex_weights[static_cast( + face.local_vertices[static_cast(i)])]; + s += local_w[static_cast(i)]; + } + if (s <= eps) + continue; + + std::array zero_w = {}; + std::array zero_uv = {}; + for (int zi = 0; zi < face.num_zero_vertices; ++zi) + { + for (int li = 0; li < face.num_local_vertices; ++li) + { + if (face.zero_vertices[static_cast(zi)] + == map.vertices[static_cast( + face.local_vertices[static_cast(li)])]) + { + const T w = local_w[static_cast(li)] / s; + if (face.zero_face_type == cell::type::quadrilateral) + { + const T u = (zi == 1 || zi == 3) ? T(1) : T(0); + const T v = (zi == 2 || zi == 3) ? T(1) : T(0); + zero_uv[0] += w * u; + zero_uv[1] += w * v; + } + else + { + zero_w[static_cast(zi)] = w; + } + } + } + } + + std::span face_coordinates = + (face.zero_face_type == cell::type::quadrilateral) + ? std::span(zero_uv.data(), zero_uv.size()) + : std::span(zero_w.data(), zero_w.size()); + const auto curved = eval_curved_face_ref( + face, map.geometry_order, map.node_family, face_coordinates); + std::vector straight(static_cast(map.parent_tdim), T(0)); + for (int li = 0; li < face.num_local_vertices; ++li) + { + const int lv = face.local_vertices[static_cast(li)]; + const T w = local_w[static_cast(li)] / s; + for (int d = 0; d < map.parent_tdim; ++d) + straight[static_cast(d)] += w * map.ref_vertex_coords[ + static_cast(lv * map.parent_tdim + d)]; + } + for (int d = 0; d < map.parent_tdim; ++d) + ref[static_cast(d)] += s * (curved[static_cast(d)] + - straight[static_cast(d)]); + } + + for (const auto& edge : map.curved_edges) + { + if (edge_is_in_any_curved_face( + edge, std::span>(map.curved_faces.data(), map.curved_faces.size()))) + { + continue; + } + const T a = vertex_weights[static_cast(edge.local_vertices[0])]; + const T b = vertex_weights[static_cast(edge.local_vertices[1])]; + const T s = a + b; + if (s <= eps) + continue; + const T t = b / s; + const auto curved = eval_curved_edge_ref( + edge, map.geometry_order, map.node_family, t); + std::vector straight(static_cast(map.parent_tdim), T(0)); + for (int d = 0; d < map.parent_tdim; ++d) + { + const T x0 = map.ref_vertex_coords[ + static_cast(edge.local_vertices[0] * map.parent_tdim + d)]; + const T x1 = map.ref_vertex_coords[ + static_cast(edge.local_vertices[1] * map.parent_tdim + d)]; + straight[static_cast(d)] = (T(1) - t) * x0 + t * x1; + ref[static_cast(d)] += s * (curved[static_cast(d)] + - straight[static_cast(d)]); + } + } + + return ref; +} + +template +std::vector curved_map_physical_point(const LocalCurvedSimplexMap& map, + std::span xi) +{ + const auto ref = curved_map_ref_point(map, xi); + return push_parent_ref_to_physical(map, std::span(ref.data(), ref.size())); +} + +template +std::vector canonical_simplex_vertices(cell::type simplex_type) +{ + return cell::reference_vertices(simplex_type); +} + +template +std::vector map_child_xi_to_parent_xi(cell::type simplex_type, + std::span child_vertices, + std::span xi) +{ + const int dim = cell::get_tdim(simplex_type); + const auto weights = cell_vertex_shape_weights(simplex_type, xi); + std::vector out(static_cast(dim), T(0)); + for (std::size_t v = 0; v < weights.size(); ++v) + { + for (int d = 0; d < dim; ++d) + { + out[static_cast(d)] += weights[v] * child_vertices[ + v * static_cast(dim) + static_cast(d)]; + } + } + return out; +} + +template +std::vector child_point(cell::type cell_type, + std::span child_vertices, + std::initializer_list xi) +{ + std::vector coords(xi); + return map_child_xi_to_parent_xi( + cell_type, + child_vertices, + std::span(coords.data(), coords.size())); +} + +template +std::vector> subdivide_child_simplex(cell::type simplex_type, + std::span vertices) +{ + const int dim = cell::get_tdim(simplex_type); + auto vertex = [&](int i) + { + return std::vector( + vertices.begin() + static_cast(i * dim), + vertices.begin() + static_cast((i + 1) * dim)); + }; + auto midpoint = [&](const std::vector& a, const std::vector& b) + { + std::vector m(static_cast(dim), T(0)); + for (int d = 0; d < dim; ++d) + m[static_cast(d)] = T(0.5) * (a[static_cast(d)] + + b[static_cast(d)]); + return m; + }; + auto pack = [&](std::initializer_list> pts) + { + std::vector child; + child.reserve(pts.size() * static_cast(dim)); + for (const auto& p : pts) + child.insert(child.end(), p.begin(), p.end()); + return child; + }; + + if (simplex_type == cell::type::interval) + { + const auto v0 = vertex(0); + const auto v1 = vertex(1); + const auto m01 = midpoint(v0, v1); + return {pack({v0, m01}), pack({m01, v1})}; + } + + if (simplex_type == cell::type::triangle) + { + const auto v0 = vertex(0); + const auto v1 = vertex(1); + const auto v2 = vertex(2); + const auto m01 = midpoint(v0, v1); + const auto m12 = midpoint(v1, v2); + const auto m20 = midpoint(v2, v0); + return { + pack({v0, m01, m20}), + pack({m01, v1, m12}), + pack({m20, m12, v2}), + pack({m01, m12, m20}) + }; + } + + if (simplex_type == cell::type::tetrahedron) + { + const auto v0 = vertex(0); + const auto v1 = vertex(1); + const auto v2 = vertex(2); + const auto v3 = vertex(3); + const auto m01 = midpoint(v0, v1); + const auto m02 = midpoint(v0, v2); + const auto m03 = midpoint(v0, v3); + const auto m12 = midpoint(v1, v2); + const auto m13 = midpoint(v1, v3); + const auto m23 = midpoint(v2, v3); + return { + pack({v0, m01, m02, m03}), + pack({m01, v1, m12, m13}), + pack({m02, m12, v2, m23}), + pack({m03, m13, m23, v3}), + pack({m01, m02, m03, m13}), + pack({m01, m02, m12, m13}), + pack({m02, m12, m13, m23}), + pack({m02, m03, m13, m23}) + }; + } + + if (simplex_type == cell::type::quadrilateral) + { + return { + pack({child_point(simplex_type, vertices, {T(0), T(0)}), + child_point(simplex_type, vertices, {T(0.5), T(0)}), + child_point(simplex_type, vertices, {T(0), T(0.5)}), + child_point(simplex_type, vertices, {T(0.5), T(0.5)})}), + pack({child_point(simplex_type, vertices, {T(0.5), T(0)}), + child_point(simplex_type, vertices, {T(1), T(0)}), + child_point(simplex_type, vertices, {T(0.5), T(0.5)}), + child_point(simplex_type, vertices, {T(1), T(0.5)})}), + pack({child_point(simplex_type, vertices, {T(0), T(0.5)}), + child_point(simplex_type, vertices, {T(0.5), T(0.5)}), + child_point(simplex_type, vertices, {T(0), T(1)}), + child_point(simplex_type, vertices, {T(0.5), T(1)})}), + pack({child_point(simplex_type, vertices, {T(0.5), T(0.5)}), + child_point(simplex_type, vertices, {T(1), T(0.5)}), + child_point(simplex_type, vertices, {T(0.5), T(1)}), + child_point(simplex_type, vertices, {T(1), T(1)})}) + }; + } + + if (simplex_type == cell::type::prism) + { + const std::array, 6> tri = {{ + {T(0), T(0)}, {T(1), T(0)}, {T(0), T(1)}, + {T(0.5), T(0)}, {T(0.5), T(0.5)}, {T(0), T(0.5)} + }}; + const std::array, 4> sub_tri = {{ + {0, 3, 5}, + {3, 1, 4}, + {5, 4, 2}, + {3, 4, 5} + }}; + std::vector> children; + children.reserve(8); + for (int slab = 0; slab < 2; ++slab) + { + const T z0 = T(slab) * T(0.5); + const T z1 = T(slab + 1) * T(0.5); + for (const auto& st : sub_tri) + { + children.push_back(pack({ + child_point(simplex_type, vertices, {tri[st[0]][0], tri[st[0]][1], z0}), + child_point(simplex_type, vertices, {tri[st[1]][0], tri[st[1]][1], z0}), + child_point(simplex_type, vertices, {tri[st[2]][0], tri[st[2]][1], z0}), + child_point(simplex_type, vertices, {tri[st[0]][0], tri[st[0]][1], z1}), + child_point(simplex_type, vertices, {tri[st[1]][0], tri[st[1]][1], z1}), + child_point(simplex_type, vertices, {tri[st[2]][0], tri[st[2]][1], z1}) + })); + } + } + return children; + } + + throw std::runtime_error("subdivide_child_simplex: unsupported simplex type"); +} + +template +T finite_difference_measure(const LocalCurvedSimplexMap& map, + std::span xi) +{ + const int d = map.dim; + constexpr T h = T(1e-6); + std::vector J(static_cast(map.gdim * d), T(0)); + for (int c = 0; c < d; ++c) + { + std::vector xp(xi.begin(), xi.end()); + std::vector xm(xi.begin(), xi.end()); + xp[static_cast(c)] += h; + xm[static_cast(c)] -= h; + const auto pp = curved_map_physical_point(map, std::span(xp.data(), xp.size())); + const auto pm = curved_map_physical_point(map, std::span(xm.data(), xm.size())); + for (int r = 0; r < map.gdim; ++r) + J[static_cast(c * map.gdim + r)] = + (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); + } + + if (d == 1) + { + T n2 = T(0); + for (int r = 0; r < map.gdim; ++r) + n2 += J[static_cast(r)] * J[static_cast(r)]; + return std::sqrt(n2); + } + if (d == map.gdim) + { + if (d == 2) + return J[0] * J[3] - J[2] * J[1]; + return J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + } + + T G[9] = {}; + for (int i = 0; i < d; ++i) + for (int j = 0; j < d; ++j) + for (int r = 0; r < map.gdim; ++r) + G[i * d + j] += J[static_cast(i * map.gdim + r)] + * J[static_cast(j * map.gdim + r)]; + if (d == 2) + return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); + return T(0); +} + +template +T finite_difference_straight_measure(const LocalCurvedSimplexMap& map, + std::span xi) +{ + const int d = map.dim; + constexpr T h = T(1e-6); + std::vector J(static_cast(map.gdim * d), T(0)); + auto straight_physical = [&](std::span xref) + { + const auto ref = straight_ref_from_cell_point(map, xref); + return push_parent_ref_to_physical( + map, std::span(ref.data(), ref.size())); + }; + + for (int c = 0; c < d; ++c) + { + std::vector xp(xi.begin(), xi.end()); + std::vector xm(xi.begin(), xi.end()); + xp[static_cast(c)] += h; + xm[static_cast(c)] -= h; + const auto pp = straight_physical(std::span(xp.data(), xp.size())); + const auto pm = straight_physical(std::span(xm.data(), xm.size())); + for (int r = 0; r < map.gdim; ++r) + J[static_cast(c * map.gdim + r)] = + (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); + } + + if (d == 1) + { + T n2 = T(0); + for (int r = 0; r < map.gdim; ++r) + n2 += J[static_cast(r)] * J[static_cast(r)]; + return std::sqrt(n2); + } + if (d == map.gdim) + { + if (d == 2) + return std::abs(J[0] * J[3] - J[2] * J[1]); + return std::abs(J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2])); + } + + T G[9] = {}; + for (int i = 0; i < d; ++i) + for (int j = 0; j < d; ++j) + for (int r = 0; r < map.gdim; ++r) + G[i * d + j] += J[static_cast(i * map.gdim + r)] + * J[static_cast(j * map.gdim + r)]; + if (d == 2) + return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); + return T(0); +} + +template +T finite_difference_child_measure(cell::type cell_type, + std::span child_vertices, + std::span xi) +{ + const int d = cell::get_tdim(cell_type); + constexpr T h = T(1e-6); + std::vector J(static_cast(d * d), T(0)); + for (int c = 0; c < d; ++c) + { + std::vector xp(xi.begin(), xi.end()); + std::vector xm(xi.begin(), xi.end()); + xp[static_cast(c)] += h; + xm[static_cast(c)] -= h; + const auto pp = map_child_xi_to_parent_xi( + cell_type, child_vertices, std::span(xp.data(), xp.size())); + const auto pm = map_child_xi_to_parent_xi( + cell_type, child_vertices, std::span(xm.data(), xm.size())); + for (int r = 0; r < d; ++r) + J[static_cast(c * d + r)] = + (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); + } + + if (d == 1) + return std::abs(J[0]); + if (d == 2) + return std::abs(J[0] * J[3] - J[2] * J[1]); + return std::abs(J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2])); +} + +template +bool curved_map_valid(const LocalCurvedSimplexMap& map) +{ + std::vector> samples; + if (map.simplex_type == cell::type::interval) + samples = {{T(0.25)}, {T(0.5)}, {T(0.75)}}; + else if (map.simplex_type == cell::type::triangle) + samples = {{T(1) / T(3), T(1) / T(3)}, + {T(0.5), T(0.25)}, + {T(0.25), T(0.5)}, + {T(0.25), T(0.25)}}; + else if (map.simplex_type == cell::type::quadrilateral) + samples = {{T(0.5), T(0.5)}, + {T(0.25), T(0.25)}, + {T(0.75), T(0.25)}, + {T(0.25), T(0.75)}, + {T(0.75), T(0.75)}}; + else if (map.simplex_type == cell::type::tetrahedron) + samples = {{T(0.25), T(0.25), T(0.25)}, + {T(0.5), T(1) / T(6), T(1) / T(6)}, + {T(1) / T(6), T(0.5), T(1) / T(6)}, + {T(1) / T(6), T(1) / T(6), T(0.5)}}; + else if (map.simplex_type == cell::type::prism) + samples = {{T(1) / T(3), T(1) / T(3), T(0.5)}, + {T(0.2), T(0.2), T(0.25)}, + {T(0.6), T(0.2), T(0.25)}, + {T(0.2), T(0.6), T(0.75)}, + {T(0.2), T(0.2), T(0.75)}}; + else + return true; + + constexpr T tol = T(1e-12); + for (const auto& sample : samples) + { + const T measure = finite_difference_measure( + map, std::span(sample.data(), sample.size())); + if (!(std::abs(measure) > tol)) + return false; + } + return true; +} + +template +bool curved_child_map_valid(const LocalCurvedSimplexMap& map, + std::span child_vertices) +{ + if (map.curved_edges.empty() && map.curved_faces.empty()) + return true; + + std::vector> samples; + if (map.simplex_type == cell::type::interval) + samples = {{T(0.25)}, {T(0.5)}, {T(0.75)}}; + else if (map.simplex_type == cell::type::triangle) + samples = {{T(1) / T(3), T(1) / T(3)}, + {T(0.5), T(0.25)}, + {T(0.25), T(0.5)}, + {T(0.25), T(0.25)}}; + else if (map.simplex_type == cell::type::quadrilateral) + samples = {{T(0.5), T(0.5)}, + {T(0.25), T(0.25)}, + {T(0.75), T(0.25)}, + {T(0.25), T(0.75)}, + {T(0.75), T(0.75)}}; + else if (map.simplex_type == cell::type::tetrahedron) + samples = {{T(0.25), T(0.25), T(0.25)}, + {T(0.5), T(1) / T(6), T(1) / T(6)}, + {T(1) / T(6), T(0.5), T(1) / T(6)}, + {T(1) / T(6), T(1) / T(6), T(0.5)}}; + else if (map.simplex_type == cell::type::prism) + samples = {{T(1) / T(3), T(1) / T(3), T(0.5)}, + {T(0.2), T(0.2), T(0.25)}, + {T(0.6), T(0.2), T(0.25)}, + {T(0.2), T(0.6), T(0.75)}, + {T(0.2), T(0.2), T(0.75)}}; + else + return true; + + constexpr T tol = T(1e-12); + for (const auto& sample : samples) + { + const auto xi_parent = map_child_xi_to_parent_xi( + map.simplex_type, + child_vertices, + std::span(sample.data(), sample.size())); + const T measure = finite_difference_measure( + map, std::span(xi_parent.data(), xi_parent.size())); + if (!(std::abs(measure) > tol)) + return false; + } + return true; +} + + template bool vertex_is_zero_for_level_set(const AdaptCell& adapt_cell, int vertex_id, @@ -85,37 +1396,231 @@ template bool cell_contains_all_vertices(std::span cell_verts, std::span entity_verts) { - for (const int v : entity_verts) + for (const int v : entity_verts) + { + bool found = false; + for (const auto cv : cell_verts) + { + if (cv == v) + { + found = true; + break; + } + } + if (!found) + return false; + } + return true; +} + +template +void vertex_state_for_level_set(const AdaptCell& ac, + int vertex_id, + int level_set_id, + bool& is_negative, + bool& is_positive, + bool& is_zero) +{ + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; + const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; + is_zero = (zm & bit) != 0; + is_negative = !is_zero && ((nm & bit) != 0); + is_positive = !is_zero && !is_negative; +} + +template +const curving::CurvedZeroEntityState* accepted_curved_state( + const HOMeshPart& part, + int cut_cell_id, + int local_zero_entity_id, + const curving::CurvingOptions& options) +{ + const auto& state = curving::ensure_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + cut_cell_id, + local_zero_entity_id, + options); + if (state.status != curving::CurvingStatus::curved) + return nullptr; + return &state; +} + +template +bool zero_edge_is_same_mask_boundary_of_zero_face(const AdaptCell& ac, + const LocalCurvedSimplexMap& map, + int zero_edge_id, + std::span edge_vertices) +{ + const auto edge_mask = ac.zero_entity_zero_mask[static_cast(zero_edge_id)]; + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (ac.zero_entity_dim[static_cast(z)] != 2) + continue; + if (ac.zero_entity_zero_mask[static_cast(z)] != edge_mask) + continue; + + const auto face_vertices = zero_entity_vertex_ids(ac, z); + bool edge_in_face = true; + for (const int ev : edge_vertices) + { + bool found = false; + for (const int fv : face_vertices) + found = found || (ev == fv); + edge_in_face = edge_in_face && found; + } + if (!edge_in_face) + continue; + + bool face_in_map = true; + for (const int fv : face_vertices) + { + bool found = false; + for (const int mv : map.vertices) + found = found || (fv == mv); + face_in_map = face_in_map && found; + } + if (face_in_map) + return true; + } + return false; +} + +template +void attach_curved_boundaries(LocalCurvedSimplexMap& map, + const HOMeshPart& part, + const AdaptCell& ac, + int cut_cell_id, + const curving::CurvingOptions& options, + int required_zero_entity_index = -1) +{ + const int nzero = ac.n_zero_entities(); + for (int z = 0; z < nzero; ++z) { - bool found = false; - for (const auto cv : cell_verts) + if (required_zero_entity_index >= 0 && z != required_zero_entity_index) + continue; + + const int zdim = ac.zero_entity_dim[static_cast(z)]; + if (zdim != 1 && zdim != 2) + continue; + + const auto zverts = zero_entity_vertex_ids(ac, z); + + if (zdim == 1) { - if (cv == v) + if (map.parent_tdim == 3 && map.dim >= 2 + && zero_edge_is_same_mask_boundary_of_zero_face( + ac, map, z, std::span(zverts.data(), zverts.size()))) { - found = true; - break; + continue; + } + + for (const auto& edge : cell::edges(map.simplex_type)) + { + std::array local_pair = { + map.vertices[static_cast(edge[0])], + map.vertices[static_cast(edge[1])] + }; + if (!same_unordered( + std::span(local_pair.data(), local_pair.size()), + std::span(zverts.data(), zverts.size()))) + { + continue; + } + + const auto* state = accepted_curved_state(part, cut_cell_id, z, options); + if (state == nullptr) + continue; + + CurvedBoundaryEdge ce; + ce.local_vertices = edge; + ce.local_global_vertices = local_pair; + ce.zero_vertices = {zverts[0], zverts[1]}; + ce.state = state; + map.curved_edges.push_back(ce); } } - if (!found) - return false; - } - return true; -} + else if (zdim == 2 + && (map.simplex_type == cell::type::tetrahedron + || map.simplex_type == cell::type::triangle + || map.simplex_type == cell::type::quadrilateral + || map.simplex_type == cell::type::prism) + && (zverts.size() == 3 || zverts.size() == 4)) + { + auto local_face_is_on_zero_face = [&](std::span local_face) + { + for (const int v : local_face) + { + bool found = false; + for (const int zv : zverts) + found = found || (v == zv); + if (!found) + return false; + } + return true; + }; -template -void vertex_state_for_level_set(const AdaptCell& ac, - int vertex_id, - int level_set_id, - bool& is_negative, - bool& is_positive, - bool& is_zero) -{ - const std::uint64_t bit = std::uint64_t(1) << level_set_id; - const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; - const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; - is_zero = (zm & bit) != 0; - is_negative = !is_zero && ((nm & bit) != 0); - is_positive = !is_zero && !is_negative; + auto append_face = [&](std::array local_ids, int nlocal) + { + std::array local_face = {-1, -1, -1, -1}; + for (int i = 0; i < nlocal; ++i) + local_face[static_cast(i)] = + map.vertices[static_cast(local_ids[static_cast(i)])]; + if (!local_face_is_on_zero_face( + std::span(local_face.data(), static_cast(nlocal)))) + { + return; + } + + const auto* state = accepted_curved_state(part, cut_cell_id, z, options); + if (state == nullptr) + return; + + CurvedBoundaryFace cf; + cf.local_vertices = local_ids; + cf.num_local_vertices = nlocal; + cf.num_zero_vertices = static_cast(zverts.size()); + cf.zero_face_type = ac.entity_types[2][ + static_cast(ac.zero_entity_id[static_cast(z)])]; + for (std::size_t i = 0; i < zverts.size(); ++i) + cf.zero_vertices[i] = zverts[i]; + cf.state = state; + map.curved_faces.push_back(cf); + }; + + if (map.simplex_type == cell::type::triangle) + { + append_face({0, 1, 2, -1}, 3); + } + else if (map.simplex_type == cell::type::quadrilateral) + { + append_face({0, 1, 2, 3}, 4); + } + else if (map.simplex_type == cell::type::prism) + { + for (int f = 0; f < cell::num_faces(cell::type::prism); ++f) + { + const auto fv = cell::face_vertices(cell::type::prism, f); + std::array local_ids = {-1, -1, -1, -1}; + for (std::size_t i = 0; i < fv.size(); ++i) + local_ids[i] = fv[i]; + append_face(local_ids, static_cast(fv.size())); + } + } + else + { + for (int f = 0; f < cell::num_faces(cell::type::tetrahedron); ++f) + { + const auto fv = cell::face_vertices(cell::type::tetrahedron, f); + append_face({fv[0], fv[1], fv[2], -1}, 3); + } + } + } + } } template @@ -272,6 +1777,7 @@ std::vector selected_entities(const HOMeshPart& part, entity.type = (zdim == 0) ? cell::type::point : adapt_cell.entity_types[zdim][static_cast(zid)]; + entity.zero_entity_index = z; if (zdim == 0) { entity.vertices.push_back(zid); @@ -866,11 +2372,455 @@ void append_entity_quadrature(quadrature::QuadratureRules& rules, rules._offset.push_back(static_cast(rules._weights.size())); } +template +void append_lagrange_child_simplex(CurvedVTUGrid& out, + const LocalCurvedSimplexMap& map, + int parent_cell_id, + int curving_status, + std::span child_vertices, + int subdivision_depth, + int max_subdivision_depth) +{ + const int order = std::max(map.geometry_order, 1); + const bool curving_failed = + curving_status == static_cast(curving::CurvingStatus::failed); + const bool map_valid = curved_child_map_valid(map, child_vertices); + const bool valid = !curving_failed && map_valid; + if (!map_valid && !curving_failed && subdivision_depth < max_subdivision_depth) + { + const auto children = subdivide_child_simplex(map.simplex_type, child_vertices); + for (const auto& child : children) + { + append_lagrange_child_simplex( + out, + map, + parent_cell_id, + curving_status, + std::span(child.data(), child.size()), + subdivision_depth + 1, + max_subdivision_depth); + } + return; + } + + const auto local_nodes = lagrange_cell_nodes(map.simplex_type, order); + const int local_dofs = static_cast(local_nodes.size()); + const int point_base = static_cast(out.points.size()) / out.gdim; + + for (const auto& xi : local_nodes) + { + const auto xi_parent = map_child_xi_to_parent_xi( + map.simplex_type, + child_vertices, + std::span(xi.data(), xi.size())); + std::vector x; + if (valid) + { + x = curved_map_physical_point( + map, std::span(xi_parent.data(), xi_parent.size())); + } + else + { + const auto ref = straight_ref_from_cell_point( + map, std::span(xi_parent.data(), xi_parent.size())); + x = push_parent_ref_to_physical(map, std::span(ref.data(), ref.size())); + } + out.points.insert(out.points.end(), x.begin(), x.end()); + } + + std::vector perm; + int vtk_type = 0; + if (map.simplex_type == cell::type::interval) + { + vtk_type = vtk_lagrange_curve; + perm.resize(static_cast(local_dofs)); + std::iota(perm.begin(), perm.end(), 0); + } + else if (map.simplex_type == cell::type::triangle) + { + vtk_type = vtk_lagrange_triangle; + perm = io::basix_to_vtk_lagrange_permutation( + cell::type::triangle, local_dofs, order); + } + else if (map.simplex_type == cell::type::quadrilateral) + { + vtk_type = vtk_lagrange_quadrilateral; + perm.resize(static_cast(local_dofs)); + std::iota(perm.begin(), perm.end(), 0); + } + else if (map.simplex_type == cell::type::tetrahedron) + { + vtk_type = vtk_lagrange_tetrahedron; + perm = io::basix_to_vtk_lagrange_permutation( + cell::type::tetrahedron, local_dofs, order); + } + else if (map.simplex_type == cell::type::prism) + { + vtk_type = vtk_lagrange_wedge; + perm.resize(static_cast(local_dofs)); + std::iota(perm.begin(), perm.end(), 0); + } + else + { + throw std::runtime_error("append_lagrange_simplex: unsupported simplex type"); + } + + for (const int p : perm) + out.connectivity.push_back(point_base + p); + out.offsets.push_back(static_cast(out.connectivity.size())); + out.vtk_types.push_back(vtk_type); + out.parent_map.push_back(static_cast(parent_cell_id)); + out.curved_valid.push_back(valid ? 1 : 0); + out.subdivision_depth.push_back(static_cast(subdivision_depth)); + out.curving_status.push_back(static_cast(curving_status)); +} + +template +void append_lagrange_simplex(CurvedVTUGrid& out, + const LocalCurvedSimplexMap& map, + int parent_cell_id, + int curving_status, + int max_subdivision_depth) +{ + const auto child = canonical_simplex_vertices(map.simplex_type); + append_lagrange_child_simplex( + out, + map, + parent_cell_id, + curving_status, + std::span(child.data(), child.size()), + 0, + max_subdivision_depth); +} + +template +void append_linear_entity_to_curved_grid(CurvedVTUGrid& out, + const AdaptCell& adapt_cell, + const SelectedEntity& entity, + std::span parent_vertex_coords, + int parent_cell_id) +{ + const auto ref_coords = entity_reference_coords( + adapt_cell, + std::span(entity.vertices.data(), entity.vertices.size())); + const std::vector parent_coords(parent_vertex_coords.begin(), + parent_vertex_coords.end()); + const auto phys_coords = cell::push_forward_affine_map( + adapt_cell.parent_cell_type, + parent_coords, + out.gdim, + std::span(ref_coords.data(), ref_coords.size())); + + const int point_base = static_cast(out.points.size()) / out.gdim; + out.points.insert(out.points.end(), phys_coords.begin(), phys_coords.end()); + + if (is_simplex(entity.type)) + { + for (int i = 0; i < static_cast(entity.vertices.size()); ++i) + out.connectivity.push_back(point_base + i); + } + else + { + const auto perm = cell::basix_to_vtk_vertex_permutation(entity.type); + for (const int p : perm) + out.connectivity.push_back(point_base + p); + } + + out.offsets.push_back(static_cast(out.connectivity.size())); + out.vtk_types.push_back(static_cast(cell::map_cell_type_to_vtk(entity.type))); + out.parent_map.push_back(static_cast(parent_cell_id)); + out.curved_valid.push_back(0); + out.subdivision_depth.push_back(0); + out.curving_status.push_back(0); +} + +template +void append_curved_child_simplex_quadrature(quadrature::QuadratureRules& rules, + const LocalCurvedSimplexMap& map, + int parent_cell_id, + int order, + std::span child_vertices, + int subdivision_depth, + int max_subdivision_depth) +{ + if (rules._tdim == 0) + rules._tdim = map.parent_tdim; + if (rules._offset.empty()) + rules._offset.push_back(0); + + const bool valid = curved_child_map_valid(map, child_vertices); + if (!valid && subdivision_depth < max_subdivision_depth) + { + const auto children = subdivide_child_simplex(map.simplex_type, child_vertices); + for (const auto& child : children) + { + append_curved_child_simplex_quadrature( + rules, + map, + parent_cell_id, + order, + std::span(child.data(), child.size()), + subdivision_depth + 1, + max_subdivision_depth); + } + return; + } + + const auto ref_rule = quadrature::get_reference_rule(map.simplex_type, order); + for (int q = 0; q < ref_rule._num_points; ++q) + { + std::span xi_child( + ref_rule._points.data() + static_cast(q * ref_rule._tdim), + static_cast(ref_rule._tdim)); + const auto xi = map_child_xi_to_parent_xi( + map.simplex_type, + child_vertices, + xi_child); + std::vector ref; + if (valid) + ref = curved_map_ref_point(map, std::span(xi.data(), xi.size())); + else + { + ref = straight_ref_from_cell_point(map, std::span(xi.data(), xi.size())); + } + rules._points.insert(rules._points.end(), ref.begin(), ref.end()); + + const T child_measure = finite_difference_child_measure( + map.simplex_type, child_vertices, xi_child); + T measure = T(0); + if (valid) + { + measure = std::abs(finite_difference_measure( + map, std::span(xi.data(), xi.size()))) * child_measure; + } + else + { + measure = finite_difference_straight_measure( + map, std::span(xi.data(), xi.size())) * child_measure; + } + rules._weights.push_back(ref_rule._weights[static_cast(q)] * measure); + } + rules._parent_map.push_back(static_cast(parent_cell_id)); + rules._offset.push_back(static_cast(rules._weights.size())); +} + +template +void append_curved_simplex_quadrature(quadrature::QuadratureRules& rules, + const LocalCurvedSimplexMap& map, + int parent_cell_id, + int order, + int max_subdivision_depth) +{ + const auto child = canonical_simplex_vertices(map.simplex_type); + append_curved_child_simplex_quadrature( + rules, + map, + parent_cell_id, + order, + std::span(child.data(), child.size()), + 0, + max_subdivision_depth); +} + +template +LocalCurvedSimplexMap make_local_map(const HOMeshPart& part, + const AdaptCell& adapt_cell, + int cut_cell_id, + std::span simplex_vertices, + cell::type simplex_type, + std::span parent_vertex_coords, + int geometry_order, + curving::NodeFamily node_family, + const curving::CurvingOptions& options, + int required_zero_entity_index = -1) +{ + const auto& mesh = *part.bg->mesh; + LocalCurvedSimplexMap map; + map.simplex_type = simplex_type; + map.dim = cell::get_tdim(simplex_type); + map.parent_tdim = mesh.tdim; + map.gdim = mesh.gdim; + map.parent_cell_type = adapt_cell.parent_cell_type; + map.geometry_order = geometry_order; + map.node_family = node_family; + map.vertices.assign(simplex_vertices.begin(), simplex_vertices.end()); + map.parent_physical_coords.assign(parent_vertex_coords.begin(), parent_vertex_coords.end()); + map.ref_vertex_coords = entity_reference_coords(adapt_cell, simplex_vertices); + + attach_curved_boundaries( + map, part, adapt_cell, cut_cell_id, options, required_zero_entity_index); + return map; +} + +template +void append_curved_entity(CurvedVTUGrid& out, + const HOMeshPart& part, + const AdaptCell& adapt_cell, + int cut_cell_id, + const SelectedEntity& entity, + std::span parent_vertex_coords, + int geometry_order, + curving::NodeFamily node_family, + const curving::CurvingOptions& options) +{ + const int entity_dim = cell::get_tdim(entity.type); + if (entity_dim == 0) + return; + + std::vector> simplices; + cell::type simplex_type = entity.type; + if (is_simplex(entity.type)) + { + simplices.push_back(entity.vertices); + } + else if (supports_curved_lagrange_cell(entity.type)) + { + simplices.push_back(entity.vertices); + } + else + { + append_linear_entity_to_curved_grid( + out, + adapt_cell, + entity, + parent_vertex_coords, + static_cast( + part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)])); + return; + } + + for (const auto& simplex_vertices : simplices) + { + auto map = make_local_map( + part, + adapt_cell, + cut_cell_id, + std::span(simplex_vertices.data(), simplex_vertices.size()), + simplex_type, + parent_vertex_coords, + geometry_order, + node_family, + options, + entity.zero_entity_index); + int status = 0; + if (entity.zero_entity_index >= 0) + { + const auto& state = curving::ensure_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + cut_cell_id, + entity.zero_entity_index, + options); + status = static_cast(state.status); + } + else if (!map.curved_faces.empty() || !map.curved_edges.empty()) + { + status = static_cast(curving::CurvingStatus::curved); + } + append_lagrange_simplex( + out, map, + static_cast(part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]), + status, + options.max_subdivision_depth); + } +} + +template +void append_curved_entity_quadrature(quadrature::QuadratureRules& rules, + const HOMeshPart& part, + const AdaptCell& adapt_cell, + int cut_cell_id, + const SelectedEntity& entity, + std::span parent_vertex_coords, + int geometry_order, + curving::NodeFamily node_family, + const curving::CurvingOptions& options, + int quadrature_order) +{ + const int entity_dim = cell::get_tdim(entity.type); + if (entity_dim == 0) + return; + + std::vector> simplices; + cell::type simplex_type = entity.type; + if (is_simplex(entity.type)) + { + simplices.push_back(entity.vertices); + } + else if (supports_curved_lagrange_cell(entity.type)) + { + simplices.push_back(entity.vertices); + } + else + { + return; + } + + const int parent_cell_id = + static_cast(part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]); + for (const auto& simplex_vertices : simplices) + { + auto map = make_local_map( + part, + adapt_cell, + cut_cell_id, + std::span(simplex_vertices.data(), simplex_vertices.size()), + simplex_type, + parent_vertex_coords, + geometry_order, + node_family, + options, + entity.zero_entity_index); + append_curved_simplex_quadrature( + rules, map, parent_cell_id, quadrature_order, + options.max_subdivision_depth); + } +} + +template +void append_curved_cut_quadrature(quadrature::QuadratureRules& rules, + const HOMeshPart& part, + int quadrature_order, + int geometry_order, + curving::NodeFamily node_family, + const curving::CurvingOptions& options) +{ + const auto& mesh = *part.bg->mesh; + for (std::int32_t cut_id : part.cut_cell_ids) + { + const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; + const auto entities = selected_entities(part, adapt_cell, cut_id); + if (entities.empty()) + continue; + + const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + const auto cut_active_mask = + part.cut_cells->active_level_set_mask[static_cast(cut_id)]; + const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); + for (const auto& entity : entities) + { + append_curved_entity_quadrature( + rules, + part, + adapt_cell, + cut_id, + entity, + std::span(parent_vertex_coords.data(), parent_vertex_coords.size()), + geometry_order, + node_family, + options, + quadrature_order); + } + } +} + template void append_cut_entities(mesh::CutMesh& out, quadrature::QuadratureRules* rules, const HOMeshPart& part, - bool triangulate, int quadrature_order) { const auto& mesh = *part.bg->mesh; @@ -924,7 +2874,7 @@ void append_cut_entities(mesh::CutMesh& out, mesh.gdim, entity.type, static_cast(parent_cell_id), - triangulate, + /*triangulate=*/false, /*input_is_basix=*/true, adapt_cell.parent_cell_type, std::span(root_vertex_flags.data(), root_vertex_flags.size())); @@ -940,7 +2890,7 @@ void append_cut_entities(mesh::CutMesh& out, mesh.gdim, static_cast(parent_cell_id), quadrature_order, - triangulate || !is_simplex(entity.type), + /*triangulate=*/false, /*input_is_basix=*/true, adapt_cell.parent_cell_type, std::span(root_vertex_flags.data(), root_vertex_flags.size())); @@ -953,7 +2903,6 @@ template void append_uncut_volume_cells(mesh::CutMesh& out, quadrature::QuadratureRules* rules, const HOMeshPart& part, - bool triangulate, int quadrature_order) { const auto& mesh = *part.bg->mesh; @@ -970,7 +2919,7 @@ void append_uncut_volume_cells(mesh::CutMesh& out, mesh.gdim, ctype, static_cast(cell_id), - triangulate, + /*triangulate=*/false, /*input_is_basix=*/false, cell::type::point, std::span()); @@ -987,7 +2936,7 @@ void append_uncut_volume_cells(mesh::CutMesh& out, mesh.gdim, static_cast(cell_id), quadrature_order, - triangulate, + /*triangulate=*/false, /*input_is_basix=*/false, cell::type::point, std::span()); @@ -995,15 +2944,64 @@ void append_uncut_volume_cells(mesh::CutMesh& out, } } +template +void append_uncut_curved_cells(CurvedVTUGrid& out, + const HOMeshPart& part, + int geometry_order, + curving::NodeFamily node_family) +{ + const auto& mesh = *part.bg->mesh; + if (part.dim != mesh.tdim) + return; + + for (I cell_id : part.uncut_cell_ids) + { + const auto ctype = mesh.cell_type(cell_id); + if (!supports_curved_lagrange_cell(ctype)) + continue; + + const auto parent_coords = parent_cell_vertex_coords_vtk(mesh, cell_id); + const auto ref_vertices = cell::reference_vertices(ctype); + const int nv = cell::get_num_vertices(ctype); + LocalCurvedSimplexMap map; + map.simplex_type = ctype; + map.dim = cell::get_tdim(ctype); + map.parent_tdim = mesh.tdim; + map.gdim = mesh.gdim; + map.parent_cell_type = ctype; + map.geometry_order = geometry_order; + map.node_family = node_family; + map.vertices.resize(static_cast(nv)); + std::iota(map.vertices.begin(), map.vertices.end(), 0); + map.ref_vertex_coords = ref_vertices; + map.parent_physical_coords = parent_coords; + append_lagrange_simplex(out, map, static_cast(cell_id), 0, 0); + } +} + } // namespace template mesh::CutMesh visualization_mesh(const HOMeshPart& part, bool include_uncut_cells, - bool triangulate) + int geometry_order, + curving::NodeFamily node_family) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); + if (geometry_order > 1) + { + curving::CurvingOptions options; + options.geometry_order = geometry_order; + options.node_family = node_family; + curving::ensure_all_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + options); + } mesh::CutMesh out; out._gdim = part.bg->mesh->gdim; @@ -1014,7 +3012,6 @@ mesh::CutMesh visualization_mesh(const HOMeshPart& part, out, static_cast*>(nullptr), part, - triangulate, /*quadrature_order=*/0); if (include_uncut_cells) { @@ -1022,7 +3019,6 @@ mesh::CutMesh visualization_mesh(const HOMeshPart& part, out, static_cast*>(nullptr), part, - triangulate, /*quadrature_order=*/0); } @@ -1033,43 +3029,129 @@ template quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, bool include_uncut_cells, - bool triangulate) + int geometry_order, + curving::NodeFamily node_family) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - if (part.bg->num_level_sets != 1) + + curving::CurvingOptions options; + if (geometry_order > 1) { - throw std::runtime_error( - "HOMeshPart output currently supports exactly one level set"); + options.geometry_order = geometry_order; + options.node_family = construction_node_family(geometry_order, node_family); + curving::ensure_all_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + options); } mesh::CutMesh unused_mesh; quadrature::QuadratureRules rules; rules._offset.push_back(0); - append_cut_entities(unused_mesh, &rules, part, triangulate, order); + if (geometry_order > 1) + { + append_curved_cut_quadrature( + rules, part, order, geometry_order, options.node_family, options); + } + else + { + append_cut_entities(unused_mesh, &rules, part, order); + } if (include_uncut_cells) - append_uncut_volume_cells(unused_mesh, &rules, part, triangulate, order); + append_uncut_volume_cells(unused_mesh, &rules, part, order); return rules; } +template +CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, + bool include_uncut_cells, + int geometry_order, + curving::NodeFamily node_family) +{ + if (!part.cut_cells || !part.bg || !part.bg->mesh) + throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); + if (geometry_order <= 1) + throw std::runtime_error("curved_lagrange_grid requires geometry_order > 1"); + + curving::CurvingOptions options; + options.geometry_order = geometry_order; + options.node_family = construction_node_family(geometry_order, node_family); + curving::ensure_all_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + options); + + CurvedVTUGrid out; + out.gdim = part.bg->mesh->gdim; + out.tdim = part.dim; + out.geometry_order = geometry_order; + out.offsets.push_back(0); + + const auto& mesh = *part.bg->mesh; + for (std::int32_t cut_id : part.cut_cell_ids) + { + const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; + const auto entities = selected_entities(part, adapt_cell, cut_id); + if (entities.empty()) + continue; + + const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); + for (const auto& entity : entities) + { + append_curved_entity( + out, + part, + adapt_cell, + cut_id, + entity, + std::span(parent_vertex_coords.data(), parent_vertex_coords.size()), + geometry_order, + options.node_family, + options); + } + } + + if (include_uncut_cells) + append_uncut_curved_cells(out, part, geometry_order, node_family); + + return out; +} + template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, bool); + const HOMeshPart&, bool, int, curving::NodeFamily); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, bool); + const HOMeshPart&, bool, int, curving::NodeFamily); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, bool); + const HOMeshPart&, bool, int, curving::NodeFamily); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, bool); + const HOMeshPart&, bool, int, curving::NodeFamily); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, bool); + const HOMeshPart&, int, bool, int, curving::NodeFamily); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, bool); + const HOMeshPart&, int, bool, int, curving::NodeFamily); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, bool); + const HOMeshPart&, int, bool, int, curving::NodeFamily); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, bool); + const HOMeshPart&, int, bool, int, curving::NodeFamily); + +template CurvedVTUGrid curved_lagrange_grid( + const HOMeshPart&, bool, int, curving::NodeFamily); +template CurvedVTUGrid curved_lagrange_grid( + const HOMeshPart&, bool, int, curving::NodeFamily); +template CurvedVTUGrid curved_lagrange_grid( + const HOMeshPart&, bool, int, curving::NodeFamily); +template CurvedVTUGrid curved_lagrange_grid( + const HOMeshPart&, bool, int, curving::NodeFamily); } // namespace cutcells::output diff --git a/cpp/src/ho_mesh_part_output.h b/cpp/src/ho_mesh_part_output.h index 3dc28ec..4f5516a 100644 --- a/cpp/src/ho_mesh_part_output.h +++ b/cpp/src/ho_mesh_part_output.h @@ -6,23 +6,52 @@ #pragma once #include +#include +#include #include "cut_mesh.h" +#include "curving.h" #include "ho_cut_mesh.h" #include "quadrature.h" namespace cutcells::output { +template +struct CurvedVTUGrid +{ + int gdim = 0; + int tdim = 0; + int geometry_order = 1; + + std::vector points; + std::vector connectivity; + std::vector offsets; + std::vector vtk_types; + + std::vector parent_map; + std::vector curved_valid; + std::vector subdivision_depth; + std::vector curving_status; +}; + template mesh::CutMesh visualization_mesh(const HOMeshPart& part, bool include_uncut_cells, - bool triangulate); + int geometry_order = -1, + curving::NodeFamily node_family = curving::NodeFamily::gll); template quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, bool include_uncut_cells, - bool triangulate); + int geometry_order = -1, + curving::NodeFamily node_family = curving::NodeFamily::gll); + +template +CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, + bool include_uncut_cells, + int geometry_order, + curving::NodeFamily node_family = curving::NodeFamily::lagrange); } // namespace cutcells::output diff --git a/cpp/src/level_set_cell.cpp b/cpp/src/level_set_cell.cpp index 807f889..60513fc 100644 --- a/cpp/src/level_set_cell.cpp +++ b/cpp/src/level_set_cell.cpp @@ -453,11 +453,30 @@ make_cell_level_set(const LevelSetFunction& global_ls, // Determine cell type cell::type ctype = infer_ls_cell_type(md, cell_id); cell_ls.cell_type = ctype; + cell_ls.gdim = gdim; cell_ls.tdim = cell::get_tdim(ctype); // Get the cell's DOF indices auto cell_dofs = md.cell_dofs_span(cell_id); const int ndofs = static_cast(cell_dofs.size()); + const int nv = cell::get_num_vertices(ctype); + if (ndofs < nv) + { + throw std::runtime_error( + "make_cell_level_set: cell dof map is missing vertex dofs"); + } + cell_ls.parent_vertex_coords.resize( + static_cast(nv * gdim), T(0)); + for (int i = 0; i < nv; ++i) + { + const I dof = cell_dofs[static_cast(i)]; + const T* x = md.dof_coordinate(dof); + for (int d = 0; d < gdim; ++d) + { + cell_ls.parent_vertex_coords[ + static_cast(i * gdim + d)] = x[d]; + } + } // Extract nodal values from the global DOF array cell_ls.nodal_values.resize(static_cast(ndofs)); diff --git a/cpp/src/level_set_cell.h b/cpp/src/level_set_cell.h index 8731c6b..dbc34b0 100644 --- a/cpp/src/level_set_cell.h +++ b/cpp/src/level_set_cell.h @@ -26,8 +26,10 @@ struct LevelSetCell // --- Cell geometry --- cell::type cell_type = cell::type::point; ///< cell type of the background cell + int gdim = 0; ///< geometric dimension int tdim = 0; ///< topological dimension I cell_id = static_cast(-1); ///< background cell index + std::vector parent_vertex_coords; ///< physical parent-cell vertices // --- Evaluation interface (uniform for both backends) --- // Evaluate the level set at a point xi in reference coordinates. @@ -62,4 +64,4 @@ LevelSetCell make_cell_level_set(const LevelSetFunction& global_ls, I cell_id); -} // namespace cutcells \ No newline at end of file +} // namespace cutcells diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index f157af8..32ce2a4 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -398,22 +398,22 @@ int expected_local_dofs(const HOCellFamily family, const int degree) return (degree + 1) * (degree + 2) * (degree + 3) / 6; } -std::vector basix_to_vtk_lagrange_permutation(const HOCellFamily family, - const int local_dofs, - const int degree) +std::vector basix_to_vtk_lagrange_permutation_impl(const HOCellFamily family, + const int local_dofs, + const int degree) { - if (degree < 2 || degree > 4) - { - throw std::runtime_error( - "write_level_set_vtu: unsupported polynomial degree " + std::to_string(degree) + if (degree < 2 || degree > 4) + { + throw std::runtime_error( + "basix_to_vtk_lagrange_permutation: unsupported polynomial degree " + std::to_string(degree) + " (supported: 2, 3, 4)"); - } + } - if (local_dofs != expected_local_dofs(family, degree)) - { - throw std::runtime_error( - "write_level_set_vtu: local dof count does not match cell family/degree"); - } + if (local_dofs != expected_local_dofs(family, degree)) + { + throw std::runtime_error( + "basix_to_vtk_lagrange_permutation: local dof count does not match cell family/degree"); + } if (family == HOCellFamily::triangle) return basix_to_vtk_triangle(local_dofs); @@ -424,6 +424,21 @@ std::vector basix_to_vtk_lagrange_permutation(const HOCellFamily family, namespace cutcells::io { + std::vector basix_to_vtk_lagrange_permutation(cell::type cell_type, + int local_dofs, + int degree) + { + HOCellFamily family; + if (cell_type == cell::type::triangle) + family = HOCellFamily::triangle; + else if (cell_type == cell::type::tetrahedron) + family = HOCellFamily::tetrahedron; + else + throw std::runtime_error( + "basix_to_vtk_lagrange_permutation: supported cells are triangle and tetrahedron"); + return basix_to_vtk_lagrange_permutation_impl(family, local_dofs, degree); + } + void write_vtk(std::string filename, const std::span element_vertex_coords, const std::span connectivity, const std::span offsets, @@ -534,7 +549,7 @@ namespace cutcells::io { const std::span cell_dofs = mesh_data.cell_dofs_span(cell_id); const HOCellFamily family = infer_cell_family(mesh_data, cell_id); - const std::vector perm = basix_to_vtk_lagrange_permutation( + const std::vector perm = basix_to_vtk_lagrange_permutation_impl( family, static_cast(cell_dofs.size()), mesh_data.degree); @@ -604,4 +619,102 @@ namespace cutcells::io << "\n"; } + void write_lagrange_vtk(std::string filename, + const std::span point_coords, + const std::span connectivity, + const std::span offsets, + const std::span vtk_types, + int gdim, + const std::span parent_map, + const std::span curved_valid, + const std::span subdivision_depth, + const std::span curving_status) + { + if (gdim < 1 || gdim > 3) + throw std::runtime_error("write_lagrange_vtk: gdim must be 1, 2, or 3"); + if (offsets.empty()) + throw std::runtime_error("write_lagrange_vtk: offsets must not be empty"); + + const int num_points = static_cast(point_coords.size()) / gdim; + const int num_cells = static_cast(offsets.size()) - 1; + + auto check_cell_data = [num_cells](std::span data, + const char* name) + { + if (!data.empty() && static_cast(data.size()) != num_cells) + throw std::runtime_error(std::string("write_lagrange_vtk: cell data size mismatch for ") + name); + }; + check_cell_data(parent_map, "parent_map"); + check_cell_data(curved_valid, "curved_valid"); + check_cell_data(subdivision_depth, "subdivision_depth"); + check_cell_data(curving_status, "curving_status"); + + std::ofstream ofs(filename.c_str(), std::ios::out); + if (!ofs) + throw std::runtime_error("write_lagrange_vtk: unable to open file " + filename); + + ofs << "\n" + << "\n" + << "\t\n" + << "\t\t\n"; + + ofs << "\t\t\t\n" + << "\t\t\t "; + for (int i = 0; i < num_points; ++i) + { + const std::size_t base = static_cast(i) * static_cast(gdim); + const double x = point_coords[base]; + const double y = (gdim >= 2) ? point_coords[base + 1] : 0.0; + const double z = (gdim >= 3) ? point_coords[base + 2] : 0.0; + ofs << x << " " << y << " " << z << " "; + } + ofs << "\n" + << "\t\t\t\n"; + + if (!parent_map.empty() || !curved_valid.empty() + || !subdivision_depth.empty() || !curving_status.empty()) + { + ofs << "\t\t\t\n"; + auto write_i32_data = [&ofs](std::span data, + const char* name) + { + if (data.empty()) + return; + ofs << "\t\t\t "; + for (const auto v : data) + ofs << v << " "; + ofs << "\n"; + }; + write_i32_data(parent_map, "parent_id"); + write_i32_data(curved_valid, "curved_valid"); + write_i32_data(subdivision_depth, "subdivision_depth"); + write_i32_data(curving_status, "curving_status"); + ofs << "\t\t\t\n"; + } + + ofs << "\t\t\t\n"; + ofs << "\t\t\t "; + for (const int dof : connectivity) + ofs << dof << " "; + ofs << "\n"; + + ofs << "\t\t\t "; + for (int i = 1; i < static_cast(offsets.size()); ++i) + ofs << offsets[static_cast(i)] << " "; + ofs << "\n"; + + ofs << "\t\t\t "; + for (const int type : vtk_types) + ofs << type << " "; + ofs << "\n"; + ofs << "\t\t\t\n"; + + ofs << "\t\t\n" + << "\t\n" + << "\n"; + } + } diff --git a/cpp/src/write_vtk.h b/cpp/src/write_vtk.h index 4e844a3..ea37335 100644 --- a/cpp/src/write_vtk.h +++ b/cpp/src/write_vtk.h @@ -7,6 +7,7 @@ #include #include +#include #include #include "cut_cell.h" #include "cut_mesh.h" @@ -15,6 +16,10 @@ namespace cutcells::io { + std::vector basix_to_vtk_lagrange_permutation(cell::type cell_type, + int local_dofs, + int degree); + void write_vtk(std::string filename, const std::span element_vertex_coords, const std::span connectivity, const std::span offsets, @@ -78,4 +83,41 @@ namespace cutcells::io void write_level_set_vtu(std::string filename, const cutcells::LevelSetFunction& ls, std::string field_name = "phi"); + + void write_lagrange_vtk(std::string filename, + const std::span point_coords, + const std::span connectivity, + const std::span offsets, + const std::span vtk_types, + int gdim, + const std::span parent_map = {}, + const std::span curved_valid = {}, + const std::span subdivision_depth = {}, + const std::span curving_status = {}); + + template + void write_lagrange_vtk(std::string filename, + const std::span point_coords, + const std::span connectivity, + const std::span offsets, + const std::span vtk_types, + int gdim, + const std::span parent_map = {}, + const std::span curved_valid = {}, + const std::span subdivision_depth = {}, + const std::span curving_status = {}) + { + std::vector coords_d(point_coords.begin(), point_coords.end()); + write_lagrange_vtk( + filename, + std::span(coords_d.data(), coords_d.size()), + connectivity, + offsets, + vtk_types, + gdim, + parent_map, + curved_valid, + subdivision_depth, + curving_status); + } } diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 1f1d8c5..b5db3c3 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -23,15 +23,23 @@ def _load_cpp_module(): # If we're running from a source tree, prefer an up-to-date in-tree build. # The repo keeps `cutcells/wrapper.cpp` next to this file; if the extension - # is older than the wrapper source, it's almost certainly stale (and tends - # to miss newer bindings like HOMeshPart.visualization_mesh, etc.). + # is older than the wrapper source or the C++ library sources, it's stale. wrapper_cpp = _Path(__file__).with_name("wrapper.cpp") - if wrapper_cpp.exists(): + source_roots = [wrapper_cpp] + cpp_src = _Path(__file__).resolve().parents[2] / "cpp" / "src" + if cpp_src.exists(): try: - wrapper_ts = wrapper_cpp.stat().st_mtime + source_roots.extend(cpp_src.glob("*.cpp")) + source_roots.extend(cpp_src.glob("*.h")) + except OSError: + pass + source_roots = [p for p in source_roots if p.exists()] + if source_roots: + try: + newest_source_ts = max(p.stat().st_mtime for p in source_roots) candidates = [ c for c in candidates - if c.exists() and c.stat().st_mtime >= wrapper_ts + if c.exists() and c.stat().st_mtime >= newest_source_ts ] except OSError: pass diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 4d3aa94..134ac81 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "../../cpp/src/cell_types.h" #include "../../cpp/src/cut_cell.h" @@ -721,22 +722,59 @@ inline bool part_mode_is_cut_only(std::string_view mode) template cutcells::mesh::CutMesh part_visualization_mesh( const cutcells::HOMeshPart& part, - std::string_view mode) + std::string_view mode, + int geometry_order, + std::string_view node_family) { const bool cut_only = part_mode_is_cut_only(mode); + const auto family = cutcells::curving::node_family_from_string(node_family); return cutcells::output::visualization_mesh( - part, /*include_uncut_cells=*/!cut_only, /*triangulate=*/false); + part, /*include_uncut_cells=*/!cut_only, geometry_order, family); } template cutcells::quadrature::QuadratureRules part_quadrature( const cutcells::HOMeshPart& part, int order, - std::string_view mode) + std::string_view mode, + int geometry_order, + std::string_view node_family) { const bool cut_only = part_mode_is_cut_only(mode); + const auto family = cutcells::curving::node_family_from_string(node_family); return cutcells::output::quadrature_rules( - part, order, /*include_uncut_cells=*/!cut_only, /*triangulate=*/false); + part, order, /*include_uncut_cells=*/!cut_only, geometry_order, family); +} + +template +void part_write_vtu(const cutcells::HOMeshPart& part, + const std::string& filename, + std::string_view mode, + int geometry_order, + std::string_view node_family) +{ + const bool cut_only = part_mode_is_cut_only(mode); + auto family = cutcells::curving::node_family_from_string(node_family); + if (geometry_order > 1) + { + const auto grid = cutcells::output::curved_lagrange_grid( + part, /*include_uncut_cells=*/!cut_only, geometry_order, family); + cutcells::io::write_lagrange_vtk( + filename, + std::span(grid.points.data(), grid.points.size()), + std::span(grid.connectivity.data(), grid.connectivity.size()), + std::span(grid.offsets.data(), grid.offsets.size()), + std::span(grid.vtk_types.data(), grid.vtk_types.size()), + grid.gdim, + std::span(grid.parent_map.data(), grid.parent_map.size()), + std::span(grid.curved_valid.data(), grid.curved_valid.size()), + std::span(grid.subdivision_depth.data(), grid.subdivision_depth.size()), + std::span(grid.curving_status.data(), grid.curving_status.size())); + return; + } + + auto vis = part_visualization_mesh(part, mode, geometry_order, node_family); + cutcells::io::write_vtk(filename, vis); } template @@ -1859,7 +1897,204 @@ void declare_ho_cut(nb::module_& m, const std::string& type) }, nb::arg("cut_cell_id"), nb::rv_policy::reference_internal, - "Return the AdaptCell for a cut-cell index."); + "Return the AdaptCell for a cut-cell index.") + .def( + "curved_zero_nodes", + [](HOCutResult& self, + int geometry_order, + const std::string& node_family) + { + cutcells::curving::CurvingOptions options; + options.geometry_order = geometry_order; + options.node_family = + cutcells::curving::node_family_from_string(node_family); + + try + { + cutcells::curving::ensure_all_curved( + self.cut_cells.curving, + std::span(self.cut_cells.parent_cell_ids), + std::span>(self.cut_cells.adapt_cells), + std::span>(self.cut_cells.level_set_cells), + std::span(self.cut_cells.ls_offsets), + options); + } + catch (const std::exception& e) + { + throw std::runtime_error( + std::string("curved_zero_nodes: ensure_all_curved failed: ") + + e.what()); + } + + std::vector points; + std::vector offsets; + std::vector status; + std::vector cut_cell_id; + std::vector local_zero_entity_id; + std::vector dim; + std::vector parent_dim; + std::vector parent_id; + std::vector zero_mask; + std::vector stats_offsets; + std::vector node_iterations; + std::vector node_status; + std::vector node_failure_code; + std::vector node_residual; + std::vector node_active_face_mask; + std::vector node_closest_face_id; + std::vector node_safe_subspace_dim; + std::vector node_projection_mode; + std::vector node_retry_count; + nb::list failure_reason; + std::vector total_iterations; + std::vector max_iterations; + offsets.reserve(self.cut_cells.curving.states.size() + 1); + offsets.push_back(0); + stats_offsets.reserve(self.cut_cells.curving.states.size() + 1); + stats_offsets.push_back(0); + + for (std::size_t i = 0; i < self.cut_cells.curving.states.size(); ++i) + { + try + { + const auto& state = self.cut_cells.curving.states[i]; + const auto& ident = self.cut_cells.curving.identities[i]; + points.insert(points.end(), state.ref_nodes.begin(), state.ref_nodes.end()); + offsets.push_back(static_cast( + points.size() / static_cast(self.cut_cells.tdim))); + status.push_back(static_cast(state.status)); + cut_cell_id.push_back(static_cast(ident.cut_cell_id)); + local_zero_entity_id.push_back(static_cast(ident.local_zero_entity_id)); + dim.push_back(static_cast(ident.dim)); + parent_dim.push_back(ident.parent_dim); + parent_id.push_back(ident.parent_id); + zero_mask.push_back(ident.zero_mask); + node_iterations.insert( + node_iterations.end(), + state.node_iterations.begin(), + state.node_iterations.end()); + node_status.insert( + node_status.end(), + state.node_status.begin(), + state.node_status.end()); + node_failure_code.insert( + node_failure_code.end(), + state.node_failure_code.begin(), + state.node_failure_code.end()); + node_residual.insert( + node_residual.end(), + state.node_residual.begin(), + state.node_residual.end()); + node_active_face_mask.insert( + node_active_face_mask.end(), + state.node_active_face_mask.begin(), + state.node_active_face_mask.end()); + node_closest_face_id.insert( + node_closest_face_id.end(), + state.node_closest_face_id.begin(), + state.node_closest_face_id.end()); + node_safe_subspace_dim.insert( + node_safe_subspace_dim.end(), + state.node_safe_subspace_dim.begin(), + state.node_safe_subspace_dim.end()); + node_projection_mode.insert( + node_projection_mode.end(), + state.node_projection_mode.begin(), + state.node_projection_mode.end()); + node_retry_count.insert( + node_retry_count.end(), + state.node_retry_count.begin(), + state.node_retry_count.end()); + stats_offsets.push_back(static_cast(node_iterations.size())); + failure_reason.append(state.failure_reason); + int total = 0; + int max_iter = 0; + for (const auto it : state.node_iterations) + { + total += static_cast(it); + max_iter = std::max(max_iter, static_cast(it)); + } + total_iterations.push_back(static_cast(total)); + max_iterations.push_back(static_cast(max_iter)); + } + catch (const std::exception& e) + { + throw std::runtime_error( + std::string("curved_zero_nodes: packaging state ") + + std::to_string(i) + + " failed: " + + e.what()); + } + } + + nb::dict result; + const std::size_t npoints = + self.cut_cells.tdim > 0 + ? points.size() / static_cast(self.cut_cells.tdim) + : 0; + result["points"] = as_nbarray( + std::move(points), + {npoints, static_cast(self.cut_cells.tdim)}); + result["offsets"] = as_nbarray(std::move(offsets)); + result["status"] = as_nbarray(std::move(status)); + result["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); + result["local_zero_entity_id"] = as_nbarray(std::move(local_zero_entity_id)); + result["dim"] = as_nbarray(std::move(dim)); + result["parent_dim"] = as_nbarray(std::move(parent_dim)); + result["parent_id"] = as_nbarray(std::move(parent_id)); + result["zero_mask"] = as_nbarray(std::move(zero_mask)); + result["stats_offsets"] = as_nbarray(std::move(stats_offsets)); + result["node_iterations"] = as_nbarray(std::move(node_iterations)); + result["node_status"] = as_nbarray(std::move(node_status)); + result["node_failure_code"] = as_nbarray(std::move(node_failure_code)); + result["node_residual"] = as_nbarray(std::move(node_residual)); + result["node_active_face_mask"] = as_nbarray(std::move(node_active_face_mask)); + result["node_closest_face_id"] = as_nbarray(std::move(node_closest_face_id)); + result["node_safe_subspace_dim"] = as_nbarray(std::move(node_safe_subspace_dim)); + result["node_projection_mode"] = as_nbarray(std::move(node_projection_mode)); + result["node_retry_count"] = as_nbarray(std::move(node_retry_count)); + result["failure_reason"] = std::move(failure_reason); + + nb::list failure_code_names; + for (const char* name : { + "none", + "exact_vertex", + "boundary_from_edge", + "invalid_constraint_count", + "missing_level_set_cell", + "empty_zero_mask", + "unsupported_entity", + "no_host_interval", + "no_sign_changing_bracket", + "brent_failed", + "outside_host_domain", + "singular_gradient_system", + "line_search_failed", + "max_iterations", + "missing_boundary_edge", + "boundary_edge_failed", + "projection_failed", + "closest_face_retry_failed", + "constrained_newton_failed"}) + failure_code_names.append(name); + result["failure_code_names"] = std::move(failure_code_names); + + nb::list projection_mode_names; + for (const char* name : { + "none", + "safe_line", + "closest_face_retry", + "constrained_newton", + "vector_newton"}) + projection_mode_names.append(name); + result["projection_mode_names"] = std::move(projection_mode_names); + result["total_iterations"] = as_nbarray(std::move(total_iterations)); + result["max_iterations"] = as_nbarray(std::move(max_iterations)); + return result; + }, + nb::arg("geometry_order") = 2, + nb::arg("node_family") = "gll", + "Return cached curved zero-entity nodes in parent reference coordinates."); // --- HOMeshPart --- std::string part_name = "HOMeshPart_" + type; @@ -1894,77 +2129,90 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::rv_policy::reference_internal) .def( "visualization_mesh", - [](const PartT& self, const std::string& mode) { + [](const PartT& self, + const std::string& mode, + int geometry_order, + const std::string& node_family) { nb::gil_scoped_release release; - return part_visualization_mesh(self, mode); + return part_visualization_mesh(self, mode, geometry_order, node_family); }, nb::arg("mode") = "full", - "Return a straight visualization mesh for an HOMeshPart.") + nb::arg("geometry_order") = -1, + nb::arg("node_family") = "gll", + "Return a visualization mesh for an HOMeshPart, preserving AdaptCell topology.") .def( "quadrature", - [](const PartT& self, int order, const std::string& mode) { + [](const PartT& self, + int order, + const std::string& mode, + int geometry_order, + const std::string& node_family) { nb::gil_scoped_release release; - return part_quadrature(self, order, mode); + return part_quadrature(self, order, mode, geometry_order, node_family); }, nb::arg("order") = 3, nb::arg("mode") = "full", - "Return straight quadrature rules for a one-level-set HOMeshPart.\n" - "This bridge currently supports only single-level-set selections.") + nb::arg("geometry_order") = -1, + nb::arg("node_family") = "gll", + "Return quadrature rules for an HOMeshPart, preserving AdaptCell topology. For curved geometry this is the construction node family.") .def( "write_vtu", [](const PartT& self, const std::string& filename, - const std::string& mode) { + const std::string& mode, + int geometry_order, + const std::string& node_family) { nb::gil_scoped_release release; - auto vis = part_visualization_mesh(self, mode); - io::write_vtk(filename, vis); + part_write_vtu(self, filename, mode, geometry_order, node_family); }, nb::arg("filename"), nb::arg("mode") = "full", - "Write a straight visualization VTU file for a one-level-set HOMeshPart."); + nb::arg("geometry_order") = -1, + nb::arg("node_family") = "gll", + "Write a VTU file for an HOMeshPart. geometry_order > 1 samples the requested construction node family into curved VTK Lagrange cells."); // --- ho_cut() factory --- m.def("ho_cut", - [](const MeshViewT& mesh, const LevelSetT& ls) { + [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { nb::gil_scoped_release release; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, - nb::arg("mesh"), nb::arg("level_set"), + nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("ho_cut", - [](const MeshViewT& mesh, const std::vector& level_sets) { + [](const MeshViewT& mesh, const std::vector& level_sets, bool triangulate) { nb::gil_scoped_release release; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, - nb::arg("mesh"), nb::arg("level_sets"), + nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); m.def("cut", - [](const MeshViewT& mesh, const LevelSetT& ls) { + [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { nb::gil_scoped_release release; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, - nb::arg("mesh"), nb::arg("level_set"), + nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("cut", - [](const MeshViewT& mesh, const std::vector& level_sets) { + [](const MeshViewT& mesh, const std::vector& level_sets, bool triangulate) { nb::gil_scoped_release release; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, - nb::arg("mesh"), nb::arg("level_sets"), + nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); From 18111d61efe4ca6a05ada068a6bfb081b869baba Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Tue, 28 Apr 2026 10:55:24 +0200 Subject: [PATCH 15/23] 3d looking better but still tangling --- python/cutcells/wrapper.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 134ac81..5c010bd 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -75,13 +75,14 @@ auto as_nbarray(V&& x, std::size_t ndim, const std::size_t* shape) template auto as_nbarray(V&& x, const std::initializer_list shape) { - return as_nbarray(x, shape.size(), shape.begin()); + return as_nbarray(std::forward(x), shape.size(), shape.begin()); } template auto as_nbarray(V&& x) { - return as_nbarray(std::move(x), {x.size()}); + const std::size_t size = x.size(); + return as_nbarray(std::forward(x), {size}); } template From 38abf29e9d4f99eb0f081d5601f45212b7d05cd2 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Thu, 30 Apr 2026 12:32:42 +0200 Subject: [PATCH 16/23] introduction of tolerances for level set values and edge lengths --- cpp/src/cell_certification.cpp | 30 ++- cpp/src/curving.cpp | 171 +++++++++++- cpp/src/curving.h | 13 +- cpp/src/edge_root.h | 16 +- cpp/src/refine_cell.cpp | 142 ++++++++-- python/cutcells/__init__.py | 26 +- python/cutcells/wrapper.cpp | 56 +++- python/demo/demo_sphere_p2_cut_vtu.py | 254 ++++++++++++++++++ python/tests/test_certification_refinement.py | 66 +++++ python/tests/test_ho_part_straight_output.py | 129 +++++++++ 10 files changed, 856 insertions(+), 47 deletions(-) create mode 100644 python/demo/demo_sphere_p2_cut_vtu.py diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index 4f619a6..c7adff5 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -563,6 +563,7 @@ void append_ready_cut_part_cells( int parent_dim = parent_tdim; int parent_id = -1; + std::vector parent_param; int old_edge_id_for_token = -1; if (token >= 0 && token < 100) @@ -579,13 +580,32 @@ void append_ready_cut_part_cells( { parent_dim = 1; parent_id = parent_edge_id; + + auto edge_vertices = + adapt_cell.entity_to_vertex[1][static_cast(old_edge_id)]; + T parent_t0 = T(0); + T parent_t1 = T(1); + if (edge_vertices.size() == 2 + && vertex_parameter_on_parent_edge( + adapt_cell, edge_vertices[0], parent_edge_id, parent_t0) + && vertex_parameter_on_parent_edge( + adapt_cell, edge_vertices[1], parent_edge_id, parent_t1)) + { + parent_param.assign(1, T(0.5) * (parent_t0 + parent_t1)); + } } } } + if (parent_dim == parent_tdim && parent_id < 0) + { + parent_id = adapt_cell.parent_cell_id; + parent_param.assign(x.begin(), x.end()); + } vertex_id = append_vertex_with_parent_info( adapt_cell, x, parent_dim, parent_id, - std::span(), /*source_edge_id=*/-1); + std::span(parent_param), + old_edge_id_for_token); if (token >= 0 && token < 100) { @@ -1662,7 +1682,11 @@ void certify_and_refine(AdaptCell& adapt_cell, if (did_green) { if (refine_green_on_multiple_root_edges(adapt_cell, level_set_id)) + { + fill_all_vertex_signs_from_level_set( + adapt_cell, ls_cell, level_set_id, zero_tol); continue; + } } // 4. Red refinement: ambiguous cells. @@ -1682,7 +1706,11 @@ void certify_and_refine(AdaptCell& adapt_cell, if (did_red) { if (refine_red_on_ambiguous_cells(adapt_cell, level_set_id)) + { + fill_all_vertex_signs_from_level_set( + adapt_cell, ls_cell, level_set_id, zero_tol); continue; + } } // 5. No refinement needed — stop. diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp index cde749b..c8117cb 100644 --- a/cpp/src/curving.cpp +++ b/cpp/src/curving.cpp @@ -138,6 +138,94 @@ std::vector zero_entity_vertex_ids(const AdaptCell& ac, int local_zero_e return out; } +template +T distance_between_vertices(const AdaptCell& ac, int v0, int v1) +{ + T length2 = T(0); + for (int d = 0; d < ac.tdim; ++d) + { + const T delta = + ac.vertex_coords[static_cast(v1 * ac.tdim + d)] + - ac.vertex_coords[static_cast(v0 * ac.tdim + d)]; + length2 += delta * delta; + } + return std::sqrt(length2); +} + +template +T zero_entity_edge_length(const AdaptCell& ac, int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 1) + return std::numeric_limits::infinity(); + + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[1][static_cast(zid)]; + if (verts.size() != 2) + return std::numeric_limits::infinity(); + return distance_between_vertices( + ac, static_cast(verts[0]), static_cast(verts[1])); +} + +template +T zero_entity_face_max_edge_length(const AdaptCell& ac, int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 2) + return std::numeric_limits::infinity(); + + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[2][static_cast(zid)]; + if (verts.size() < 2) + return T(0); + + T max_length = T(0); + for (std::size_t i = 0; i < verts.size(); ++i) + { + const int v0 = static_cast(verts[i]); + const int v1 = static_cast(verts[(i + 1) % verts.size()]); + max_length = std::max( + max_length, + distance_between_vertices( + ac, + v0, + v1)); + } + return max_length; +} + +template +bool zero_entity_below_curving_threshold(const AdaptCell& ac, + int local_zero_entity_id, + const CurvingOptions& options) +{ + if (!(options.small_entity_tol > T(0))) + return false; + + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim == 1) + return zero_entity_edge_length(ac, local_zero_entity_id) + <= options.small_entity_tol; + if (zdim == 2) + return zero_entity_face_max_edge_length(ac, local_zero_entity_id) + <= options.small_entity_tol; + return false; +} + +template +void append_straight_seed_node_stats(CurvedZeroEntityState& state, + const AdaptCell& ac, + std::span seeds) +{ + const int n_nodes = static_cast( + seeds.size() / static_cast(std::max(ac.tdim, 1))); + for (int i = 0; i < n_nodes; ++i) + append_node_stats( + state, + accepted_node_stats( + CurvingFailureCode::small_entity_kept_straight)); +} + template std::pair legendre_value_and_derivative(int order, T x) { @@ -1312,6 +1400,45 @@ std::vector physical_host_gradient_reference_direction( fallback_ref); } +template +bool accept_seed_if_level_sets_within_tolerance( + const AdaptCell& ac, + int local_zero_entity_id, + std::span* const> active_cells, + std::span seed, + const CurvingOptions& options, + std::vector& out, + ProjectionStats& stats) +{ + if (active_cells.empty()) + return false; + + T max_abs_value = T(0); + for (const LevelSetCell* ls_cell : active_cells) + { + if (ls_cell == nullptr) + return false; + const T value = curving_level_set_value(*ls_cell, seed); + max_abs_value = std::max(max_abs_value, std::fabs(value)); + if (max_abs_value > options.ftol) + return false; + } + + out.assign(seed.begin(), seed.end()); + stats = {}; + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = max_abs_value; + stats.active_face_mask = add_near_active_faces( + ac, + seed, + structural_active_face_mask(ac, local_zero_entity_id), + options.active_face_tol); + stats.safe_subspace_dim = active_subspace_dim(ac, stats.active_face_mask); + stats.projection_mode = CurvingProjectionMode::none; + return true; +} + template std::vector> scalar_candidate_directions(const AdaptCell& ac, int local_zero_entity_id, @@ -1809,7 +1936,7 @@ bool scalar_project(const AdaptCell& ac, structural_active_face_mask(ac, local_zero_entity_id), options.active_face_tol); stats.safe_subspace_dim = active_subspace_dim(ac, stats.active_face_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; + stats.projection_mode = CurvingProjectionMode::none; return true; } std::vector grad(static_cast(tdim), T(0)); @@ -2108,6 +2235,10 @@ bool vector_project(const AdaptCell& ac, return false; } + if (accept_seed_if_level_sets_within_tolerance( + ac, local_zero_entity_id, ls_cells, seed, options, out, stats)) + return true; + std::vector values(static_cast(m), T(0)); std::vector grads(static_cast(m * tdim), T(0)); std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); @@ -2118,28 +2249,31 @@ bool vector_project(const AdaptCell& ac, for (int iter = 0; iter < options.max_iter; ++iter) { T norm_f = T(0); + T max_f = T(0); for (int i = 0; i < m; ++i) { values[static_cast(i)] = ls_cells[static_cast(i)]->value( std::span(out.data(), out.size())); norm_f += values[static_cast(i)] * values[static_cast(i)]; + max_f = std::max(max_f, std::fabs(values[static_cast(i)])); ls_cells[static_cast(i)]->grad( std::span(out.data(), out.size()), std::span(grads.data() + static_cast(i * tdim), static_cast(tdim))); } norm_f = std::sqrt(norm_f); - if (norm_f <= options.ftol) + if (max_f <= options.ftol) { stats.iterations = iter; stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; - stats.residual = norm_f; + stats.residual = max_f; stats.active_face_mask = active_mask; stats.closest_face_id = closest_face_id; stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - stats.projection_mode = CurvingProjectionMode::vector_newton; + stats.projection_mode = + (iter == 0) ? CurvingProjectionMode::none : CurvingProjectionMode::vector_newton; stats.retry_count = retry_count; return true; } @@ -2331,21 +2465,21 @@ bool vector_project(const AdaptCell& ac, } } - T final_norm = T(0); + T final_max = T(0); for (int i = 0; i < m; ++i) { const T f = ls_cells[static_cast(i)]->value( std::span(out.data(), out.size())); - final_norm += f * f; + final_max = std::max(final_max, std::fabs(f)); } stats.iterations = options.max_iter; - stats.residual = std::sqrt(final_norm); + stats.residual = final_max; stats.active_face_mask = active_mask; stats.closest_face_id = closest_face_id; stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); stats.projection_mode = CurvingProjectionMode::vector_newton; stats.retry_count = retry_count; - if (stats.residual <= options.ftol) + if (final_max <= options.ftol) { stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; @@ -2781,6 +2915,7 @@ void build_curved_state(CurvedZeroEntityState& state, state.failure_reason.clear(); state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; state.ref_nodes.clear(); @@ -2823,6 +2958,19 @@ void build_curved_state(CurvedZeroEntityState& state, return; } + if (zero_entity_below_curving_threshold(ac, local_zero_entity_id, options)) + { + if (zdim == 2) + append_face_seed_nodes(ac, local_zero_entity_id, options, seeds); + state.ref_nodes = seeds; + append_straight_seed_node_stats( + state, + ac, + std::span(state.ref_nodes.data(), state.ref_nodes.size())); + state.status = CurvingStatus::curved; + return; + } + const std::uint64_t effective_zero_mask = state.zero_mask & ac.active_level_set_mask; const auto ls_ids = active_level_sets(effective_zero_mask); if (ls_ids.empty()) @@ -3055,16 +3203,19 @@ const CurvedZeroEntityState& ensure_curved( state.status == CurvingStatus::curved && state.geometry_order == options.geometry_order && state.node_family == options.node_family + && state.small_entity_tol == options.small_entity_tol && state.zero_entity_version == ac.zero_entity_version; const bool failed = state.status == CurvingStatus::failed && state.geometry_order == options.geometry_order && state.node_family == options.node_family + && state.small_entity_tol == options.small_entity_tol && state.zero_entity_version == ac.zero_entity_version; if (!valid && !failed) { std::vector> boundary_edges; - if (ac.zero_entity_dim[static_cast(local_zero_entity_id)] == 2) + if (ac.zero_entity_dim[static_cast(local_zero_entity_id)] == 2 + && !zero_entity_below_curving_threshold(ac, local_zero_entity_id, options)) { const auto boundary_edge_refs = face_boundary_zero_edges(ac, local_zero_entity_id); @@ -3074,6 +3225,7 @@ const CurvedZeroEntityState& ensure_curved( state.failure_reason = "hierarchical zero-face curving requires boundary zero edges"; state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[ static_cast(local_zero_entity_id)]; @@ -3114,6 +3266,7 @@ const CurvedZeroEntityState& ensure_curved( state.failure_reason = "hierarchical zero-face boundary edge curving failed"; state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[ static_cast(local_zero_entity_id)]; diff --git a/cpp/src/curving.h b/cpp/src/curving.h index 90d59f1..f544a5d 100644 --- a/cpp/src/curving.h +++ b/cpp/src/curving.h @@ -6,7 +6,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -52,7 +54,8 @@ enum class CurvingFailureCode : std::uint8_t boundary_edge_failed = 15, projection_failed = 16, closest_face_retry_failed = 17, - constrained_newton_failed = 18 + constrained_newton_failed = 18, + small_entity_kept_straight = 19 }; enum class CurvingProjectionMode : std::uint8_t @@ -71,9 +74,14 @@ struct CurvingOptions NodeFamily node_family = NodeFamily::gll; int max_iter = 32; T xtol = T(1e-12); - T ftol = T(1e-12); + T ftol = std::sqrt(std::numeric_limits::epsilon()); T domain_tol = T(1e-10); T active_face_tol = T(1e-9); + // Length-scale threshold in parent reference coordinates. Zero edges + // shorter than this and zero faces whose boundary edges are all shorter + // than this are kept on their straight interpolation seeds instead of + // being projected. + T small_entity_tol = std::sqrt(std::numeric_limits::epsilon()); int max_subdivision_depth = 3; }; @@ -83,6 +91,7 @@ struct CurvedZeroEntityState CurvingStatus status = CurvingStatus::not_built; int geometry_order = -1; NodeFamily node_family = NodeFamily::gll; + T small_entity_tol = T(0); std::uint32_t zero_entity_version = 0; std::uint64_t zero_mask = 0; std::string failure_reason; diff --git a/cpp/src/edge_root.h b/cpp/src/edge_root.h index b6db9fd..f344194 100644 --- a/cpp/src/edge_root.h +++ b/cpp/src/edge_root.h @@ -19,6 +19,12 @@ namespace cutcells::cell::edge_root { +template +inline T default_value_tolerance() +{ + return std::sqrt(std::numeric_limits::epsilon()); +} + enum class method { linear = 0, @@ -69,7 +75,7 @@ struct RaySearchOptions { int max_iter = 64; T xtol = T(1e-12); - T ftol = T(1e-12); + T ftol = default_value_tolerance(); T domain_tol = T(1e-10); std::array probe_scales = {T(1), T(2), T(4)}; T max_probe_distance = std::numeric_limits::infinity(); @@ -572,7 +578,7 @@ inline RootSolveInfo find_root_parameter_info(const std::span p0, const T level = T(0), const int max_iter = 64, const T xtol = T(1e-12), - const T ftol = T(1e-12)) + const T ftol = default_value_tolerance()) { if (p0.size() != p1.size()) throw std::invalid_argument("find_root_parameter: inconsistent point dimensions"); @@ -739,7 +745,8 @@ template inline T find_root_parameter(const std::span p0, const std::span p1, Phi&& level_set, const method root_method = method::linear, const T level = T(0), const int max_iter = 64, - const T xtol = T(1e-12), const T ftol = T(1e-12)) + const T xtol = T(1e-12), + const T ftol = default_value_tolerance()) { return find_root_parameter_info(p0, p1, std::forward(level_set), root_method, level, max_iter, xtol, ftol).t; @@ -750,7 +757,8 @@ inline void find_root_point(const std::span p0, const std::span root_point, Phi&& level_set, const method root_method = method::linear, const T level = T(0), const int max_iter = 64, - const T xtol = T(1e-12), const T ftol = T(1e-12)) + const T xtol = T(1e-12), + const T ftol = default_value_tolerance()) { const T t = find_root_parameter(p0, p1, std::forward(level_set), root_method, level, max_iter, xtol, ftol); diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp index 40a6cbb..52aceef 100644 --- a/cpp/src/refine_cell.cpp +++ b/cpp/src/refine_cell.cpp @@ -121,26 +121,104 @@ void clear_topology_caches(AdaptCell& adapt_cell) } template -int append_vertex(AdaptCell& adapt_cell, std::span coords) +int append_vertex_with_parent_info(AdaptCell& adapt_cell, + std::span coords, + int parent_dim, + int parent_id, + std::span parent_param, + int source_edge_id) { const int new_v = adapt_cell.n_vertices(); adapt_cell.vertex_coords.insert(adapt_cell.vertex_coords.end(), coords.begin(), coords.end()); - adapt_cell.vertex_parent_dim.push_back(static_cast(adapt_cell.tdim)); - adapt_cell.vertex_parent_id.push_back(-1); - const std::int32_t param_offset = adapt_cell.vertex_parent_param_offset.empty() - ? 0 - : adapt_cell.vertex_parent_param_offset.back(); - adapt_cell.vertex_parent_param_offset.push_back(param_offset); + adapt_cell.vertex_parent_dim.push_back(static_cast(parent_dim)); + adapt_cell.vertex_parent_id.push_back(static_cast(parent_id)); + const std::int32_t param_offset = + static_cast(adapt_cell.vertex_parent_param.size()); + adapt_cell.vertex_parent_param.insert(adapt_cell.vertex_parent_param.end(), + parent_param.begin(), + parent_param.end()); + adapt_cell.vertex_parent_param_offset.push_back( + param_offset + static_cast(parent_param.size())); adapt_cell.zero_mask_per_vertex.push_back(0); adapt_cell.negative_mask_per_vertex.push_back(0); - adapt_cell.vertex_source_edge_id.push_back(-1); + adapt_cell.vertex_source_edge_id.push_back(static_cast(source_edge_id)); return new_v; } template -int append_interpolated_vertex(AdaptCell& adapt_cell, int v0, int v1, T t) +bool vertex_parameter_on_parent_edge(const AdaptCell& adapt_cell, + int vertex_id, + int parent_edge_id, + T& t) { + const int dim = adapt_cell.vertex_parent_dim[static_cast(vertex_id)]; + if (dim == 0) + { + const auto parent_edge = + cell::edges(adapt_cell.parent_cell_type)[static_cast(parent_edge_id)]; + if (parent_edge[0] == vertex_id) + { + t = T(0); + return true; + } + if (parent_edge[1] == vertex_id) + { + t = T(1); + return true; + } + return false; + } + + if (dim != 1) + return false; + if (adapt_cell.vertex_parent_id[static_cast(vertex_id)] != parent_edge_id) + return false; + + const auto begin = + static_cast(adapt_cell.vertex_parent_param_offset[static_cast(vertex_id)]); + const auto end = + static_cast(adapt_cell.vertex_parent_param_offset[static_cast(vertex_id + 1)]); + if (end - begin != 1) + return false; + + t = adapt_cell.vertex_parent_param[begin]; + return true; +} + +template +bool edge_is_on_single_parent_edge(const AdaptCell& adapt_cell, + int edge_id, + int& parent_edge_id) +{ + auto edge_vertices = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + if (edge_vertices.size() != 2) + return false; + + const auto parent_edges = cell::edges(adapt_cell.parent_cell_type); + for (int pe = 0; pe < static_cast(parent_edges.size()); ++pe) + { + T unused = T(0); + if (vertex_parameter_on_parent_edge(adapt_cell, edge_vertices[0], pe, unused) + && vertex_parameter_on_parent_edge(adapt_cell, edge_vertices[1], pe, unused)) + { + parent_edge_id = pe; + return true; + } + } + + return false; +} + +template +int append_interpolated_vertex_on_edge(AdaptCell& adapt_cell, int edge_id, T t) +{ + auto edge_vertices = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + if (edge_vertices.size() != 2) + throw std::runtime_error("append_interpolated_vertex_on_edge: invalid edge"); + const int v0 = edge_vertices[0]; + const int v1 = edge_vertices[1]; + std::vector x(static_cast(adapt_cell.tdim), T(0)); for (int d = 0; d < adapt_cell.tdim; ++d) { @@ -148,7 +226,30 @@ int append_interpolated_vertex(AdaptCell& adapt_cell, int v0, int v1, T t) const T x1 = adapt_cell.vertex_coords[static_cast(v1 * adapt_cell.tdim + d)]; x[static_cast(d)] = (T(1) - t) * x0 + t * x1; } - return append_vertex(adapt_cell, std::span(x)); + + int parent_dim = adapt_cell.tdim; + int parent_id = adapt_cell.parent_cell_id; + std::vector parent_param(x.begin(), x.end()); + + int parent_edge_id = -1; + T parent_t0 = T(0); + T parent_t1 = T(1); + if (edge_is_on_single_parent_edge(adapt_cell, edge_id, parent_edge_id) + && vertex_parameter_on_parent_edge(adapt_cell, v0, parent_edge_id, parent_t0) + && vertex_parameter_on_parent_edge(adapt_cell, v1, parent_edge_id, parent_t1)) + { + parent_dim = 1; + parent_id = parent_edge_id; + parent_param.assign(1, (T(1) - t) * parent_t0 + t * parent_t1); + } + + return append_vertex_with_parent_info( + adapt_cell, + std::span(x), + parent_dim, + parent_id, + std::span(parent_param), + edge_id); } template @@ -162,7 +263,13 @@ int append_cell_center_vertex(AdaptCell& adapt_cell, std::span(d)] += adapt_cell.vertex_coords[static_cast(v * adapt_cell.tdim + d)] * inv; } - return append_vertex(adapt_cell, std::span(x)); + return append_vertex_with_parent_info( + adapt_cell, + std::span(x), + adapt_cell.tdim, + adapt_cell.parent_cell_id, + std::span(x), + -1); } template @@ -426,11 +533,6 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, if (split_edge < 0) return false; - auto ev = adapt_cell.entity_to_vertex[1][static_cast(split_edge)]; - const int v0 = ev[0]; - const int v1 = ev[1]; - const int new_v = append_interpolated_vertex(adapt_cell, v0, v1, split_t); - const int tdim = adapt_cell.tdim; const std::vector old_types = adapt_cell.entity_types[tdim]; const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; @@ -448,6 +550,11 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, return false; } + auto ev = adapt_cell.entity_to_vertex[1][static_cast(split_edge)]; + const int v0 = ev[0]; + const int v1 = ev[1]; + const int new_v = append_interpolated_vertex_on_edge(adapt_cell, split_edge, split_t); + EntityAdjacency new_cells; new_cells.offsets.push_back(0); std::vector new_types; @@ -589,8 +696,7 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, if (it != midpoint_vertex_by_edge.end()) return it->second; - auto ev = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; - const int mid = append_interpolated_vertex(adapt_cell, ev[0], ev[1], T(0.5)); + const int mid = append_interpolated_vertex_on_edge(adapt_cell, edge_id, T(0.5)); midpoint_vertex_by_edge[edge_id] = mid; return mid; }; diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index b5db3c3..d8032d9 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -256,14 +256,18 @@ def _load_cpp_module(): box_hex_mesh, ) -from .triangulation_analysis import ( - CellCase, - analyze_all_cases, - analyze_single_cell_case, - classify_new_triangulation_edges, - classify_new_triangulation_simplices, - enumerate_cases, - single_cell_level_set, - single_cell_mesh, - summarize_analysis, -) +try: + from .triangulation_analysis import ( + CellCase, + analyze_all_cases, + analyze_single_cell_case, + classify_new_triangulation_edges, + classify_new_triangulation_simplices, + enumerate_cases, + single_cell_level_set, + single_cell_mesh, + summarize_analysis, + ) +except ModuleNotFoundError as exc: + if exc.name != f"{__name__}.triangulation_analysis": + raise diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 5c010bd..d16cebd 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #include @@ -48,6 +50,12 @@ using namespace cutcells; namespace { +template +T default_sqrt_epsilon_tol() +{ + return std::sqrt(std::numeric_limits::epsilon()); +} + const std::string& cell_domain_to_str(cell::domain domain_id) { static const std::map type_to_name @@ -1903,12 +1911,14 @@ void declare_ho_cut(nb::module_& m, const std::string& type) "curved_zero_nodes", [](HOCutResult& self, int geometry_order, - const std::string& node_family) + const std::string& node_family, + T small_entity_tol) { cutcells::curving::CurvingOptions options; options.geometry_order = geometry_order; options.node_family = cutcells::curving::node_family_from_string(node_family); + options.small_entity_tol = small_entity_tol; try { @@ -2076,7 +2086,8 @@ void declare_ho_cut(nb::module_& m, const std::string& type) "boundary_edge_failed", "projection_failed", "closest_face_retry_failed", - "constrained_newton_failed"}) + "constrained_newton_failed", + "small_entity_kept_straight"}) failure_code_names.append(name); result["failure_code_names"] = std::move(failure_code_names); @@ -2095,6 +2106,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) }, nb::arg("geometry_order") = 2, nb::arg("node_family") = "gll", + nb::arg("small_entity_tol") = default_sqrt_epsilon_tol(), "Return cached curved zero-entity nodes in parent reference coordinates."); // --- HOMeshPart --- @@ -2262,6 +2274,46 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal) + .def_prop_ro( + "vertex_parent_dim", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_parent_dim.data(), + {self.vertex_parent_dim.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "vertex_parent_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_parent_id.data(), + {self.vertex_parent_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "vertex_parent_param", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_parent_param.data(), + {self.vertex_parent_param.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "vertex_parent_param_offset", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.vertex_parent_param_offset.data(), + {self.vertex_parent_param_offset.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) .def_prop_ro( "zero_mask_per_vertex", [](const AdaptCellT& self) diff --git a/python/demo/demo_sphere_p2_cut_vtu.py b/python/demo/demo_sphere_p2_cut_vtu.py new file mode 100644 index 0000000..98322b8 --- /dev/null +++ b/python/demo/demo_sphere_p2_cut_vtu.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: cut one sphere level set and write a P2 Lagrange VTU. + +The demo builds a structured tetrahedral MeshView, interpolates one sphere +level set with P=2 nodes, cuts the mesh with CutCells, and writes the curved +P2 zero interface as a higher-order VTU file. The cut path uses +triangulation=true by default. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np + +import cutcells + + +P = 2 +VTK_TETRA = 10 +DEFAULT_CENTER = np.array([0.15, -0.1, 0.05], dtype=np.float64) +DEFAULT_RADIUS = 0.55 + + +def sphere_phi(X: np.ndarray, center: np.ndarray, radius: float) -> np.ndarray: + return np.sqrt( + (X[0] - center[0]) ** 2 + + (X[1] - center[1]) ** 2 + + (X[2] - center[2]) ** 2 + ) - radius + + +def structured_tetra_mesh( + x0: float, + y0: float, + z0: float, + x1: float, + y1: float, + z1: float, + nx: int, + ny: int, + nz: int, +) -> cutcells.MeshView: + xs = np.linspace(x0, x1, num=nx) + ys = np.linspace(y0, y1, num=ny) + zs = np.linspace(z0, z1, num=nz) + xx, yy, zz = np.meshgrid(xs, ys, zs, indexing="ij") + points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()].astype(np.float64, copy=False) + + def vertex_id(i: int, j: int, k: int) -> int: + return i + nx * (j + ny * k) + + tets: list[list[int]] = [] + for k in range(nz - 1): + for j in range(ny - 1): + for i in range(nx - 1): + v000 = vertex_id(i, j, k) + v100 = vertex_id(i + 1, j, k) + v010 = vertex_id(i, j + 1, k) + v110 = vertex_id(i + 1, j + 1, k) + v001 = vertex_id(i, j, k + 1) + v101 = vertex_id(i + 1, j, k + 1) + v011 = vertex_id(i, j + 1, k + 1) + v111 = vertex_id(i + 1, j + 1, k + 1) + + tets.extend( + ( + [v000, v100, v110, v111], + [v000, v110, v010, v111], + [v000, v010, v011, v111], + [v000, v011, v001, v111], + [v000, v001, v101, v111], + [v000, v101, v100, v111], + ) + ) + + connectivity = np.asarray(tets, dtype=np.int32).reshape(-1) + offsets = np.arange(0, connectivity.size + 1, 4, dtype=np.int32) + cell_types = np.full(offsets.size - 1, VTK_TETRA, dtype=np.int32) + + for cell_id in range(cell_types.size): + start = int(offsets[cell_id]) + tet = connectivity[start : start + 4].copy() + vertices = points[tet] + jacobian = np.column_stack( + ( + vertices[1] - vertices[0], + vertices[2] - vertices[0], + vertices[3] - vertices[0], + ) + ) + if np.linalg.det(jacobian) < 0.0: + connectivity[start + 1], connectivity[start + 2] = ( + connectivity[start + 2], + connectivity[start + 1], + ) + + return cutcells.MeshView(points, connectivity, offsets, cell_types, tdim=3) + + +def write_background_mesh(path: Path, mesh: cutcells.MeshView) -> None: + coordinates = np.ascontiguousarray(np.asarray(mesh.coordinates).reshape(-1)) + connectivity = np.ascontiguousarray(np.asarray(mesh.connectivity), dtype=np.int32) + offsets = np.ascontiguousarray(np.asarray(mesh.offsets), dtype=np.int32) + cell_types = np.ascontiguousarray(np.asarray(mesh.cell_types), dtype=np.int32) + + cutcells.write_vtk( + str(path), + coordinates, + connectivity, + offsets, + cell_types, + gdim=mesh.gdim, + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Cut one sphere level set and write a P2 Lagrange VTU." + ) + parser.add_argument( + "--n", + type=int, + default=7, + help="Number of grid points per coordinate direction.", + ) + parser.add_argument( + "--center", + type=float, + nargs=3, + default=DEFAULT_CENTER.tolist(), + metavar=("X", "Y", "Z"), + help="Sphere center.", + ) + parser.add_argument( + "--radius", + type=float, + default=DEFAULT_RADIUS, + help="Sphere radius.", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("demo_sphere_p2_cut_vtu_output"), + help="Directory for generated VTU files.", + ) + parser.add_argument( + "--node-family", + choices=["lagrange", "gll"], + default="gll", + help="Construction/output nodes for the P2 geometry.", + ) + parser.add_argument( + "--write-domains", + action="store_true", + help="Also write P2 VTUs for phi < 0 and phi > 0 on cut cells.", + ) + parser.add_argument( + "--triangulate", + action=argparse.BooleanOptionalAction, + default=True, + help="Triangulate cut parts before extracting VTU output.", + ) + args = parser.parse_args() + + if args.n < 2: + raise SystemExit("--n must be at least 2") + if args.radius <= 0.0: + raise SystemExit("--radius must be positive") + + center = np.asarray(args.center, dtype=np.float64) + args.output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Building structured tetrahedral mesh with n={args.n} ...") + mesh = structured_tetra_mesh( + -1.0, + -1.0, + -1.0, + 1.0, + 1.0, + 1.0, + args.n, + args.n, + args.n, + ) + print(f" cells={mesh.num_cells()}, points={mesh.num_nodes()}") + + background_path = args.output_dir / "sphere_background_mesh.vtu" + write_background_mesh(background_path, mesh) + print(f" wrote {background_path}") + + print("Interpolating one P2 sphere level set ...") + ls = cutcells.create_level_set( + mesh, + lambda X: sphere_phi(X, center, args.radius), + degree=P, + name="phi", + ) + print(f" level-set dofs={ls.mesh_data.num_dofs()}") + + print(f"Running CutCells cut(mesh, ls, triangulate={args.triangulate}) ...") + result = cutcells.cut(mesh, ls, triangulate=args.triangulate) + negative = result["phi < 0"] + interface = result["phi = 0"] + positive = result["phi > 0"] + + print(f" num_cut_cells={result.num_cut_cells}") + print(f" phi < 0 : cut={negative.num_cut_cells}, uncut={negative.num_uncut_cells}") + print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") + print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") + + if interface.num_cut_cells == 0: + raise RuntimeError( + "The sphere does not cut the mesh. Adjust --center, --radius, or --n." + ) + + suffix = "_triangulated" if args.triangulate else "" + interface_path = args.output_dir / f"sphere_interface{suffix}_p2.vtu" + interface.write_vtu( + str(interface_path), + mode="cut_only", + geometry_order=P, + node_family=args.node_family, + ) + print(f" wrote {interface_path}") + + if args.write_domains: + inside_path = args.output_dir / f"sphere_inside_cut_only{suffix}_p2.vtu" + outside_path = args.output_dir / f"sphere_outside_cut_only{suffix}_p2.vtu" + negative.write_vtu( + str(inside_path), + mode="cut_only", + geometry_order=P, + node_family=args.node_family, + ) + positive.write_vtu( + str(outside_path), + mode="cut_only", + geometry_order=P, + node_family=args.node_family, + ) + print(f" wrote {inside_path}") + print(f" wrote {outside_path}") + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index 5d14f93..2bd285a 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -292,6 +292,72 @@ def test_cut_overload_supports_mesh_part_selection(self): self.assertGreater(adapt.num_cells(), 0) self.assertGreater(adapt.num_vertices(), 3) + def test_curving_accepts_near_zero_multi_level_set_node_without_projection(self): + mesh = _single_triangle_mesh() + + def phi(X): + return X[0] + X[1] - 0.25 + 5.12e-11 * X[0] * X[1] + + ls0 = cutcells.create_level_set(mesh, phi, degree=2, name="phi") + ls1 = cutcells.create_level_set(mesh, phi, degree=2, name="psi") + + result = cutcells.ho_cut(mesh, [ls0, ls1]) + curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") + + status = np.asarray(curved["status"], dtype=np.uint8) + dim = np.asarray(curved["dim"], dtype=np.int32) + zero_mask = np.asarray(curved["zero_mask"], dtype=np.uint64) + offsets = np.asarray(curved["offsets"], dtype=np.int32) + stats_offsets = np.asarray(curved["stats_offsets"], dtype=np.int32) + node_iterations = np.asarray(curved["node_iterations"], dtype=np.int32) + node_residual = np.asarray(curved["node_residual"], dtype=np.float64) + node_projection_mode = np.asarray(curved["node_projection_mode"], dtype=np.uint8) + points = np.asarray(curved["points"], dtype=np.float64) + + self.assertTrue(np.all(status == 2)) # CurvingStatus::curved + edge_state = int(np.flatnonzero((dim == 1) & (zero_mask == 3))[0]) + node_begin = int(offsets[edge_state]) + node_end = int(offsets[edge_state + 1]) + stat_begin = int(stats_offsets[edge_state]) + stat_end = int(stats_offsets[edge_state + 1]) + + self.assertEqual(node_end - node_begin, 3) + self.assertEqual(stat_end - stat_begin, 3) + mid_node = node_begin + 1 + mid_stat = stat_begin + 1 + + np.testing.assert_allclose(points[mid_node], [0.125, 0.125], atol=1e-15) + self.assertLess(node_residual[mid_stat], 1.0e-12) + self.assertEqual(int(node_iterations[mid_stat]), 0) + self.assertEqual(int(node_projection_mode[mid_stat]), 0) # CurvingProjectionMode::none + + def test_curving_keeps_only_short_edge_faces_straight(self): + mesh = _single_tetra_mesh() + eps = 1.0e-4 + + def phi(X): + return X[0] + X[1] - eps + + ls = cutcells.create_level_set(mesh, phi, degree=1, name="phi") + result = cutcells.ho_cut(mesh, ls) + curved = result.curved_zero_nodes( + geometry_order=2, + node_family="gll", + small_entity_tol=2.0e-2, + ) + + dim = np.asarray(curved["dim"], dtype=np.int32) + stats_offsets = np.asarray(curved["stats_offsets"], dtype=np.int32) + node_failure_code = np.asarray(curved["node_failure_code"], dtype=np.uint8) + small_code = list(curved["failure_code_names"]).index("small_entity_kept_straight") + + face_states = np.flatnonzero(dim == 2) + self.assertGreater(len(face_states), 0) + for face_state in face_states: + begin = int(stats_offsets[face_state]) + end = int(stats_offsets[face_state + 1]) + self.assertFalse(np.all(node_failure_code[begin:end] == small_code)) + def test_cut_detects_quadratic_interior_tetra_intersection(self): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( diff --git a/python/tests/test_ho_part_straight_output.py b/python/tests/test_ho_part_straight_output.py index 6f37a48..3214828 100644 --- a/python/tests/test_ho_part_straight_output.py +++ b/python/tests/test_ho_part_straight_output.py @@ -71,6 +71,45 @@ def _edge_counts(cut_mesh): return counts +def _assert_zero_edges_have_only_zero_vertices(adapt_cell): + zero_masks = np.asarray(adapt_cell.zero_mask_per_vertex, dtype=np.uint64) + edge_connectivity = np.asarray(adapt_cell.edge_connectivity, dtype=np.int32) + edge_offsets = np.asarray(adapt_cell.edge_offsets, dtype=np.int32) + zero_entity_dim = np.asarray(adapt_cell.zero_entity_dim, dtype=np.uint8) + zero_entity_id = np.asarray(adapt_cell.zero_entity_id, dtype=np.int32) + + for dim, entity_id in zip(zero_entity_dim, zero_entity_id): + if int(dim) != 1: + continue + verts = edge_connectivity[edge_offsets[entity_id] : edge_offsets[entity_id + 1]] + assert all(zero_masks[vertex_id] & 1 for vertex_id in verts) + + +def _assert_vertex_provenance_is_populated(adapt_cell, parent_cell_id: int): + parent_dim = np.asarray(adapt_cell.vertex_parent_dim, dtype=np.int8) + parent_id = np.asarray(adapt_cell.vertex_parent_id, dtype=np.int32) + parent_offsets = np.asarray(adapt_cell.vertex_parent_param_offset, dtype=np.int32) + parent_params = np.asarray(adapt_cell.vertex_parent_param, dtype=np.float64) + + assert len(parent_dim) == adapt_cell.num_vertices() + assert len(parent_id) == adapt_cell.num_vertices() + assert len(parent_offsets) == adapt_cell.num_vertices() + 1 + + for vertex_id, dim in enumerate(parent_dim): + dim = int(dim) + begin = int(parent_offsets[vertex_id]) + end = int(parent_offsets[vertex_id + 1]) + params = parent_params[begin:end] + assert dim >= 0 + assert int(parent_id[vertex_id]) >= 0 + assert len(params) == dim + if dim == adapt_cell.tdim: + assert int(parent_id[vertex_id]) == parent_cell_id + if dim == 1: + assert np.all(params >= -1.0e-12) + assert np.all(params <= 1.0 + 1.0e-12) + + def test_triangle_lut_triangulated_quad_connects_uncut_edge_vertices_to_adjacent_roots(): vertex_coordinates = np.array( [ @@ -147,6 +186,96 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): assert "Name=\"types\" format=\"ascii\">9 " in interface_path.read_text() +def test_triangulated_tetra_prism_midpoints_keep_masks_and_source_edges(): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.6, + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls, triangulate=True) + adapt_cell = result.adapt_cell(0) + parent_cell_id = int(np.asarray(result.parent_cell_ids, dtype=np.int32)[0]) + + source_edges = np.asarray(adapt_cell.vertex_source_edge_id, dtype=np.int32) + zero_masks = np.asarray(adapt_cell.zero_mask_per_vertex, dtype=np.uint64) + negative_masks = np.asarray(adapt_cell.negative_mask_per_vertex, dtype=np.uint64) + vertex_coords = np.asarray(adapt_cell.vertex_coords, dtype=np.float64) + + # The first four vertices are the original tetra vertices. All vertices + # inserted by one-root localization or prism triangulation must retain an + # originating leaf edge so subsequent topology updates can preserve masks. + assert np.all(source_edges[4:] >= 0) + _assert_vertex_provenance_is_populated(adapt_cell, parent_cell_id) + + phi = vertex_coords[:, 0] + vertex_coords[:, 1] - 0.6 + for vertex_id in range(vertex_coords.shape[0]): + is_zero = bool(zero_masks[vertex_id] & 1) + is_negative = bool(negative_masks[vertex_id] & 1) + assert not (is_zero and is_negative) + if not is_zero: + assert is_negative == bool(phi[vertex_id] < 0.0) + + _assert_zero_edges_have_only_zero_vertices(adapt_cell) + + +def test_triangulated_triangle_quad_midpoint_keeps_masks_and_parent_edge(): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: 0.1 - 0.2 * X[0] - 0.1 * X[1], + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls, triangulate=True) + adapt_cell = result.adapt_cell(0) + parent_cell_id = int(np.asarray(result.parent_cell_ids, dtype=np.int32)[0]) + + source_edges = np.asarray(adapt_cell.vertex_source_edge_id, dtype=np.int32) + parent_dim = np.asarray(adapt_cell.vertex_parent_dim, dtype=np.int8) + parent_id = np.asarray(adapt_cell.vertex_parent_id, dtype=np.int32) + parent_offsets = np.asarray(adapt_cell.vertex_parent_param_offset, dtype=np.int32) + parent_params = np.asarray(adapt_cell.vertex_parent_param, dtype=np.float64) + zero_masks = np.asarray(adapt_cell.zero_mask_per_vertex, dtype=np.uint64) + negative_masks = np.asarray(adapt_cell.negative_mask_per_vertex, dtype=np.uint64) + vertex_coords = np.asarray(adapt_cell.vertex_coords, dtype=np.float64) + + # Vertices 3 and 4 are one-root vertices; vertex 5 is the triangulation + # midpoint on the uncut parent edge of the triangle-derived quadrilateral. + assert np.all(source_edges[3:] >= 0) + assert int(parent_dim[5]) == 1 + assert int(parent_id[5]) == int(source_edges[5]) + begin = int(parent_offsets[5]) + end = int(parent_offsets[6]) + np.testing.assert_allclose(parent_params[begin:end], [0.5]) + _assert_vertex_provenance_is_populated(adapt_cell, parent_cell_id) + + ls_cell = cutcells.make_cell_level_set(ls, parent_cell_id) + phi = np.array( + [ + cutcells.evaluate_bernstein( + ls_cell.cell_type, + ls_cell.bernstein_order, + np.asarray(ls_cell.bernstein_coeffs), + vertex_coords[vertex_id], + ) + for vertex_id in range(vertex_coords.shape[0]) + ], + dtype=np.float64, + ) + for vertex_id in range(vertex_coords.shape[0]): + is_zero = bool(zero_masks[vertex_id] & 1) + is_negative = bool(negative_masks[vertex_id] & 1) + assert not (is_zero and is_negative) + if not is_zero: + assert is_negative == bool(phi[vertex_id] < 0.0) + + _assert_zero_edges_have_only_zero_vertices(adapt_cell) + + def test_interface_output_reflects_adaptcell_quad_leaf(): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( From 549ee311880a692f771d7ea913d98bafd7fcb714 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Mon, 4 May 2026 19:29:43 +0200 Subject: [PATCH 17/23] graph and jacobian checks in place, refinement currently brings no improvement --- cpp/src/CMakeLists.txt | 2 + cpp/src/cell_certification.cpp | 2791 ++++++++++++++++- cpp/src/cell_certification.h | 195 ++ cpp/src/curving.cpp | 1499 +++++++-- cpp/src/curving.h | 55 + cpp/src/ho_cut_mesh.cpp | 117 +- cpp/src/ho_cut_mesh.h | 11 +- cpp/src/ho_mesh_part_output.cpp | 200 +- cpp/src/ho_mesh_part_output.h | 24 +- cpp/src/level_set_cell.cpp | 61 +- python/cutcells/__init__.py | 8 + python/cutcells/wrapper.cpp | 995 +++++- python/pyproject.toml | 1 + python/tests/test_certification_refinement.py | 253 +- 14 files changed, 5890 insertions(+), 322 deletions(-) diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 5ee1c3e..292b8c7 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -39,6 +39,8 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.h ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.h ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.h + ${CMAKE_CURRENT_SOURCE_DIR}/geometric_quantity.h + ${CMAKE_CURRENT_SOURCE_DIR}/tangent_shift.h ${CMAKE_CURRENT_SOURCE_DIR}/refine_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature_tables.h diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index c7adff5..7f8ba04 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -7,10 +7,13 @@ #include "bernstein.h" #include "cell_flags.h" #include "cell_topology.h" +#include "curving.h" #include "cut_cell.h" #include "cut_tetrahedron.h" #include "cut_triangle.h" #include "edge_certification.h" +#include "geometric_quantity.h" +#include "mapping.h" #include "reference_cell.h" #include "refine_cell.h" @@ -22,6 +25,7 @@ #include #include #include +#include namespace cutcells { @@ -1345,6 +1349,2320 @@ void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, } } +template +std::span point_span(const std::vector& points, + int point_id, + int dim) +{ + return std::span( + points.data() + + static_cast(point_id) + * static_cast(dim), + static_cast(dim)); +} + +template +geom::ParentEntity common_parent_entity_for_points( + cell::type parent_cell_type, + const std::vector& points, + int dim, + T tol) +{ + const int npoints = static_cast( + points.size() / static_cast(dim)); + + if (npoints == 0) + return {-1, -1}; + + const auto edges = cell::edges(parent_cell_type); + for (int e = 0; e < static_cast(edges.size()); ++e) + { + bool all_on_edge = true; + for (int i = 0; i < npoints; ++i) + all_on_edge = all_on_edge + && geom::point_on_parent_edge( + parent_cell_type, e, point_span(points, i, dim), tol); + if (all_on_edge) + return {1, e}; + } + + if (cell::get_tdim(parent_cell_type) == 3) + { + for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) + { + bool all_on_face = true; + for (int i = 0; i < npoints; ++i) + all_on_face = all_on_face + && geom::point_on_parent_face( + parent_cell_type, f, point_span(points, i, dim), tol); + if (all_on_face) + return {2, f}; + } + } + + return {cell::get_tdim(parent_cell_type), -1}; +} + +template +geom::ParentEntity common_parent_face_or_cell_for_points( + cell::type parent_cell_type, + const std::vector& points, + int dim, + T tol) +{ + const int npoints = static_cast( + points.size() / static_cast(dim)); + if (cell::get_tdim(parent_cell_type) == 3) + { + for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) + { + bool all_on_face = true; + for (int i = 0; i < npoints; ++i) + all_on_face = all_on_face + && geom::point_on_parent_face( + parent_cell_type, f, point_span(points, i, dim), tol); + if (all_on_face) + return {2, f}; + } + } + return {cell::get_tdim(parent_cell_type), -1}; +} + +template +void mark_graph_failure(ReadyCellGraphDiagnostics& diagnostics, + int cell_id, + graph_criteria::FailureReason reason) +{ + diagnostics.accepted = false; + ++diagnostics.failed_checks; + if (diagnostics.first_failed_cell < 0) + { + diagnostics.first_failed_cell = cell_id; + diagnostics.first_failure_reason = reason; + } +} + +template +void mark_graph_refinement_request(ReadyCellGraphDiagnostics& diagnostics, + int entity_dim, + int entity_id) +{ + if (entity_dim < 0 || entity_id < 0) + return; + if (diagnostics.first_requested_refinement_entity_dim < 0) + { + diagnostics.first_requested_refinement_entity_dim = entity_dim; + diagnostics.first_requested_refinement_entity_id = entity_id; + } +} + +template +void observe_graph_direction(ReadyCellGraphDiagnostics& diagnostics, + const graph_criteria::DirectionReport& report) +{ + diagnostics.min_true_transversality = + std::min(diagnostics.min_true_transversality, + report.metrics.true_transversality); + diagnostics.min_host_normal_alignment = + std::min(diagnostics.min_host_normal_alignment, + report.metrics.host_normal_alignment); + diagnostics.max_drift_amplification = + std::max(diagnostics.max_drift_amplification, + report.metrics.drift_amplification); + diagnostics.max_relative_correction_distance = + std::max(diagnostics.max_relative_correction_distance, + report.metrics.relative_correction_distance); + diagnostics.max_relative_tangential_shift = + std::max(diagnostics.max_relative_tangential_shift, + report.metrics.relative_tangential_shift); +} + +template +void observe_graph_node(ReadyCellGraphDiagnostics& diagnostics, + const GraphNodeDiagnostics& node) +{ + diagnostics.nodes.push_back(node); + if (std::isfinite(node.level_set_gradient_host_alignment)) + { + diagnostics.min_level_set_gradient_host_alignment = + std::min(diagnostics.min_level_set_gradient_host_alignment, + node.level_set_gradient_host_alignment); + } + if (!node.accepted) + { + mark_graph_refinement_request( + diagnostics, + node.requested_refinement_entity_dim, + node.requested_refinement_entity_id); + } +} + +template +void observe_failed_graph_projection( + ReadyCellGraphDiagnostics& diagnostics, + const curving::ProjectionDiagnostic& projection) +{ + if (!diagnostics.first_failed_projection_seed.empty()) + return; + diagnostics.first_failed_projection_seed = projection.seed; + diagnostics.first_failed_projection_direction = projection.direction; + diagnostics.first_failed_projection_clip_lo = projection.clip_lo; + diagnostics.first_failed_projection_clip_hi = projection.clip_hi; + diagnostics.first_failed_projection_root_t = projection.root_t; +} + +template +void observe_failed_face_orientation( + ReadyCellGraphDiagnostics& diagnostics, + const graph_criteria::FaceQualityReport& quality) +{ + if (diagnostics.first_failed_face_triangle_index < 0) + { + diagnostics.first_failed_face_triangle_index = + quality.failed_triangle_index; + diagnostics.first_failed_face_area_ratio = + quality.failed_surface_jacobian_ratio; + } +} + +template +int find_adapt_edge_by_vertices(const AdaptCell& adapt_cell, int a, int b) +{ + if (a < 0 || b < 0) + return -1; + const int n_edges = adapt_cell.n_entities(1); + for (int e = 0; e < n_edges; ++e) + { + auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; + if (ev.size() != 2) + continue; + const int e0 = static_cast(ev[0]); + const int e1 = static_cast(ev[1]); + if ((e0 == a && e1 == b) || (e0 == b && e1 == a)) + return e; + } + return -1; +} + +template +int graph_refinement_edge_from_failed_face_segment( + const AdaptCell& adapt_cell, + int a, + int b) +{ + int edge_id = find_adapt_edge_by_vertices(adapt_cell, a, b); + if (edge_id >= 0) + return edge_id; + + auto source_edge = [&](int v) -> int + { + if (v < 0 + || v >= static_cast(adapt_cell.vertex_source_edge_id.size())) + { + return -1; + } + const int source = adapt_cell.vertex_source_edge_id[static_cast(v)]; + return (source >= 0 && source < adapt_cell.n_entities(1)) ? source : -1; + }; + + edge_id = source_edge(a); + if (edge_id >= 0) + return edge_id; + return source_edge(b); +} + +template +T graph_alignment_to_host_normal(std::span direction, + std::span host_normal, + T tol) +{ + if (direction.size() != host_normal.size()) + return std::numeric_limits::quiet_NaN(); + const auto alignment = geom::alignment(direction, host_normal, tol); + if (alignment.degenerate) + return T(0); + return std::fabs(alignment.cosine); +} + +template +T graph_angle_to_tangent_degrees(T host_alignment) +{ + if (!std::isfinite(host_alignment)) + return std::numeric_limits::quiet_NaN(); + const T clipped = std::clamp(host_alignment, T(0), T(1)); + const T pi = std::acos(T(-1)); + return std::asin(clipped) * T(180) / pi; +} + +template +bool solve_graph_dense_small(std::vector A, + std::vector b, + int n, + std::vector& x, + T tol) +{ + if (n <= 0 || static_cast(A.size()) != n * n + || static_cast(b.size()) != n) + { + return false; + } + + for (int k = 0; k < n; ++k) + { + int pivot = k; + T pivot_abs = std::fabs(A[static_cast(k * n + k)]); + for (int i = k + 1; i < n; ++i) + { + const T candidate = + std::fabs(A[static_cast(i * n + k)]); + if (candidate > pivot_abs) + { + pivot = i; + pivot_abs = candidate; + } + } + if (pivot_abs <= tol) + return false; + + if (pivot != k) + { + for (int j = k; j < n; ++j) + { + std::swap(A[static_cast(k * n + j)], + A[static_cast(pivot * n + j)]); + } + std::swap(b[static_cast(k)], + b[static_cast(pivot)]); + } + + const T diag = A[static_cast(k * n + k)]; + for (int i = k + 1; i < n; ++i) + { + const T factor = A[static_cast(i * n + k)] / diag; + A[static_cast(i * n + k)] = T(0); + for (int j = k + 1; j < n; ++j) + { + A[static_cast(i * n + j)] -= + factor * A[static_cast(k * n + j)]; + } + b[static_cast(i)] -= + factor * b[static_cast(k)]; + } + } + + x.assign(static_cast(n), T(0)); + for (int i = n - 1; i >= 0; --i) + { + T value = b[static_cast(i)]; + for (int j = i + 1; j < n; ++j) + value -= A[static_cast(i * n + j)] + * x[static_cast(j)]; + const T diag = A[static_cast(i * n + i)]; + if (std::fabs(diag) <= tol) + return false; + x[static_cast(i)] = value / diag; + } + return true; +} + +template +bool graph_affine_jacobian(const LevelSetCell& ls_cell, + std::vector& jacobian) +{ + const int tdim = ls_cell.tdim; + const int gdim = ls_cell.gdim; + if (tdim <= 0 || tdim > 3 || gdim <= 0 + || ls_cell.parent_vertex_coords.empty()) + { + return false; + } + + const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); + jacobian.assign(static_cast(gdim * tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + const int va = cols[static_cast(a)]; + if (va < 0) + return false; + for (int r = 0; r < gdim; ++r) + { + jacobian[static_cast(r * tdim + a)] = + ls_cell.parent_vertex_coords[ + static_cast(va * gdim + r)] + - ls_cell.parent_vertex_coords[static_cast(r)]; + } + } + return true; +} + +template +T graph_metric_alignment_to_host_normal( + const LevelSetCell& ls_cell, + std::span direction_ref, + const graph_criteria::HostFrame& host, + T tol) +{ + if (static_cast(direction_ref.size()) != ls_cell.tdim) + { + return std::numeric_limits::quiet_NaN(); + } + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return graph_alignment_to_host_normal( + direction_ref, + std::span(host.normal.data(), host.normal.size()), + tol); + + std::vector direction_phys(static_cast(ls_cell.gdim), T(0)); + for (int r = 0; r < ls_cell.gdim; ++r) + { + for (int a = 0; a < ls_cell.tdim; ++a) + { + const T j_ra = jacobian[static_cast( + r * ls_cell.tdim + a)]; + direction_phys[static_cast(r)] += + j_ra * direction_ref[static_cast(a)]; + } + } + + const T direction_norm = geom::norm( + std::span(direction_phys.data(), direction_phys.size())); + if (direction_norm <= tol) + return T(0); + + if (host.dimension == graph_criteria::HostDimension::edge) + { + if (static_cast(host.tangent.size()) != ls_cell.tdim) + return std::numeric_limits::quiet_NaN(); + + std::vector tangent_phys(static_cast(ls_cell.gdim), T(0)); + for (int r = 0; r < ls_cell.gdim; ++r) + { + for (int a = 0; a < ls_cell.tdim; ++a) + { + tangent_phys[static_cast(r)] += + jacobian[static_cast(r * ls_cell.tdim + a)] + * host.tangent[static_cast(a)]; + } + } + + const T tangent_norm = geom::norm( + std::span(tangent_phys.data(), tangent_phys.size())); + if (tangent_norm <= tol) + return std::numeric_limits::quiet_NaN(); + + const T cosine = geom::dot( + std::span(direction_phys.data(), direction_phys.size()), + std::span(tangent_phys.data(), tangent_phys.size())) + / (direction_norm * tangent_norm); + const T clipped = std::clamp(cosine, T(-1), T(1)); + return std::sqrt(std::max(T(0), T(1) - clipped * clipped)); + } + + if (static_cast(host.normal.size()) != ls_cell.tdim + || ls_cell.gdim != ls_cell.tdim) + { + return graph_alignment_to_host_normal( + direction_ref, + std::span(host.normal.data(), host.normal.size()), + tol); + } + + std::vector jt(static_cast(ls_cell.tdim * ls_cell.tdim), T(0)); + for (int a = 0; a < ls_cell.tdim; ++a) + { + for (int r = 0; r < ls_cell.gdim; ++r) + { + jt[static_cast(a * ls_cell.tdim + r)] = + jacobian[static_cast(r * ls_cell.tdim + a)]; + } + } + std::vector normal_phys; + std::vector rhs(host.normal.begin(), host.normal.end()); + if (!solve_graph_dense_small( + std::move(jt), std::move(rhs), ls_cell.tdim, normal_phys, tol)) + { + return graph_alignment_to_host_normal( + direction_ref, + std::span(host.normal.data(), host.normal.size()), + tol); + } + + return graph_alignment_to_host_normal( + std::span(direction_phys.data(), direction_phys.size()), + std::span(normal_phys.data(), normal_phys.size()), + tol); +} + +template +std::vector graph_pull_back_physical_direction( + std::span jacobian, + int gdim, + int tdim, + std::span direction_phys, + std::span fallback_ref, + T tol) +{ + std::vector gram(static_cast(tdim * tdim), T(0)); + std::vector rhs(static_cast(tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + for (int r = 0; r < gdim; ++r) + { + rhs[static_cast(a)] += + jacobian[static_cast(r * tdim + a)] + * direction_phys[static_cast(r)]; + } + for (int b = 0; b < tdim; ++b) + { + T value = T(0); + for (int r = 0; r < gdim; ++r) + { + value += jacobian[static_cast(r * tdim + a)] + * jacobian[static_cast(r * tdim + b)]; + } + gram[static_cast(a * tdim + b)] = value; + } + } + + std::vector out; + if (solve_graph_dense_small( + std::move(gram), std::move(rhs), tdim, out, tol)) + { + return out; + } + return std::vector(fallback_ref.begin(), fallback_ref.end()); +} + +template +std::vector graph_parent_entity_metric_gradient_direction( + const LevelSetCell& ls_cell, + geom::ParentEntity parent_entity, + std::span grad_ref, + T tol) +{ + const auto fallback = geom::restricted_level_set_gradient_in_parent_frame( + ls_cell.cell_type, parent_entity, grad_ref, tol); + std::vector fallback_ref = + fallback.value.empty() + ? std::vector(grad_ref.begin(), grad_ref.end()) + : fallback.value; + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return fallback_ref; + + const int tdim = ls_cell.tdim; + const int gdim = ls_cell.gdim; + + std::vector grad_phys; + if (gdim == tdim) + { + std::vector jt(static_cast(tdim * tdim), T(0)); + for (int a = 0; a < tdim; ++a) + { + for (int r = 0; r < gdim; ++r) + { + jt[static_cast(a * tdim + r)] = + jacobian[static_cast(r * tdim + a)]; + } + } + std::vector rhs(grad_ref.begin(), grad_ref.end()); + if (!solve_graph_dense_small( + std::move(jt), std::move(rhs), tdim, grad_phys, tol)) + { + return fallback_ref; + } + } + else + { + grad_phys.assign(static_cast(gdim), T(0)); + for (int r = 0; r < gdim; ++r) + { + for (int a = 0; a < tdim; ++a) + { + grad_phys[static_cast(r)] += + jacobian[static_cast(r * tdim + a)] + * fallback_ref[static_cast(a)]; + } + } + } + + if (parent_entity.dim == 2 && gdim == 3) + { + const auto face = cell::face_vertices(ls_cell.cell_type, parent_entity.id); + if (face.size() >= 3) + { + std::array e0 = {T(0), T(0), T(0)}; + std::array e1 = {T(0), T(0), T(0)}; + for (int d = 0; d < 3; ++d) + { + e0[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(face[1] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(face[0] * gdim + d)]; + e1[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(face[2] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(face[0] * gdim + d)]; + } + std::array normal = { + e0[1] * e1[2] - e0[2] * e1[1], + e0[2] * e1[0] - e0[0] * e1[2], + e0[0] * e1[1] - e0[1] * e1[0]}; + const T nn = normal[0] * normal[0] + + normal[1] * normal[1] + + normal[2] * normal[2]; + if (nn > tol * tol) + { + const T c = (grad_phys[0] * normal[0] + + grad_phys[1] * normal[1] + + grad_phys[2] * normal[2]) / nn; + for (int d = 0; d < 3; ++d) + grad_phys[static_cast(d)] -= c * normal[d]; + } + } + } + else if (parent_entity.dim == 1) + { + const auto edges = cell::edges(ls_cell.cell_type); + if (parent_entity.id >= 0 + && parent_entity.id < static_cast(edges.size())) + { + const auto edge = edges[static_cast(parent_entity.id)]; + std::vector tangent(static_cast(gdim), T(0)); + for (int d = 0; d < gdim; ++d) + { + tangent[static_cast(d)] = + ls_cell.parent_vertex_coords[ + static_cast(edge[1] * gdim + d)] + - ls_cell.parent_vertex_coords[ + static_cast(edge[0] * gdim + d)]; + } + const T tt = geom::dot( + std::span(tangent.data(), tangent.size()), + std::span(tangent.data(), tangent.size())); + if (tt > tol * tol) + { + const T c = geom::dot( + std::span(grad_phys.data(), grad_phys.size()), + std::span(tangent.data(), tangent.size())) / tt; + for (int d = 0; d < gdim; ++d) + grad_phys[static_cast(d)] = + c * tangent[static_cast(d)]; + } + } + } + else if (parent_entity.dim == 0) + { + return std::vector(static_cast(tdim), T(0)); + } + + auto pulled = graph_pull_back_physical_direction( + std::span(jacobian.data(), jacobian.size()), + gdim, + tdim, + std::span(grad_phys.data(), grad_phys.size()), + std::span(fallback_ref.data(), fallback_ref.size()), + tol); + const auto admissible = geom::admissible_direction_in_parent_frame( + ls_cell.cell_type, + parent_entity, + std::span(pulled.data(), pulled.size()), + tol); + return admissible.value.empty() ? pulled : admissible.value; +} + +template +curving::CurvingOptions graph_curving_options( + const ReadyCellGraphOptions& graph_options) +{ + curving::CurvingOptions options; + options.geometry_order = std::max(graph_options.geometry_order, 1); + options.direction_mode = + (graph_options.projection_mode + == GraphProjectionMode::straight_zero_entity_normal) + ? curving::CurvingDirectionMode::straight_zero_entity_normal + : curving::CurvingDirectionMode::level_set_gradient; + options.domain_tol = graph_options.criteria.tolerance; + options.active_face_tol = graph_options.criteria.tolerance; + return options; +} + +inline graph_criteria::FailureReason graph_failure_from_curving( + curving::CurvingFailureCode code) +{ + switch (code) + { + case curving::CurvingFailureCode::none: + case curving::CurvingFailureCode::exact_vertex: + case curving::CurvingFailureCode::boundary_from_edge: + case curving::CurvingFailureCode::small_entity_kept_straight: + return graph_criteria::FailureReason::none; + case curving::CurvingFailureCode::invalid_constraint_count: + case curving::CurvingFailureCode::missing_level_set_cell: + case curving::CurvingFailureCode::empty_zero_mask: + case curving::CurvingFailureCode::unsupported_entity: + case curving::CurvingFailureCode::missing_boundary_edge: + case curving::CurvingFailureCode::boundary_edge_failed: + return graph_criteria::FailureReason::invalid_input; + case curving::CurvingFailureCode::no_host_interval: + case curving::CurvingFailureCode::outside_host_domain: + return graph_criteria::FailureReason::root_segment_leaves_parent_entity; + case curving::CurvingFailureCode::singular_gradient_system: + return graph_criteria::FailureReason::degenerate_direction; + case curving::CurvingFailureCode::no_sign_changing_bracket: + case curving::CurvingFailureCode::brent_failed: + case curving::CurvingFailureCode::line_search_failed: + case curving::CurvingFailureCode::max_iterations: + case curving::CurvingFailureCode::projection_failed: + case curving::CurvingFailureCode::closest_face_retry_failed: + case curving::CurvingFailureCode::constrained_newton_failed: + return graph_criteria::FailureReason::root_not_on_search_line; + } + return graph_criteria::FailureReason::root_not_on_search_line; +} + +template +bool project_graph_point( + const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + geom::ParentEntity admissible_parent_entity, + const graph_criteria::HostFrame& host, + std::span host_point, + GraphNodeKind node_kind, + int node_index, + int requested_refinement_edge_id, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics, + int source_cell_id, + std::vector& corrected_point) +{ + GraphNodeDiagnostics node; + node.node_index = node_index; + node.node_kind = node_kind; + node.parent_entity_dim = admissible_parent_entity.dim; + node.parent_entity_id = admissible_parent_entity.id; + node.seed.assign(host_point.begin(), host_point.end()); + node.straight_helper_normal.assign(host.normal.begin(), host.normal.end()); + if (requested_refinement_edge_id >= 0) + { + node.requested_refinement_entity_dim = 1; + node.requested_refinement_entity_id = requested_refinement_edge_id; + } + node.selected_direction_kind = + (graph_options.projection_mode + == GraphProjectionMode::straight_zero_entity_normal) + ? graph_criteria::DirectionKind::projected_straight_host_normal + : graph_criteria::DirectionKind::projected_level_set_gradient; + + std::vector gradient(static_cast(ls_cell.tdim), T(0)); + curving::reference_level_set_gradient( + ls_cell, + host_point, + std::span(gradient.data(), gradient.size())); + auto graph_gradient_direction = + graph_parent_entity_metric_gradient_direction( + ls_cell, + admissible_parent_entity, + std::span(gradient.data(), gradient.size()), + graph_options.criteria.tolerance); + node.level_set_gradient_direction = graph_gradient_direction; + node.level_set_gradient_host_alignment = + graph_metric_alignment_to_host_normal( + ls_cell, + std::span( + graph_gradient_direction.data(), graph_gradient_direction.size()), + host, + graph_options.criteria.tolerance); + node.level_set_gradient_angle_to_tangent_deg = + graph_angle_to_tangent_degrees( + node.level_set_gradient_host_alignment); + if (std::isfinite(node.level_set_gradient_host_alignment) + && node.level_set_gradient_host_alignment + < graph_options.min_level_set_gradient_host_alignment) + { + node.accepted = false; + node.failure_reason = + graph_criteria::FailureReason::direction_too_tangential_to_host; + observe_graph_node(diagnostics, node); + mark_graph_failure( + diagnostics, source_cell_id, node.failure_reason); + return false; + } + + std::vector straight_normal_direction; + const auto admissible_normal = + geom::admissible_direction_in_parent_frame( + ls_cell.cell_type, + admissible_parent_entity, + std::span(host.normal.data(), host.normal.size()), + graph_options.criteria.tolerance); + if (!admissible_normal.degenerate()) + straight_normal_direction = admissible_normal.value; + + const T grad_norm = geom::norm( + std::span( + graph_gradient_direction.data(), graph_gradient_direction.size())); + const T normal_norm = geom::norm( + std::span( + straight_normal_direction.data(), straight_normal_direction.size())); + std::vector selected_policy_direction; + if (graph_options.projection_mode == GraphProjectionMode::level_set_gradient) + { + if (grad_norm > graph_options.criteria.tolerance) + { + selected_policy_direction = graph_gradient_direction; + node.selected_direction_kind = + graph_criteria::DirectionKind::projected_level_set_gradient; + } + else + { + selected_policy_direction = straight_normal_direction; + node.selected_direction_kind = + graph_criteria::DirectionKind::projected_straight_host_normal; + node.fallback_used = true; + } + } + else + { + if (normal_norm > graph_options.criteria.tolerance) + { + selected_policy_direction = straight_normal_direction; + node.selected_direction_kind = + graph_criteria::DirectionKind::projected_straight_host_normal; + } + else + { + selected_policy_direction = graph_gradient_direction; + node.selected_direction_kind = + graph_criteria::DirectionKind::projected_level_set_gradient; + node.fallback_used = true; + } + } + + const auto curving_options = graph_curving_options(graph_options); + auto projection = curving::project_seed_to_zero_entity_diagnostic( + adapt_cell, + local_zero_entity_id, + ls_cell, + host_point, + curving_options); + + if (!projection.accepted) + { + node.accepted = false; + node.failure_reason = graph_failure_from_curving(projection.failure_code); + node.selected_direction = selected_policy_direction; + node.corrected = projection.projected; + observe_graph_node(diagnostics, node); + observe_failed_graph_projection(diagnostics, projection); + mark_graph_failure(diagnostics, source_cell_id, node.failure_reason); + return false; + } + + corrected_point = std::move(projection.projected); + node.corrected = corrected_point; + if (corrected_point.size() != host_point.size()) + { + node.accepted = false; + node.failure_reason = graph_criteria::FailureReason::invalid_input; + observe_graph_node(diagnostics, node); + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + std::vector direction = std::move(selected_policy_direction); + if (geom::norm(std::span(direction.data(), direction.size())) + <= graph_options.criteria.tolerance) + { + direction.resize(host_point.size(), T(0)); + for (std::size_t d = 0; d < host_point.size(); ++d) + direction[d] = corrected_point[d] - host_point[d]; + } + + if (geom::norm(std::span(direction.data(), direction.size())) + <= graph_options.criteria.tolerance) + { + node.accepted = true; + node.failure_reason = graph_criteria::FailureReason::none; + node.selected_direction = direction; + observe_graph_node(diagnostics, node); + return true; + } + node.selected_direction = direction; + + auto report = graph_criteria::evaluate_direction( + ls_cell.cell_type, + admissible_parent_entity, + host, + host_point, + std::span( + graph_gradient_direction.data(), graph_gradient_direction.size()), + std::span(direction.data(), direction.size()), + std::span(corrected_point.data(), corrected_point.size()), + node.selected_direction_kind, + graph_options.criteria); + const T metric_selected_alignment = + graph_metric_alignment_to_host_normal( + ls_cell, + std::span(direction.data(), direction.size()), + host, + graph_options.criteria.tolerance); + if (std::isfinite(metric_selected_alignment)) + { + report.metrics.host_normal_alignment = metric_selected_alignment; + report.metrics.drift_amplification = + graph_criteria::drift_from_alignment( + metric_selected_alignment, + graph_options.criteria.tolerance); + if ((report.failure_reason + == graph_criteria::FailureReason::excessive_drift_amplification + || report.failure_reason + == graph_criteria::FailureReason::direction_too_tangential_to_host) + && report.metrics.drift_amplification + <= graph_options.criteria.max_drift_amplification + && report.metrics.host_normal_alignment + >= graph_options.criteria.min_host_normal_alignment) + { + report.accepted = true; + report.failure_reason = graph_criteria::FailureReason::none; + } + } + observe_graph_direction(diagnostics, report); + node.true_transversality = report.metrics.true_transversality; + node.selected_host_alignment = report.metrics.host_normal_alignment; + node.drift_amplification = report.metrics.drift_amplification; + node.relative_correction_distance = + report.metrics.relative_correction_distance; + node.relative_tangential_shift = + report.metrics.relative_tangential_shift; + if (!report.accepted) + { + if (report.failure_reason + == graph_criteria::FailureReason::root_not_on_search_line) + { + node.accepted = true; + node.failure_reason = graph_criteria::FailureReason::none; + observe_graph_node(diagnostics, node); + return true; + } + node.accepted = false; + node.failure_reason = report.failure_reason; + observe_graph_node(diagnostics, node); + observe_failed_graph_projection(diagnostics, projection); + mark_graph_failure( + diagnostics, source_cell_id, report.failure_reason); + return false; + } + node.accepted = true; + node.failure_reason = graph_criteria::FailureReason::none; + observe_graph_node(diagnostics, node); + return true; +} + +template +bool build_edge_host_frame(std::span a, + std::span b, + std::span zero_face_normal, + graph_criteria::HostFrame& host, + T tol) +{ + const auto tangent = geom::segment_tangent(a, b, true, tol); + if (tangent.degenerate()) + return false; + + host.dimension = graph_criteria::HostDimension::edge; + host.tangent = tangent.value; + host.h = tangent.norm; + + if (a.size() == 2) + { + const auto normal = geom::segment_normal(a, b, true, tol); + if (normal.degenerate()) + return false; + host.normal = normal.value; + return true; + } + + if (zero_face_normal.size() == 3) + { + const auto normal = geom::in_face_segment_normal( + a, b, zero_face_normal, true, tol); + if (normal.degenerate()) + return false; + host.normal = normal.value; + return true; + } + + return false; +} + +template +bool build_face_host_frame(const std::vector& face_vertices, + graph_criteria::HostFrame& host, + T tol) +{ + const int dim = 3; + const int nverts = static_cast( + face_vertices.size() / static_cast(dim)); + if (nverts < 3) + return false; + + const auto normal = geom::face_normal( + point_span(face_vertices, 0, dim), + point_span(face_vertices, 1, dim), + point_span(face_vertices, 2, dim), + true, + tol); + if (normal.degenerate()) + return false; + + T h = T(0); + for (int i = 0; i < nverts; ++i) + { + const auto a = point_span(face_vertices, i, dim); + const auto b = point_span(face_vertices, (i + 1) % nverts, dim); + h = std::max(h, geom::norm( + std::span(geom::subtract(b, a).data(), dim))); + } + + host.dimension = graph_criteria::HostDimension::face; + host.normal = normal.value; + host.tangent.clear(); + host.h = h; + return h > tol; +} + +template +bool check_zero_edge_graph(const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span a, + std::span b, + std::span zero_face_normal, + bool face_boundary_edge_check, + int requested_refinement_edge_id, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics, + int source_cell_id) +{ + ++diagnostics.checked_edges; + + std::vector host_points; + const int order = std::max(graph_options.geometry_order, 1); + for (int k = 0; k <= order; ++k) + { + const T s = T(k) / T(order); + for (std::size_t d = 0; d < a.size(); ++d) + host_points.push_back((T(1) - s) * a[d] + s * b[d]); + } + + const auto parent_entity = + (ls_cell.tdim == 3 && zero_face_normal.size() == 3) + ? common_parent_face_or_cell_for_points( + ls_cell.cell_type, + host_points, + ls_cell.tdim, + graph_options.criteria.tolerance) + : common_parent_entity_for_points( + ls_cell.cell_type, + host_points, + ls_cell.tdim, + graph_options.criteria.tolerance); + + std::vector frame_normal(zero_face_normal.begin(), zero_face_normal.end()); + if (ls_cell.tdim == 3 && parent_entity.dim == 2) + { + const auto parent_normal = geom::parent_face_normal( + ls_cell.cell_type, + parent_entity.id, + true, + graph_options.criteria.tolerance); + if (!parent_normal.degenerate()) + frame_normal = parent_normal.value; + } + + graph_criteria::HostFrame host; + if (!build_edge_host_frame( + a, + b, + std::span(frame_normal.data(), frame_normal.size()), + host, + graph_options.criteria.tolerance)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_host_frame); + return false; + } + + std::vector corrected_points; + corrected_points.reserve(host_points.size()); + for (int i = 0; i <= order; ++i) + { + if (i == 0 || i == order) + { + auto endpoint = point_span(host_points, i, ls_cell.tdim); + corrected_points.insert( + corrected_points.end(), endpoint.begin(), endpoint.end()); + continue; + } + + const GraphNodeKind node_kind = + face_boundary_edge_check + ? GraphNodeKind::face_boundary_edge + : GraphNodeKind::edge_interior; + std::vector corrected; + if (!project_graph_point( + adapt_cell, + local_zero_entity_id, + ls_cell, + parent_entity, + host, + point_span(host_points, i, ls_cell.tdim), + node_kind, + i, + requested_refinement_edge_id, + graph_options, + diagnostics, + source_cell_id, + corrected)) + { + return false; + } + corrected_points.insert( + corrected_points.end(), corrected.begin(), corrected.end()); + } + + const auto ordering = graph_criteria::evaluate_projected_edge_ordering( + std::span(host_points.data(), host_points.size()), + std::span(corrected_points.data(), corrected_points.size()), + ls_cell.tdim, + std::span(host.tangent.data(), host.tangent.size()), + host.h, + graph_options.criteria); + diagnostics.min_edge_gap_ratio = + std::min(diagnostics.min_edge_gap_ratio, ordering.minimum_gap_ratio); + if (!ordering.accepted) + { + mark_graph_failure( + diagnostics, source_cell_id, ordering.failure_reason); + return false; + } + + return true; +} + +template +std::vector graph_push_forward_vector(const std::vector& jacobian, + int gdim, + int tdim, + std::span vector_ref) +{ + std::vector out(static_cast(gdim), T(0)); + if (static_cast(vector_ref.size()) != tdim + || static_cast(jacobian.size()) != gdim * tdim) + { + return out; + } + for (int r = 0; r < gdim; ++r) + { + for (int a = 0; a < tdim; ++a) + { + out[static_cast(r)] += + jacobian[static_cast(r * tdim + a)] + * vector_ref[static_cast(a)]; + } + } + return out; +} + +template +std::pair graph_legendre_value_and_derivative(int order, T x) +{ + if (order == 0) + return {T(1), T(0)}; + if (order == 1) + return {x, T(1)}; + + T pm2 = T(1); + T pm1 = x; + for (int n = 2; n <= order; ++n) + { + const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); + pm2 = pm1; + pm1 = p; + } + + const T denom = T(1) - x * x; + if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) + return {pm1, T(0)}; + return {pm1, T(order) * (pm2 - x * pm1) / denom}; +} + +template +std::vector graph_gll_parameters(int order) +{ + order = std::max(order, 1); + std::vector params(static_cast(order + 1), T(0)); + params.front() = T(0); + params.back() = T(1); + if (order == 1) + return params; + + const T pi = std::acos(T(-1)); + const T eps = T(128) * std::numeric_limits::epsilon(); + for (int i = 1; i < order; ++i) + { + T x = -std::cos(pi * T(i) / T(order)); + for (int iter = 0; iter < 32; ++iter) + { + const auto [p, dp] = graph_legendre_value_and_derivative(order, x); + const T denom = T(1) - x * x; + if (std::abs(denom) <= eps) + break; + const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; + if (std::abs(d2p) <= eps) + break; + const T step = dp / d2p; + x -= step; + x = std::clamp(x, -T(1) + eps, T(1) - eps); + if (std::abs(step) <= eps) + break; + } + params[static_cast(i)] = T(0.5) * (x + T(1)); + } + return params; +} + +template +std::vector graph_interpolation_parameters(int order, + curving::NodeFamily family) +{ + order = std::max(order, 1); + if (family == curving::NodeFamily::gll) + return graph_gll_parameters(order); + + std::vector params(static_cast(order + 1), T(0)); + for (int i = 0; i <= order; ++i) + params[static_cast(i)] = T(i) / T(order); + return params; +} + +template +T graph_lagrange_basis_1d(int i, std::span params, T x) +{ + T value = T(1); + const T xi = params[static_cast(i)]; + for (int j = 0; j < static_cast(params.size()); ++j) + { + if (j == i) + continue; + value *= (x - params[static_cast(j)]) + / (xi - params[static_cast(j)]); + } + return value; +} + +template +T graph_warp_factor(int order, T r) +{ + if (order <= 1) + return T(0); + + std::vector equispaced(static_cast(order + 1), T(0)); + auto gll = graph_gll_parameters(order); + for (int i = 0; i <= order; ++i) + { + equispaced[static_cast(i)] = + -T(1) + T(2 * i) / T(order); + gll[static_cast(i)] = + T(2) * gll[static_cast(i)] - T(1); + } + + T warp = T(0); + for (int i = 0; i <= order; ++i) + { + const T Li = graph_lagrange_basis_1d( + i, std::span(equispaced.data(), equispaced.size()), r); + warp += Li * (gll[static_cast(i)] + - equispaced[static_cast(i)]); + } + + const T edge_factor = T(1) - r * r; + if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) + warp /= edge_factor; + return warp; +} + +template +std::array graph_equilateral_to_reference_barycentric(T x, T y) +{ + const T sqrt3 = std::sqrt(T(3)); + std::array w = {}; + w[2] = (sqrt3 * y + T(1)) / T(3); + w[1] = (x + T(1) - w[2]) / T(2); + w[0] = T(1) - w[1] - w[2]; + + T sum = T(0); + for (T& wi : w) + { + if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) + wi = T(0); + if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) + wi = T(1); + wi = std::clamp(wi, T(0), T(1)); + sum += wi; + } + if (sum > T(0)) + for (T& wi : w) + wi /= sum; + return w; +} + +template +std::vector> graph_triangle_barycentric_nodes( + int order, + curving::NodeFamily family) +{ + order = std::max(order, 1); + std::vector> nodes; + nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); + + if (family != curving::NodeFamily::gll) + { + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + nodes.push_back({T(1) - u - v, u, v}); + } + } + return nodes; + } + + constexpr std::array alpha_opt = { + T(0.0), T(0.0), T(1.4152), T(0.1001), + T(0.2751), T(0.9800), T(1.0999), T(1.2832), + T(1.3648), T(1.4773), T(1.4959), T(1.5743), + T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; + const T alpha = (order < static_cast(alpha_opt.size())) + ? alpha_opt[static_cast(order)] + : T(5) / T(3); + const T sqrt3 = std::sqrt(T(3)); + const T cos120 = -T(0.5); + const T sin120 = sqrt3 / T(2); + const T cos240 = -T(0.5); + const T sin240 = -sqrt3 / T(2); + + for (int j = 0; j <= order; ++j) + { + for (int i = 0; i <= order - j; ++i) + { + const T u = T(i) / T(order); + const T v = T(j) / T(order); + const T lambda0 = T(1) - u - v; + const T lambda1 = u; + const T lambda2 = v; + + T x = -lambda0 + lambda1; + T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; + + const T L1 = lambda2; + const T L2 = lambda0; + const T L3 = lambda1; + const T warp1 = T(4) * L2 * L3 * graph_warp_factor(order, L3 - L2) + * (T(1) + (alpha * L1) * (alpha * L1)); + const T warp2 = T(4) * L1 * L3 * graph_warp_factor(order, L1 - L3) + * (T(1) + (alpha * L2) * (alpha * L2)); + const T warp3 = T(4) * L1 * L2 * graph_warp_factor(order, L2 - L1) + * (T(1) + (alpha * L3) * (alpha * L3)); + + x += warp1 + cos120 * warp2 + cos240 * warp3; + y += sin120 * warp2 + sin240 * warp3; + nodes.push_back(graph_equilateral_to_reference_barycentric(x, y)); + } + } + return nodes; +} + +template +std::vector graph_triangle_monomials(int order, std::span bary) +{ + const T u = bary[1]; + const T v = bary[2]; + std::vector values; + values.reserve(static_cast((order + 1) * (order + 2) / 2)); + for (int total = 0; total <= order; ++total) + { + for (int j = 0; j <= total; ++j) + { + const int i = total - j; + values.push_back(std::pow(u, i) * std::pow(v, j)); + } + } + return values; +} + +template +std::vector graph_triangle_lagrange_basis( + int order, + curving::NodeFamily family, + std::span bary) +{ + const auto nodes = graph_triangle_barycentric_nodes(order, family); + const int n = static_cast(nodes.size()); + std::vector matrix(static_cast(n * n), T(0)); + for (int row = 0; row < n; ++row) + { + const auto mono = graph_triangle_monomials( + order, + std::span(nodes[static_cast(row)].data(), 3)); + for (int col = 0; col < n; ++col) + matrix[static_cast(col * n + row)] = + mono[static_cast(col)]; + } + + const auto rhs = graph_triangle_monomials(order, bary); + std::vector basis; + if (!solve_graph_dense_small(std::move(matrix), rhs, n, basis, + T(256) * std::numeric_limits::epsilon())) + { + return {}; + } + return basis; +} + +template +std::vector graph_eval_curved_zero_face_ref( + const curving::CurvedZeroEntityState& state, + cell::type entity_type, + int order, + curving::NodeFamily family, + std::span xi) +{ + const int nodes_per_face = + (entity_type == cell::type::quadrilateral) + ? (order + 1) * (order + 1) + : (order + 1) * (order + 2) / 2; + if (nodes_per_face <= 0 + || state.ref_nodes.size() % static_cast(nodes_per_face) != 0) + { + return {}; + } + const int tdim = static_cast( + state.ref_nodes.size() / static_cast(nodes_per_face)); + std::vector out(static_cast(tdim), T(0)); + + if (entity_type == cell::type::quadrilateral) + { + const auto params = graph_interpolation_parameters(order, family); + const T u = xi[0]; + const T v = xi[1]; + int node = 0; + for (int j = 0; j <= order; ++j) + { + const T Lj = graph_lagrange_basis_1d( + j, std::span(params.data(), params.size()), v); + for (int i = 0; i <= order; ++i) + { + const T Li = graph_lagrange_basis_1d( + i, std::span(params.data(), params.size()), u); + const T L = Li * Lj; + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] += L * state.ref_nodes[ + static_cast(node * tdim + d)]; + ++node; + } + } + return out; + } + + if (entity_type == cell::type::triangle) + { + const std::array bary = {T(1) - xi[0] - xi[1], xi[0], xi[1]}; + const auto basis = graph_triangle_lagrange_basis( + order, family, std::span(bary.data(), bary.size())); + if (basis.empty()) + return {}; + for (int node = 0; node < static_cast(basis.size()); ++node) + { + const T L = basis[static_cast(node)]; + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] += L * state.ref_nodes[ + static_cast(node * tdim + d)]; + } + return out; + } + + return {}; +} + +template +std::vector graph_eval_straight_zero_face_ref( + const AdaptCell& adapt_cell, + std::span vertices, + cell::type entity_type, + std::span xi) +{ + std::vector out(static_cast(adapt_cell.tdim), T(0)); + std::array weights = {}; + int nweights = 0; + if (entity_type == cell::type::triangle) + { + weights = {T(1) - xi[0] - xi[1], xi[0], xi[1], T(0)}; + nweights = 3; + } + else if (entity_type == cell::type::quadrilateral) + { + const T u = xi[0]; + const T v = xi[1]; + weights = {(T(1) - u) * (T(1) - v), + u * (T(1) - v), + (T(1) - u) * v, + u * v}; + nweights = 4; + } + else + { + return {}; + } + + if (static_cast(vertices.size()) < nweights) + return {}; + for (int i = 0; i < nweights; ++i) + { + const int vertex = vertices[static_cast(i)]; + for (int d = 0; d < adapt_cell.tdim; ++d) + { + out[static_cast(d)] += + weights[static_cast(i)] + * adapt_cell.vertex_coords[ + static_cast(vertex * adapt_cell.tdim + d)]; + } + } + return out; +} + +template +T graph_signed_surface_jacobian_ratio(std::span host_du_phys, + std::span host_dv_phys, + std::span corr_du_phys, + std::span corr_dv_phys, + T tol) +{ + if (host_du_phys.size() != 3 || host_dv_phys.size() != 3 + || corr_du_phys.size() != 3 + || corr_dv_phys.size() != 3) + { + return std::numeric_limits::quiet_NaN(); + } + + const auto host_cross = geom::cross( + host_du_phys, host_dv_phys); + const T denom = + host_cross[0] * host_cross[0] + + host_cross[1] * host_cross[1] + + host_cross[2] * host_cross[2]; + if (denom <= tol * tol) + return std::numeric_limits::quiet_NaN(); + + const auto corr_cross = geom::cross(corr_du_phys, corr_dv_phys); + const T numer = + corr_cross[0] * host_cross[0] + + corr_cross[1] * host_cross[1] + + corr_cross[2] * host_cross[2]; + return numer / denom; +} + +template +bool check_zero_face_surface_jacobian( + const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span ordered_vertices, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics, + int source_cell_id) +{ + graph_criteria::FaceQualityReport report; + report.minimum_surface_jacobian_ratio = + std::numeric_limits::infinity(); + + const int dim = ls_cell.tdim; + if (dim != 3 || ls_cell.gdim != 3) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + const int zdim = + adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + const cell::type entity_type = + adapt_cell.entity_types[2][static_cast(zid)]; + if (entity_type != cell::type::triangle + && entity_type != cell::type::quadrilateral) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + const auto curving_options = graph_curving_options(graph_options); + curving::CurvingData curving_data; + const std::array parent_cell_ids = {static_cast(ls_cell.cell_id)}; + const std::array, 1> adapt_cells = {adapt_cell}; + const std::array, 1> level_set_cells = {ls_cell}; + const std::array ls_offsets = {0, 1}; + const auto& state = curving::ensure_curved( + curving_data, + std::span(parent_cell_ids.data(), parent_cell_ids.size()), + std::span>(adapt_cells.data(), adapt_cells.size()), + std::span>( + level_set_cells.data(), level_set_cells.size()), + std::span(ls_offsets.data(), ls_offsets.size()), + 0, + local_zero_entity_id, + curving_options); + if (state.status != curving::CurvingStatus::curved) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + std::vector> samples; + if (entity_type == cell::type::triangle) + { + samples = {{{T(1) / T(3), T(1) / T(3)}, + {T(0.2), T(0.2)}, + {T(0.6), T(0.2)}, + {T(0.2), T(0.6)}}}; + } + else + { + samples = {{{T(0.5), T(0.5)}, + {T(0.25), T(0.25)}, + {T(0.75), T(0.25)}, + {T(0.25), T(0.75)}, + {T(0.75), T(0.75)}}}; + } + + const T h = T(1.0e-5); + auto eval_curved = [&](std::span xi) + { + return graph_eval_curved_zero_face_ref( + state, entity_type, curving_options.geometry_order, + curving_options.node_family, xi); + }; + auto eval_straight = [&](std::span xi) + { + return graph_eval_straight_zero_face_ref( + adapt_cell, ordered_vertices, entity_type, xi); + }; + + for (int sample_id = 0; sample_id < static_cast(samples.size()); ++sample_id) + { + const auto xi = samples[static_cast(sample_id)]; + const std::array xi_u_plus = {xi[0] + h, xi[1]}; + const std::array xi_u_minus = {xi[0] - h, xi[1]}; + const std::array xi_v_plus = {xi[0], xi[1] + h}; + const std::array xi_v_minus = {xi[0], xi[1] - h}; + + const auto curved_u_plus = eval_curved( + std::span(xi_u_plus.data(), xi_u_plus.size())); + const auto curved_u_minus = eval_curved( + std::span(xi_u_minus.data(), xi_u_minus.size())); + const auto curved_v_plus = eval_curved( + std::span(xi_v_plus.data(), xi_v_plus.size())); + const auto curved_v_minus = eval_curved( + std::span(xi_v_minus.data(), xi_v_minus.size())); + const auto straight_u_plus = eval_straight( + std::span(xi_u_plus.data(), xi_u_plus.size())); + const auto straight_u_minus = eval_straight( + std::span(xi_u_minus.data(), xi_u_minus.size())); + const auto straight_v_plus = eval_straight( + std::span(xi_v_plus.data(), xi_v_plus.size())); + const auto straight_v_minus = eval_straight( + std::span(xi_v_minus.data(), xi_v_minus.size())); + + if (curved_u_plus.empty() || curved_u_minus.empty() + || curved_v_plus.empty() || curved_v_minus.empty() + || straight_u_plus.empty() || straight_u_minus.empty() + || straight_v_plus.empty() || straight_v_minus.empty()) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + std::vector curved_du(static_cast(dim), T(0)); + std::vector curved_dv(static_cast(dim), T(0)); + std::vector straight_du(static_cast(dim), T(0)); + std::vector straight_dv(static_cast(dim), T(0)); + for (int d = 0; d < dim; ++d) + { + curved_du[static_cast(d)] = + (curved_u_plus[static_cast(d)] + - curved_u_minus[static_cast(d)]) / (T(2) * h); + curved_dv[static_cast(d)] = + (curved_v_plus[static_cast(d)] + - curved_v_minus[static_cast(d)]) / (T(2) * h); + straight_du[static_cast(d)] = + (straight_u_plus[static_cast(d)] + - straight_u_minus[static_cast(d)]) / (T(2) * h); + straight_dv[static_cast(d)] = + (straight_v_plus[static_cast(d)] + - straight_v_minus[static_cast(d)]) / (T(2) * h); + } + + const auto curved_du_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(curved_du.data(), curved_du.size())); + const auto curved_dv_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(curved_dv.data(), curved_dv.size())); + const auto straight_du_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(straight_du.data(), straight_du.size())); + const auto straight_dv_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(straight_dv.data(), straight_dv.size())); + + const T ratio = graph_signed_surface_jacobian_ratio( + std::span(straight_du_phys.data(), straight_du_phys.size()), + std::span(straight_dv_phys.data(), straight_dv_phys.size()), + std::span(curved_du_phys.data(), curved_du_phys.size()), + std::span(curved_dv_phys.data(), curved_dv_phys.size()), + graph_options.criteria.tolerance); + if (!std::isfinite(ratio)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::face_degenerate); + return false; + } + + report.minimum_surface_jacobian_ratio = + std::min(report.minimum_surface_jacobian_ratio, ratio); + if (ratio <= graph_options.criteria.min_surface_jacobian_ratio) + { + report.accepted = false; + report.failure_reason = + graph_criteria::FailureReason::surface_jacobian_not_positive; + report.failed_triangle_index = sample_id; + report.failed_surface_jacobian_ratio = ratio; + diagnostics.min_face_area_ratio = + std::min(diagnostics.min_face_area_ratio, + report.minimum_surface_jacobian_ratio); + observe_failed_face_orientation(diagnostics, report); + int requested_edge_id = -1; + if (entity_type == cell::type::triangle + && ordered_vertices.size() >= 3) + { + const std::array bary = {T(1) - xi[0] - xi[1], xi[0], xi[1]}; + int closest = 0; + if (bary[1] < bary[closest]) + closest = 1; + if (bary[2] < bary[closest]) + closest = 2; + const int a = ordered_vertices[ + static_cast((closest + 1) % 3)]; + const int b = ordered_vertices[ + static_cast((closest + 2) % 3)]; + requested_edge_id = + graph_refinement_edge_from_failed_face_segment( + adapt_cell, a, b); + } + else if (entity_type == cell::type::quadrilateral + && ordered_vertices.size() >= 4) + { + std::array dist = {xi[1], T(1) - xi[0], T(1) - xi[1], xi[0]}; + int side = 0; + for (int i = 1; i < 4; ++i) + if (dist[static_cast(i)] + < dist[static_cast(side)]) + side = i; + const std::array, 4> side_vertices = {{ + {{0, 1}}, {{1, 3}}, {{2, 3}}, {{0, 2}}}}; + const int a = ordered_vertices[static_cast( + side_vertices[static_cast(side)][0])]; + const int b = ordered_vertices[static_cast( + side_vertices[static_cast(side)][1])]; + requested_edge_id = + graph_refinement_edge_from_failed_face_segment( + adapt_cell, a, b); + } + mark_graph_refinement_request( + diagnostics, 1, requested_edge_id); + mark_graph_failure( + diagnostics, source_cell_id, report.failure_reason); + return false; + } + } + + diagnostics.min_face_area_ratio = + std::min(diagnostics.min_face_area_ratio, + report.minimum_surface_jacobian_ratio); + return true; +} + +template +bool check_zero_face_graph(const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span ordered_vertices, + const std::vector& vertex_coords, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics, + int source_cell_id) +{ + ++diagnostics.checked_faces; + + std::vector face_vertices; + face_vertices.reserve(ordered_vertices.size() * static_cast(ls_cell.tdim)); + for (const int v : ordered_vertices) + { + auto p = point_span(vertex_coords, v, ls_cell.tdim); + face_vertices.insert(face_vertices.end(), p.begin(), p.end()); + } + + graph_criteria::HostFrame host; + if (!build_face_host_frame( + face_vertices, host, graph_options.criteria.tolerance)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_host_frame); + return false; + } + + const int nverts = static_cast(ordered_vertices.size()); + int longest_face_edge_id = -1; + T longest_face_edge_length = T(0); + for (int i = 0; i < nverts; ++i) + { + const int a = ordered_vertices[static_cast(i)]; + const int b = ordered_vertices[ + static_cast((i + 1) % nverts)]; + const auto pa = point_span(vertex_coords, a, ls_cell.tdim); + const auto pb = point_span(vertex_coords, b, ls_cell.tdim); + const auto edge_delta = geom::subtract(pb, pa); + const T edge_length = geom::norm( + std::span(edge_delta.data(), edge_delta.size())); + const int adapt_edge_id = find_adapt_edge_by_vertices( + adapt_cell, a, b); + if (adapt_edge_id >= 0 && edge_length > longest_face_edge_length) + { + longest_face_edge_length = edge_length; + longest_face_edge_id = adapt_edge_id; + } + + if (!check_zero_edge_graph( + adapt_cell, + local_zero_entity_id, + ls_cell, + pa, + pb, + std::span(host.normal.data(), host.normal.size()), + true, + adapt_edge_id, + graph_options, + diagnostics, + source_cell_id)) + { + return false; + } + } + + std::vector face_center(static_cast(ls_cell.tdim), T(0)); + for (const int v : ordered_vertices) + { + const auto p = point_span(vertex_coords, v, ls_cell.tdim); + for (int d = 0; d < ls_cell.tdim; ++d) + face_center[static_cast(d)] += p[static_cast(d)]; + } + for (T& value : face_center) + value /= static_cast(nverts); + + std::vector corrected_face_center; + const geom::ParentEntity cell_interior{ls_cell.tdim, -1}; + if (!project_graph_point( + adapt_cell, + local_zero_entity_id, + ls_cell, + cell_interior, + host, + std::span(face_center.data(), face_center.size()), + GraphNodeKind::face_interior, + 0, + longest_face_edge_id, + graph_options, + diagnostics, + source_cell_id, + corrected_face_center)) + { + return false; + } + + return check_zero_face_surface_jacobian( + adapt_cell, + local_zero_entity_id, + ls_cell, + ordered_vertices, + graph_options, + diagnostics, + source_cell_id); +} + +template +std::vector adapt_zero_entity_vertex_ids(const AdaptCell& ac, + int local_zero_entity_id) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim == 0) + return {zid}; + + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + std::vector out; + out.reserve(verts.size()); + for (const auto v : verts) + out.push_back(static_cast(v)); + return out; +} + +inline bool contains_vertex_id(std::span vertices, int vertex_id) +{ + for (const int v : vertices) + { + if (v == vertex_id) + return true; + } + return false; +} + +template +std::vector face_normal_for_zero_entity(const AdaptCell& ac, + int local_zero_entity_id, + T tol) +{ + std::vector normal(static_cast(ac.tdim), T(0)); + if (ac.tdim != 3 + || ac.zero_entity_dim[static_cast(local_zero_entity_id)] != 2) + { + return normal; + } + + const auto face_vertices = + adapt_zero_entity_vertex_ids(ac, local_zero_entity_id); + std::vector face_coords; + face_coords.reserve(face_vertices.size() * 3); + for (const int v : face_vertices) + { + auto p = point_span(ac.vertex_coords, v, 3); + face_coords.insert(face_coords.end(), p.begin(), p.end()); + } + + graph_criteria::HostFrame host; + if (!build_face_host_frame(face_coords, host, tol)) + return normal; + return host.normal; +} + +template +std::vector face_normal_for_zero_edge(const AdaptCell& ac, + int local_zero_edge_id, + int level_set_id, + T tol) +{ + std::vector normal(static_cast(ac.tdim), T(0)); + if (ac.tdim != 3 + || ac.zero_entity_dim[static_cast(local_zero_edge_id)] != 1) + { + return normal; + } + + const auto edge_vertices = + adapt_zero_entity_vertex_ids(ac, local_zero_edge_id); + if (edge_vertices.size() != 2) + return normal; + + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + T best_norm = T(0); + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (ac.zero_entity_dim[static_cast(z)] != 2) + continue; + if ((ac.zero_entity_zero_mask[static_cast(z)] & bit) == 0) + continue; + + const auto face_vertices = adapt_zero_entity_vertex_ids(ac, z); + if (!contains_vertex_id(std::span(face_vertices.data(), face_vertices.size()), + edge_vertices[0]) + || !contains_vertex_id( + std::span(face_vertices.data(), face_vertices.size()), + edge_vertices[1])) + { + continue; + } + + auto candidate = face_normal_for_zero_entity(ac, z, tol); + const T n = geom::norm( + std::span(candidate.data(), candidate.size())); + if (n > best_norm) + { + best_norm = n; + normal = std::move(candidate); + } + } + + if (best_norm <= tol) + std::fill(normal.begin(), normal.end(), T(0)); + return normal; +} + +template +ZeroEntityGraphDiagnostics make_zero_entity_graph_record( + int local_zero_entity_id, + int level_set_id, + int dimension, + std::uint64_t zero_mask, + const ReadyCellGraphDiagnostics& entity_diag) +{ + ZeroEntityGraphDiagnostics record; + record.local_zero_entity_id = local_zero_entity_id; + record.level_set_id = level_set_id; + record.dimension = dimension; + record.zero_mask = zero_mask; + record.accepted = entity_diag.accepted; + record.checked_edges = entity_diag.checked_edges; + record.checked_faces = entity_diag.checked_faces; + record.failed_checks = entity_diag.failed_checks; + record.failure_reason = entity_diag.accepted + ? graph_criteria::FailureReason::none + : entity_diag.first_failure_reason; + record.min_true_transversality = entity_diag.min_true_transversality; + record.min_host_normal_alignment = entity_diag.min_host_normal_alignment; + record.max_drift_amplification = entity_diag.max_drift_amplification; + record.max_relative_correction_distance = + entity_diag.max_relative_correction_distance; + record.max_relative_tangential_shift = + entity_diag.max_relative_tangential_shift; + record.min_edge_gap_ratio = entity_diag.min_edge_gap_ratio; + record.min_face_area_ratio = entity_diag.min_face_area_ratio; + record.min_level_set_gradient_host_alignment = + entity_diag.min_level_set_gradient_host_alignment; + record.failed_face_triangle_index = + entity_diag.first_failed_face_triangle_index; + record.failed_face_area_ratio = + entity_diag.first_failed_face_area_ratio; + record.failed_projection_seed = entity_diag.first_failed_projection_seed; + record.failed_projection_direction = + entity_diag.first_failed_projection_direction; + record.failed_projection_clip_lo = + entity_diag.first_failed_projection_clip_lo; + record.failed_projection_clip_hi = + entity_diag.first_failed_projection_clip_hi; + record.failed_projection_root_t = + entity_diag.first_failed_projection_root_t; + record.requested_refinement_entity_dim = + entity_diag.first_requested_refinement_entity_dim; + record.requested_refinement_entity_id = + entity_diag.first_requested_refinement_entity_id; + record.nodes = entity_diag.nodes; + return record; +} + +template +void accumulate_zero_entity_graph_record(ReadyCellGraphDiagnostics& diagnostics, + const ZeroEntityGraphDiagnostics& record, + int source_cell_id) +{ + diagnostics.checked_edges += record.checked_edges; + diagnostics.checked_faces += record.checked_faces; + diagnostics.min_true_transversality = + std::min(diagnostics.min_true_transversality, + record.min_true_transversality); + diagnostics.min_host_normal_alignment = + std::min(diagnostics.min_host_normal_alignment, + record.min_host_normal_alignment); + diagnostics.max_drift_amplification = + std::max(diagnostics.max_drift_amplification, + record.max_drift_amplification); + diagnostics.max_relative_correction_distance = + std::max(diagnostics.max_relative_correction_distance, + record.max_relative_correction_distance); + diagnostics.max_relative_tangential_shift = + std::max(diagnostics.max_relative_tangential_shift, + record.max_relative_tangential_shift); + diagnostics.min_edge_gap_ratio = + std::min(diagnostics.min_edge_gap_ratio, record.min_edge_gap_ratio); + diagnostics.min_face_area_ratio = + std::min(diagnostics.min_face_area_ratio, record.min_face_area_ratio); + diagnostics.min_level_set_gradient_host_alignment = + std::min(diagnostics.min_level_set_gradient_host_alignment, + record.min_level_set_gradient_host_alignment); + if (diagnostics.first_failed_face_triangle_index < 0 + && record.failed_face_triangle_index >= 0) + { + diagnostics.first_failed_face_triangle_index = + record.failed_face_triangle_index; + diagnostics.first_failed_face_area_ratio = + record.failed_face_area_ratio; + } + for (const auto& node : record.nodes) + diagnostics.nodes.push_back(node); + if (!record.accepted) + { + mark_graph_refinement_request( + diagnostics, + record.requested_refinement_entity_dim, + record.requested_refinement_entity_id); + mark_graph_failure(diagnostics, source_cell_id, record.failure_reason); + } +} + +template +void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics) +{ + const int graph_refinements = diagnostics.graph_refinements; + diagnostics = ReadyCellGraphDiagnostics{}; + diagnostics.graph_refinements = graph_refinements; + if (!graph_options.enabled) + return; + + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + for (int z = 0; z < adapt_cell.n_zero_entities(); ++z) + { + if ((adapt_cell.zero_entity_zero_mask[static_cast(z)] & bit) == 0) + continue; + + const int zdim = adapt_cell.zero_entity_dim[static_cast(z)]; + if (zdim != 1 && zdim != 2) + continue; + + ReadyCellGraphDiagnostics entity_diag; + const auto vertices = adapt_zero_entity_vertex_ids(adapt_cell, z); + if (zdim == 1) + { + if (vertices.size() != 2) + { + mark_graph_failure( + entity_diag, -1, graph_criteria::FailureReason::invalid_input); + } + else + { + const auto zero_face_normal = + face_normal_for_zero_edge( + adapt_cell, z, level_set_id, + graph_options.criteria.tolerance); + (void)check_zero_edge_graph( + adapt_cell, + z, + ls_cell, + point_span(adapt_cell.vertex_coords, vertices[0], adapt_cell.tdim), + point_span(adapt_cell.vertex_coords, vertices[1], adapt_cell.tdim), + std::span(zero_face_normal.data(), zero_face_normal.size()), + false, + find_adapt_edge_by_vertices( + adapt_cell, vertices[0], vertices[1]), + graph_options, + entity_diag, + -1); + } + } + else + { + if (adapt_cell.tdim != 3 || vertices.size() < 3) + { + mark_graph_failure( + entity_diag, -1, graph_criteria::FailureReason::invalid_input); + } + else + { + const auto face_normal = + face_normal_for_zero_entity( + adapt_cell, z, graph_options.criteria.tolerance); + const T normal_norm = geom::norm( + std::span(face_normal.data(), face_normal.size())); + if (normal_norm <= graph_options.criteria.tolerance) + { + mark_graph_failure( + entity_diag, -1, + graph_criteria::FailureReason::invalid_host_frame); + } + else + { + (void)check_zero_face_graph( + adapt_cell, + z, + ls_cell, + std::span(vertices.data(), vertices.size()), + adapt_cell.vertex_coords, + graph_options, + entity_diag, + -1); + } + } + } + + auto record = + make_zero_entity_graph_record( + z, + level_set_id, + zdim, + adapt_cell.zero_entity_zero_mask[static_cast(z)], + entity_diag); + accumulate_zero_entity_graph_record(diagnostics, record, -1); + diagnostics.zero_entities.push_back(std::move(record)); + } +} + +template +int find_top_cell_incident_to_edge(const AdaptCell& adapt_cell, int edge_id) +{ + if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) + return -1; + + const auto edge_vertices = + adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; + if (edge_vertices.size() != 2) + return -1; + + const int a = static_cast(edge_vertices[0]); + const int b = static_cast(edge_vertices[1]); + const int tdim = adapt_cell.tdim; + for (int c = 0; c < adapt_cell.n_entities(tdim); ++c) + { + const auto verts = + adapt_cell.entity_to_vertex[tdim][static_cast(c)]; + bool has_a = false; + bool has_b = false; + for (const auto v : verts) + { + has_a = has_a || static_cast(v) == a; + has_b = has_b || static_cast(v) == b; + } + if (has_a && has_b) + return c; + } + return -1; +} + +template +ReadyCellGraphDiagnostics check_processed_ready_to_cut_cell_graphs( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, + T sign_tol, + int edge_max_depth, + bool triangulate_cut_parts, + const ReadyCellGraphOptions& graph_options) +{ + ReadyCellGraphDiagnostics diagnostics; + if (!graph_options.enabled) + return diagnostics; + + bool has_ready = false; + const int tdim = adapt_cell.tdim; + for (int c = 0; c < adapt_cell.n_entities(tdim); ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) + == CellCertTag::ready_to_cut) + { + has_ready = true; + break; + } + } + if (!has_ready) + return diagnostics; + + AdaptCell temporary = adapt_cell; + process_ready_to_cut_cells( + temporary, + ls_cell, + level_set_id, + zero_tol, + sign_tol, + edge_max_depth, + triangulate_cut_parts); + fill_all_vertex_signs_from_level_set( + temporary, ls_cell, level_set_id, zero_tol); + build_edges(temporary); + if (temporary.tdim == 3) + build_faces(temporary); + recompute_active_level_set_masks(temporary, level_set_id + 1); + rebuild_zero_entity_inventory(temporary); + + populate_committed_zero_entity_graph_diagnostics( + temporary, ls_cell, level_set_id, graph_options, diagnostics); + + if (diagnostics.zero_entities.empty()) + { + mark_graph_failure( + diagnostics, -1, graph_criteria::FailureReason::invalid_input); + } + + if (!diagnostics.accepted && diagnostics.first_failed_cell < 0 + && diagnostics.first_requested_refinement_entity_dim == 1) + { + diagnostics.first_failed_cell = find_top_cell_incident_to_edge( + adapt_cell, diagnostics.first_requested_refinement_entity_id); + } + + return diagnostics; +} + // ===================================================================== // process_ready_to_cut_cells // ===================================================================== @@ -1636,6 +3954,260 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, } +template +ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + const ReadyCellGraphOptions& graph_options) +{ + ReadyCellGraphDiagnostics diagnostics; + + if (!graph_options.enabled) + return diagnostics; + + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + + for (int c = 0; c < n_cells; ++c) + { + if (adapt_cell.get_cell_cert_tag(level_set_id, c) + != CellCertTag::ready_to_cut) + { + continue; + } + + ++diagnostics.checked_cells; + + const cell::type leaf_cell_type = + adapt_cell.entity_types[tdim][static_cast(c)]; + if (leaf_cell_type != cell::type::triangle + && leaf_cell_type != cell::type::tetrahedron) + { + mark_graph_failure( + diagnostics, + c, + graph_criteria::FailureReason::invalid_input); + return diagnostics; + } + + AdaptCell temporary = adapt_cell; + for (int other = 0; other < n_cells; ++other) + { + if (other != c + && temporary.get_cell_cert_tag(level_set_id, other) + == CellCertTag::ready_to_cut) + { + temporary.set_cell_cert_tag( + level_set_id, other, CellCertTag::not_classified); + } + } + + process_ready_to_cut_cells( + temporary, + ls_cell, + level_set_id, + graph_options.criteria.tolerance, + graph_options.criteria.tolerance, + /*edge_max_depth=*/20, + /*triangulate_cut_parts=*/false); + + ReadyCellGraphDiagnostics temporary_zero_diag; + populate_committed_zero_entity_graph_diagnostics( + temporary, + ls_cell, + level_set_id, + graph_options, + temporary_zero_diag); + + bool checked_zero_entity = false; + for (const auto& record : temporary_zero_diag.zero_entities) + { + checked_zero_entity = true; + accumulate_zero_entity_graph_record(diagnostics, record, c); + if (!diagnostics.accepted) + return diagnostics; + } + + if (!checked_zero_entity) + { + mark_graph_failure( + diagnostics, + c, + graph_criteria::FailureReason::invalid_input); + return diagnostics; + } + } + + return diagnostics; +} + +template +bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int cell_id) +{ + const int tdim = adapt_cell.tdim; + if (cell_id < 0 || cell_id >= adapt_cell.n_entities(tdim)) + return false; + + const auto edge_lookup = build_leaf_edge_lookup(adapt_cell); + auto verts = adapt_cell.entity_to_vertex[tdim][static_cast(cell_id)]; + const cell::type ctype = adapt_cell.entity_types[tdim][static_cast(cell_id)]; + const auto ledges = cell::edges(ctype); + + int selected_edge = -1; + T selected_value = -std::numeric_limits::infinity(); + for (const auto& le : ledges) + { + const int a = verts[static_cast(le[0])]; + const int b = verts[static_cast(le[1])]; + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto it = edge_lookup.find(key); + if (it == edge_lookup.end()) + continue; + + std::vector midpoint(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + { + midpoint[static_cast(d)] = + T(0.5) + * (adapt_cell.vertex_coords[static_cast(a * tdim + d)] + + adapt_cell.vertex_coords[static_cast(b * tdim + d)]); + } + const T value = ls_cell.value( + std::span(midpoint.data(), midpoint.size())); + if (selected_edge < 0 || value > selected_value) + { + selected_edge = it->second; + selected_value = value; + } + } + + if (selected_edge < 0) + return false; + + const int n_edges = adapt_cell.n_entities(1); + if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) + adapt_cell.resize_edge_root_tags(level_set_id + 1); + if (adapt_cell.edge_green_split_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + { + adapt_cell.resize_green_split_data(level_set_id + 1); + } + + const auto idx = + static_cast(level_set_id * n_edges + selected_edge); + adapt_cell.set_edge_root_tag( + level_set_id, selected_edge, EdgeRootTag::multiple_roots); + adapt_cell.edge_green_split_param[idx] = T(0.5); + adapt_cell.edge_green_split_has_value[idx] = 1; + + return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); +} + +template +T graph_requested_edge_split_parameter( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int edge_id, + T zero_tol, + T sign_tol, + int edge_max_depth) +{ + const T fallback = T(0.5); + const int n_edges = adapt_cell.n_entities(1); + if (edge_id < 0 || edge_id >= n_edges) + return fallback; + + const T endpoint_tol = + std::max(zero_tol, T(64) * std::numeric_limits::epsilon()); + auto usable = [&](T t) + { + return std::isfinite(t) && t > endpoint_tol + && t < T(1) - endpoint_tol; + }; + + const auto idx = + static_cast(level_set_id * n_edges + edge_id); + if (idx < adapt_cell.edge_one_root_has_value.size() + && adapt_cell.edge_one_root_has_value[idx] + && idx < adapt_cell.edge_one_root_param.size() + && usable(adapt_cell.edge_one_root_param[idx])) + { + return adapt_cell.edge_one_root_param[idx]; + } + + if (adapt_cell.get_edge_root_tag(level_set_id, edge_id) + != EdgeRootTag::one_root) + { + return fallback; + } + + std::vector edge_coeffs; + gather_adapt_edge_bernstein(adapt_cell, ls_cell, edge_id, edge_coeffs); + + T root_t = fallback; + if (locate_one_root_parameter( + std::span(edge_coeffs), zero_tol, sign_tol, + edge_max_depth, root_t) + && usable(root_t)) + { + return root_t; + } + + return fallback; +} + +template +bool refine_green_on_requested_graph_edge(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int edge_id, + T zero_tol, + T sign_tol, + int edge_max_depth) +{ + if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) + return false; + + const int n_edges = adapt_cell.n_entities(1); + if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) + adapt_cell.resize_edge_root_tags(level_set_id + 1); + if (adapt_cell.edge_green_split_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + { + adapt_cell.resize_green_split_data(level_set_id + 1); + } + + const auto idx = + static_cast(level_set_id * n_edges + edge_id); + adapt_cell.set_edge_root_tag( + level_set_id, edge_id, EdgeRootTag::multiple_roots); + adapt_cell.edge_green_split_param[idx] = + graph_requested_edge_split_parameter( + adapt_cell, ls_cell, level_set_id, edge_id, + zero_tol, sign_tol, edge_max_depth); + adapt_cell.edge_green_split_has_value[idx] = 1; + return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); +} + +template +bool refine_red_on_graph_failed_cell(AdaptCell& adapt_cell, + int level_set_id, + int cell_id) +{ + if (cell_id < 0 || cell_id >= adapt_cell.n_entities(adapt_cell.tdim)) + return false; + + adapt_cell.set_cell_cert_tag( + level_set_id, cell_id, CellCertTag::ambiguous); + return refine_red_on_ambiguous_cells(adapt_cell, level_set_id); +} + // ===================================================================== // certify_and_refine // ===================================================================== @@ -1718,6 +4290,95 @@ void certify_and_refine(AdaptCell& adapt_cell, } } +template +ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( + AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth, + bool triangulate_cut_parts, + const ReadyCellGraphOptions& graph_options) +{ + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); + + ReadyCellGraphDiagnostics diagnostics; + for (int graph_iter = 0; graph_iter <= graph_options.max_refinements; ++graph_iter) + { + certify_and_refine(adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, edge_max_depth); + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); + + diagnostics = check_processed_ready_to_cut_cell_graphs( + adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, edge_max_depth, + triangulate_cut_parts, graph_options); + if (!diagnostics.accepted + && diagnostics.first_requested_refinement_entity_dim != 1) + { + diagnostics = check_ready_to_cut_cell_graphs( + adapt_cell, ls_cell, level_set_id, graph_options); + } + diagnostics.graph_refinements = graph_iter; + + if (diagnostics.accepted) + break; + + if (graph_iter >= graph_options.max_refinements) + { + break; + } + + bool refined = false; + if (graph_options.refinement_mode + == GraphRefinementMode::red_failed_cell) + { + refined = refine_red_on_graph_failed_cell( + adapt_cell, level_set_id, diagnostics.first_failed_cell); + } + else if (diagnostics.first_requested_refinement_entity_dim == 1) + { + refined = refine_green_on_requested_graph_edge( + adapt_cell, + ls_cell, + level_set_id, + diagnostics.first_requested_refinement_entity_id, + zero_tol, + sign_tol, + edge_max_depth); + } + + if (!refined) + { + refined = refine_ready_cell_on_largest_midpoint_value( + adapt_cell, ls_cell, level_set_id, diagnostics.first_failed_cell); + } + + if (!refined) + { + break; + } + + fill_all_vertex_signs_from_level_set( + adapt_cell, ls_cell, level_set_id, zero_tol); + } + + process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, + zero_tol, sign_tol, edge_max_depth, + triangulate_cut_parts); + fill_all_vertex_signs_from_level_set( + adapt_cell, ls_cell, level_set_id, zero_tol); + build_edges(adapt_cell); + if (adapt_cell.tdim == 3) + build_faces(adapt_cell); + recompute_active_level_set_masks(adapt_cell, level_set_id + 1); + rebuild_zero_entity_inventory(adapt_cell); + populate_committed_zero_entity_graph_diagnostics( + adapt_cell, ls_cell, level_set_id, graph_options, diagnostics); + return diagnostics; +} + template void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, const LevelSetCell& ls_cell, @@ -1727,13 +4388,17 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, int edge_max_depth, bool triangulate_cut_parts) { - fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); - certify_and_refine(adapt_cell, ls_cell, level_set_id, - max_iterations, zero_tol, sign_tol, edge_max_depth); - fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); - process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, - zero_tol, sign_tol, edge_max_depth, - triangulate_cut_parts); + const ReadyCellGraphOptions graph_options; + (void)certify_refine_graph_check_and_process_ready_cells( + adapt_cell, + ls_cell, + level_set_id, + max_iterations, + zero_tol, + sign_tol, + edge_max_depth, + triangulate_cut_parts, + graph_options); } // ===================================================================== @@ -1817,6 +4482,73 @@ template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, int, float, float, int, bool); +template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&); + +template void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&, + ReadyCellGraphDiagnostics&); +template void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&, + ReadyCellGraphDiagnostics&); +template void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&, + ReadyCellGraphDiagnostics&); +template void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell&, + const LevelSetCell&, + int, + const ReadyCellGraphOptions&, + ReadyCellGraphDiagnostics&); + +template bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell&, + const LevelSetCell&, + int, + int); +template bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell&, + const LevelSetCell&, + int, + int); +template bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell&, + const LevelSetCell&, + int, + int); +template bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell&, + const LevelSetCell&, + int, + int); + template void certify_and_refine(AdaptCell&, const LevelSetCell&, int, int, double, double, int); @@ -1840,4 +4572,49 @@ template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, int, int, float, float, int, bool); +template ReadyCellGraphDiagnostics +certify_refine_graph_check_and_process_ready_cells( + AdaptCell&, + const LevelSetCell&, + int, + int, + double, + double, + int, + bool, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics +certify_refine_graph_check_and_process_ready_cells( + AdaptCell&, + const LevelSetCell&, + int, + int, + float, + float, + int, + bool, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics +certify_refine_graph_check_and_process_ready_cells( + AdaptCell&, + const LevelSetCell&, + int, + int, + double, + double, + int, + bool, + const ReadyCellGraphOptions&); +template ReadyCellGraphDiagnostics +certify_refine_graph_check_and_process_ready_cells( + AdaptCell&, + const LevelSetCell&, + int, + int, + float, + float, + int, + bool, + const ReadyCellGraphOptions&); + } // namespace cutcells diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index a2423d5..43a7219 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -7,16 +7,162 @@ #include #include +#include #include #include #include "adapt_cell.h" #include "cell_types.h" +#include "graph_criteria.h" #include "level_set_cell.h" namespace cutcells { +enum class GraphProjectionMode : std::uint8_t +{ + straight_zero_entity_normal = 0, + level_set_gradient = 1 +}; + +enum class GraphRefinementMode : std::uint8_t +{ + green_edge = 0, + red_failed_cell = 1 +}; + +template +struct ReadyCellGraphOptions +{ + bool enabled = true; + int geometry_order = 2; + int max_refinements = 5; + GraphProjectionMode projection_mode = + GraphProjectionMode::level_set_gradient; + GraphRefinementMode refinement_mode = + GraphRefinementMode::green_edge; + graph_criteria::Options criteria = {}; + T min_level_set_gradient_host_alignment = T(0.9); +}; + +enum class GraphNodeKind : std::uint8_t +{ + edge_endpoint = 0, + edge_interior = 1, + face_boundary_edge = 2, + face_interior = 3 +}; + +template +struct GraphNodeDiagnostics +{ + int node_index = -1; + GraphNodeKind node_kind = GraphNodeKind::edge_interior; + bool accepted = true; + bool fallback_used = false; + graph_criteria::DirectionKind selected_direction_kind = + graph_criteria::DirectionKind::projected_level_set_gradient; + graph_criteria::FailureReason failure_reason = + graph_criteria::FailureReason::none; + int parent_entity_dim = -1; + int parent_entity_id = -1; + int requested_refinement_entity_dim = -1; + int requested_refinement_entity_id = -1; + + T level_set_gradient_host_alignment = std::numeric_limits::quiet_NaN(); + T level_set_gradient_angle_to_tangent_deg = + std::numeric_limits::quiet_NaN(); + T selected_host_alignment = std::numeric_limits::quiet_NaN(); + T drift_amplification = std::numeric_limits::quiet_NaN(); + T relative_correction_distance = std::numeric_limits::quiet_NaN(); + T relative_tangential_shift = std::numeric_limits::quiet_NaN(); + T true_transversality = std::numeric_limits::quiet_NaN(); + + std::vector seed; + std::vector corrected; + std::vector selected_direction; + std::vector level_set_gradient_direction; + std::vector straight_helper_normal; +}; + +template +struct ZeroEntityGraphDiagnostics +{ + int local_zero_entity_id = -1; + int level_set_id = -1; + int dimension = -1; + std::uint64_t zero_mask = 0; + bool accepted = true; + int checked_edges = 0; + int checked_faces = 0; + int failed_checks = 0; + graph_criteria::FailureReason failure_reason = + graph_criteria::FailureReason::none; + + T min_true_transversality = std::numeric_limits::infinity(); + T min_host_normal_alignment = std::numeric_limits::infinity(); + T max_drift_amplification = T(0); + T max_relative_correction_distance = T(0); + T max_relative_tangential_shift = T(0); + T min_edge_gap_ratio = std::numeric_limits::infinity(); + T min_face_area_ratio = std::numeric_limits::infinity(); + T min_level_set_gradient_host_alignment = + std::numeric_limits::infinity(); + int failed_face_triangle_index = -1; + T failed_face_area_ratio = std::numeric_limits::quiet_NaN(); + + std::vector failed_projection_seed; + std::vector failed_projection_direction; + T failed_projection_clip_lo = std::numeric_limits::quiet_NaN(); + T failed_projection_clip_hi = std::numeric_limits::quiet_NaN(); + T failed_projection_root_t = std::numeric_limits::quiet_NaN(); + + int requested_refinement_entity_dim = -1; + int requested_refinement_entity_id = -1; + std::vector> nodes; +}; + +template +struct ReadyCellGraphDiagnostics +{ + bool accepted = true; + int checked_cells = 0; + int checked_edges = 0; + int checked_faces = 0; + int failed_checks = 0; + int graph_refinements = 0; + int first_failed_cell = -1; + graph_criteria::FailureReason first_failure_reason = + graph_criteria::FailureReason::none; + + T min_true_transversality = std::numeric_limits::infinity(); + T min_host_normal_alignment = std::numeric_limits::infinity(); + T max_drift_amplification = T(0); + T max_relative_correction_distance = T(0); + T max_relative_tangential_shift = T(0); + T min_edge_gap_ratio = std::numeric_limits::infinity(); + T min_face_area_ratio = std::numeric_limits::infinity(); + T min_level_set_gradient_host_alignment = + std::numeric_limits::infinity(); + int first_failed_face_triangle_index = -1; + T first_failed_face_area_ratio = std::numeric_limits::quiet_NaN(); + + std::vector first_failed_projection_seed; + std::vector first_failed_projection_direction; + T first_failed_projection_clip_lo = std::numeric_limits::quiet_NaN(); + T first_failed_projection_clip_hi = std::numeric_limits::quiet_NaN(); + T first_failed_projection_root_t = std::numeric_limits::quiet_NaN(); + int first_requested_refinement_entity_dim = -1; + int first_requested_refinement_entity_id = -1; + + /// Diagnostics attached to the committed AdaptCell zero entities. These + /// are populated after the linear cut is committed and before any lazy + /// high-order curving can run, so visualization can display the graph + /// criterion on the zero edge/face where it is evaluated. + std::vector> zero_entities; + std::vector> nodes; +}; + // ===================================================================== // Exact subcell Bernstein restriction // ===================================================================== @@ -153,6 +299,37 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, int edge_max_depth, bool triangulate_cut_parts = false); +/// Run graph diagnostics on all current ready_to_cut cells without mutating +/// AdaptCell topology, masks, or provenance. The diagnostic cut is a temporary +/// P1/linear interface built from the current uncut ready leaf cells. +template +ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + const ReadyCellGraphOptions& graph_options = {}); + +/// Populate per-committed-zero-entity graph diagnostics on an existing +/// diagnostic object. This is used after final zero-entity inventory rebuilds +/// so visualization data is keyed to the AdaptCell zero entities that users +/// inspect. +template +void populate_committed_zero_entity_graph_diagnostics( + const AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + const ReadyCellGraphOptions& graph_options, + ReadyCellGraphDiagnostics& diagnostics); + +/// Green-refine a ready leaf cell through the midpoint of the cell edge whose +/// midpoint has the largest level-set value. +template +bool refine_ready_cell_on_largest_midpoint_value( + AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int cell_id); + // ===================================================================== // Top-level certification + refinement driver // ===================================================================== @@ -195,4 +372,22 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, int edge_max_depth, bool triangulate_cut_parts = false); +/// Full local pipeline with graph preflight: +/// 1. stamp vertex signs +/// 2. certify + green/red refine until stable +/// 3. build temporary P1 cuts for graph checks on ready cells +/// 4. on graph failure, discard the temporary cut and green-refine the +/// original uncut ready cell, then recurse +/// 5. replace ready_to_cut cells by the committed cut decomposition +template +ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( + AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth, + bool triangulate_cut_parts = false, + const ReadyCellGraphOptions& graph_options = {}); + } // namespace cutcells diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp index c8117cb..cb1ef96 100644 --- a/cpp/src/curving.cpp +++ b/cpp/src/curving.cpp @@ -7,6 +7,7 @@ #include "cell_topology.h" #include "edge_root.h" +#include "geometric_quantity.h" #include "mapping.h" #include "reference_cell.h" @@ -25,6 +26,428 @@ namespace template bool solve_dense_small(std::vector A, std::vector b, int n, std::vector& x); +template +T local_dot(std::span a, std::span b) +{ + T value = T(0); + for (std::size_t i = 0; i < a.size(); ++i) + value += a[i] * b[i]; + return value; +} + +template +T local_norm(std::span a) +{ + return std::sqrt(local_dot(a, a)); +} + +template +std::array local_cross(std::span a, std::span b) +{ + return {a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]}; +} + +template +std::span vertex_ref_point(const AdaptCell& ac, int vertex_id) +{ + return std::span( + ac.vertex_coords.data() + + static_cast(vertex_id) + * static_cast(ac.tdim), + static_cast(ac.tdim)); +} + +template +bool vertex_list_contains(std::span vertices, int vertex) +{ + for (const auto v : vertices) + { + if (static_cast(v) == vertex) + return true; + } + return false; +} + +template +bool cell_contains_vertices(std::span cell_vertices, + std::span query_vertices) +{ + for (const int v : query_vertices) + { + if (!vertex_list_contains(cell_vertices, v)) + return false; + } + return true; +} + +template +std::vector local_zero_entity_vertex_ids(const AdaptCell& ac, + int local_zero_entity_id) +{ + if (local_zero_entity_id < 0 + || local_zero_entity_id >= ac.n_zero_entities()) + { + return {}; + } + + const int zdim = + ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + ac.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim == 0) + return {zid}; + + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + std::vector out; + out.reserve(verts.size()); + for (const auto v : verts) + out.push_back(static_cast(v)); + return out; +} + +template +void append_unique_host_vertex(const AdaptCell& ac, + int vertex_id, + std::vector& vertex_ids, + std::vector& coords) +{ + if (vertex_list_contains( + std::span(vertex_ids.data(), vertex_ids.size()), + vertex_id)) + { + return; + } + vertex_ids.push_back(vertex_id); + const auto p = vertex_ref_point(ac, vertex_id); + coords.insert(coords.end(), p.begin(), p.end()); +} + +template +struct LocalHostDomain +{ + int dimension = -1; + bool ordered_boundary = false; + std::vector vertices; + + bool valid(int tdim) const + { + if (dimension <= 0 || dimension > tdim) + return false; + const int nverts = + static_cast(vertices.size() / static_cast(tdim)); + return nverts >= dimension + 1; + } +}; + +template +bool face_is_zero_for_mask(const AdaptCell& ac, + std::span face_vertices, + std::uint64_t zero_mask) +{ + if (zero_mask == 0) + return false; + for (const int v : face_vertices) + { + if ((ac.zero_mask_per_vertex[static_cast(v)] & zero_mask) + != zero_mask) + { + return false; + } + } + return true; +} + +template +LocalHostDomain local_host_domain_for_zero_entity( + const AdaptCell& ac, + int local_zero_entity_id) +{ + LocalHostDomain host; + if (ac.tdim != 3 || local_zero_entity_id < 0 + || local_zero_entity_id >= ac.n_zero_entities()) + { + return host; + } + + const int zdim = + ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const auto zverts = + local_zero_entity_vertex_ids(ac, local_zero_entity_id); + if (zverts.empty()) + return host; + + const std::uint64_t zero_mask = + ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; + const int n_cells = ac.n_entities(ac.tdim); + + if (zdim == 2) + { + std::vector ids; + for (int c = 0; c < n_cells; ++c) + { + const auto cell_vertices = + ac.entity_to_vertex[ac.tdim][static_cast(c)]; + bool touches_zero_face = false; + for (const int v : zverts) + touches_zero_face = touches_zero_face + || vertex_list_contains( + cell_vertices, v); + if (!touches_zero_face) + { + continue; + } + + for (const auto v : cell_vertices) + append_unique_host_vertex(ac, static_cast(v), ids, host.vertices); + } + + if (static_cast(ids.size()) >= 4) + host.dimension = 3; + } + + return host; +} + +template +bool clip_interval_by_halfspace(std::span x0, + std::span direction, + std::span normal, + T offset, + T tol, + T& lo, + T& hi) +{ + const T a = local_dot(direction, normal); + const T b = local_dot(x0, normal) + offset; + if (std::fabs(a) <= tol) + return b >= -tol; + + const T t = (-tol - b) / a; + if (a > T(0)) + lo = std::max(lo, t); + else + hi = std::min(hi, t); + return lo <= hi; +} + +template +bool clip_line_interval_in_ordered_face(const LocalHostDomain& host, + int tdim, + std::span x0, + std::span direction, + T tol, + T& lo, + T& hi) +{ + if (tdim != 3 || !host.ordered_boundary) + return false; + const int nverts = + static_cast(host.vertices.size() / static_cast(tdim)); + if (nverts < 3) + return false; + + const auto p0 = std::span(host.vertices.data(), 3); + const auto p1 = std::span(host.vertices.data() + 3, 3); + const auto p2 = std::span(host.vertices.data() + 6, 3); + std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + auto normal = local_cross( + std::span(e0.data(), 3), + std::span(e1.data(), 3)); + const T normal_norm = + local_norm(std::span(normal.data(), 3)); + const T direction_norm = local_norm(direction); + if (normal_norm <= tol || direction_norm <= tol) + return false; + const T plane_seed = (x0[0] - p0[0]) * normal[0] + + (x0[1] - p0[1]) * normal[1] + + (x0[2] - p0[2]) * normal[2]; + const T plane_dir = local_dot( + direction, std::span(normal.data(), 3)); + if (std::fabs(plane_seed) > tol * normal_norm + || std::fabs(plane_dir) > tol * normal_norm * direction_norm) + { + return false; + } + + std::array centroid = {T(0), T(0), T(0)}; + for (int i = 0; i < nverts; ++i) + { + const auto p = std::span( + host.vertices.data() + static_cast(3 * i), 3); + for (int d = 0; d < 3; ++d) + centroid[static_cast(d)] += p[static_cast(d)]; + } + for (T& x : centroid) + x /= static_cast(nverts); + + for (int i = 0; i < nverts; ++i) + { + const auto a = std::span( + host.vertices.data() + static_cast(3 * i), 3); + const auto b = std::span( + host.vertices.data() + + static_cast(3 * ((i + 1) % nverts)), 3); + std::array edge = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; + auto inward = local_cross( + std::span(normal.data(), 3), + std::span(edge.data(), 3)); + std::array ca = { + centroid[0] - a[0], centroid[1] - a[1], centroid[2] - a[2]}; + if (local_dot( + std::span(inward.data(), 3), + std::span(ca.data(), 3)) < T(0)) + { + for (T& v : inward) + v = -v; + } + const T offset = -local_dot( + std::span(inward.data(), 3), a); + if (!clip_interval_by_halfspace( + x0, + direction, + std::span(inward.data(), 3), + offset, + tol, + lo, + hi)) + { + return false; + } + } + return lo <= hi; +} + +template +bool clip_line_interval_in_convex_hull(const LocalHostDomain& host, + int tdim, + std::span x0, + std::span direction, + T tol, + T& lo, + T& hi) +{ + const int nverts = + static_cast(host.vertices.size() / static_cast(tdim)); + if (host.dimension == 2) + return clip_line_interval_in_ordered_face( + host, tdim, x0, direction, tol, lo, hi); + + if (host.dimension != 3 || tdim != 3 || nverts < 4) + return false; + + int accepted_planes = 0; + for (int i = 0; i < nverts; ++i) + { + const auto pi = std::span( + host.vertices.data() + static_cast(3 * i), 3); + for (int j = i + 1; j < nverts; ++j) + { + const auto pj = std::span( + host.vertices.data() + static_cast(3 * j), 3); + for (int k = j + 1; k < nverts; ++k) + { + const auto pk = std::span( + host.vertices.data() + static_cast(3 * k), 3); + std::array e0 = { + pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]}; + std::array e1 = { + pk[0] - pi[0], pk[1] - pi[1], pk[2] - pi[2]}; + auto normal = local_cross( + std::span(e0.data(), 3), + std::span(e1.data(), 3)); + const T nn = local_norm( + std::span(normal.data(), 3)); + if (nn <= tol) + continue; + + bool has_pos = false; + bool has_neg = false; + for (int m = 0; m < nverts; ++m) + { + const auto pm = std::span( + host.vertices.data() + static_cast(3 * m), 3); + const T s = (pm[0] - pi[0]) * normal[0] + + (pm[1] - pi[1]) * normal[1] + + (pm[2] - pi[2]) * normal[2]; + has_pos = has_pos || s > tol * nn; + has_neg = has_neg || s < -tol * nn; + if (has_pos && has_neg) + break; + } + if (has_pos && has_neg) + continue; + if (!has_pos && !has_neg) + continue; + if (has_neg) + { + for (T& v : normal) + v = -v; + } + const T offset = -local_dot( + std::span(normal.data(), 3), pi); + if (!clip_interval_by_halfspace( + x0, + direction, + std::span(normal.data(), 3), + offset, + tol, + lo, + hi)) + { + return false; + } + ++accepted_planes; + } + } + } + + return accepted_planes > 0 && lo <= hi; +} + +template +std::vector project_to_local_host_space(const AdaptCell& ac, + int local_zero_entity_id, + std::span direction, + T tol) +{ + std::vector out(direction.begin(), direction.end()); + const auto host = local_host_domain_for_zero_entity( + ac, local_zero_entity_id); + if (!host.valid(ac.tdim) || host.dimension == ac.tdim) + return out; + + if (host.dimension == 2 && ac.tdim == 3) + { + const int nverts = static_cast( + host.vertices.size() / static_cast(ac.tdim)); + if (nverts < 3) + return out; + const auto p0 = std::span(host.vertices.data(), 3); + const auto p1 = std::span(host.vertices.data() + 3, 3); + const auto p2 = std::span(host.vertices.data() + 6, 3); + std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + auto normal = local_cross( + std::span(e0.data(), 3), + std::span(e1.data(), 3)); + const T nn = local_dot( + std::span(normal.data(), 3), + std::span(normal.data(), 3)); + if (nn <= tol * tol) + return out; + const T c = local_dot( + std::span(out.data(), out.size()), + std::span(normal.data(), 3)) / nn; + for (int d = 0; d < 3; ++d) + out[static_cast(d)] -= c * normal[static_cast(d)]; + } + + return out; +} + template struct BoundaryEdgeState { @@ -51,11 +474,17 @@ struct ProjectionStats int safe_subspace_dim = -1; CurvingProjectionMode projection_mode = CurvingProjectionMode::none; int retry_count = 0; + std::vector seed; + std::vector direction; + T clip_lo = std::numeric_limits::quiet_NaN(); + T clip_hi = std::numeric_limits::quiet_NaN(); + T root_t = std::numeric_limits::quiet_NaN(); }; template void append_node_stats(CurvedZeroEntityState& state, - const ProjectionStats& stats) + const ProjectionStats& stats, + int tdim) { state.node_iterations.push_back(static_cast(stats.iterations)); state.node_status.push_back(static_cast(stats.status)); @@ -66,6 +495,23 @@ void append_node_stats(CurvedZeroEntityState& state, state.node_safe_subspace_dim.push_back(static_cast(stats.safe_subspace_dim)); state.node_projection_mode.push_back(static_cast(stats.projection_mode)); state.node_retry_count.push_back(static_cast(stats.retry_count)); + + const T nan = std::numeric_limits::quiet_NaN(); + auto append_vector = [&](std::vector& dst, const std::vector& src) + { + for (int d = 0; d < tdim; ++d) + { + dst.push_back( + d < static_cast(src.size()) + ? src[static_cast(d)] + : nan); + } + }; + append_vector(state.node_seed, stats.seed); + append_vector(state.node_direction, stats.direction); + state.node_clip_lo.push_back(stats.clip_lo); + state.node_clip_hi.push_back(stats.clip_hi); + state.node_root_t.push_back(stats.root_t); } template @@ -223,7 +669,8 @@ void append_straight_seed_node_stats(CurvedZeroEntityState& state, append_node_stats( state, accepted_node_stats( - CurvingFailureCode::small_entity_kept_straight)); + CurvingFailureCode::small_entity_kept_straight), + ac.tdim); } template @@ -646,40 +1093,62 @@ std::vector> reference_domain_inequalities(cell::type cell_type return ineq; } +template +geom::ParentEntity zero_entity_parent_entity(const AdaptCell& ac, + int local_zero_entity_id) +{ + if (local_zero_entity_id < 0 + || local_zero_entity_id >= ac.n_zero_entities()) + { + return {-1, -1}; + } + + const int parent_dim = + ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; + int parent_id = + ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; + if (parent_dim == ac.tdim) + parent_id = -1; + return {parent_dim, parent_id}; +} + template bool host_parameter_interval(const AdaptCell& ac, + int local_zero_entity_id, std::span x0, std::span d, T tol, T& lo, T& hi) { - lo = -std::numeric_limits::infinity(); - hi = std::numeric_limits::infinity(); - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - for (const auto& row : ineq) + const auto local_host = + local_host_domain_for_zero_entity(ac, local_zero_entity_id); + if (local_host.valid(ac.tdim)) { - T ax = T(0); - T ad = T(0); - for (int k = 0; k < ac.tdim; ++k) - { - ax += row[static_cast(k)] * x0[static_cast(k)]; - ad += row[static_cast(k)] * d[static_cast(k)]; - } - const T rhs = row[3] + tol - ax; - if (std::fabs(ad) <= T(64) * std::numeric_limits::epsilon()) + T local_lo = -std::numeric_limits::infinity(); + T local_hi = std::numeric_limits::infinity(); + if (clip_line_interval_in_convex_hull( + local_host, ac.tdim, x0, d, tol, local_lo, local_hi) + && local_lo <= local_hi) { - if (rhs < T(0)) - return false; - continue; + lo = local_lo; + hi = local_hi; + return true; } - const T bound = rhs / ad; - if (ad > T(0)) - hi = std::min(hi, bound); - else - lo = std::max(lo, bound); } - return lo <= hi; + + const auto host = zero_entity_parent_entity(ac, local_zero_entity_id); + const auto interval = geom::clip_line_interval_in_parent_entity( + ac.parent_cell_type, + host, + x0, + d, + -std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + tol); + lo = interval.t0; + hi = interval.t1; + return interval.valid && lo <= hi; } template @@ -697,6 +1166,25 @@ T vec_norm(std::span a) return std::sqrt(vec_dot(a, a)); } +template +std::vector normalized_delta_direction(std::span from, + std::span to, + T tol) +{ + std::vector direction(from.size(), T(0)); + for (std::size_t i = 0; i < from.size(); ++i) + direction[i] = to[i] - from[i]; + + const T norm = vec_norm( + std::span(direction.data(), direction.size())); + if (norm <= tol) + return {}; + + for (T& value : direction) + value /= norm; + return direction; +} + template T face_normal_norm(const std::array& row, int tdim) { @@ -1021,6 +1509,28 @@ void append_unique_direction(std::vector>& directions, directions.push_back(std::move(dir)); } +template +void append_admissible_unique_direction(std::vector>& directions, + const AdaptCell& ac, + int local_zero_entity_id, + std::span raw_direction, + T tol) +{ + const auto projected = geom::admissible_direction_in_parent_frame( + ac.parent_cell_type, + zero_entity_parent_entity(ac, local_zero_entity_id), + raw_direction, + tol); + if (projected.degenerate()) + return; + auto local_projected = project_to_local_host_space( + ac, + local_zero_entity_id, + std::span(projected.value.data(), projected.value.size()), + tol); + append_unique_direction(directions, std::move(local_projected), tol); +} + template std::vector zero_entity_edge_tangent(const AdaptCell& ac, int local_zero_entity_id) @@ -1075,6 +1585,175 @@ std::vector zero_entity_face_normal(const AdaptCell& ac, return normal; } +template +bool contains_vertex(std::span vertices, std::int32_t query) +{ + for (const auto vertex : vertices) + if (vertex == query) + return true; + return false; +} + +template +std::vector adjacent_zero_face_normal_for_edge(const AdaptCell& ac, + int local_zero_entity_id, + T tol) +{ + std::vector best(static_cast(ac.tdim), T(0)); + if (ac.tdim != 3) + return best; + + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + if (zdim != 1) + return best; + + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto edge_vertices = ac.entity_to_vertex[1][static_cast(zid)]; + if (edge_vertices.size() != 2) + return best; + + const std::uint64_t edge_mask = + ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; + T best_norm = T(0); + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (ac.zero_entity_dim[static_cast(z)] != 2) + continue; + + const std::uint64_t face_mask = + ac.zero_entity_zero_mask[static_cast(z)]; + if ((face_mask & edge_mask) != face_mask) + continue; + + const int face_id = ac.zero_entity_id[static_cast(z)]; + auto face_vertices = ac.entity_to_vertex[2][static_cast(face_id)]; + if (!contains_vertex(face_vertices, edge_vertices[0]) + || !contains_vertex(face_vertices, edge_vertices[1])) + { + continue; + } + + auto normal = zero_entity_face_normal(ac, z); + const T n = vec_norm(std::span(normal.data(), normal.size())); + if (n > best_norm) + { + best_norm = n; + best = std::move(normal); + } + } + + if (best_norm <= tol) + std::fill(best.begin(), best.end(), T(0)); + return best; +} + +template +geom::VectorQuantity straight_zero_entity_normal_direction( + const AdaptCell& ac, + int local_zero_entity_id, + T tol) +{ + const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int tdim = ac.tdim; + const auto host = zero_entity_parent_entity(ac, local_zero_entity_id); + + if (zdim == 1) + { + const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; + auto verts = ac.entity_to_vertex[1][static_cast(zid)]; + if (verts.size() != 2) + return {{}, T(0), geom::Degeneracy::invalid_parent_entity}; + + std::vector a(static_cast(tdim), T(0)); + std::vector b(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + { + a[static_cast(d)] = + ac.vertex_coords[static_cast(verts[0] * tdim + d)]; + b[static_cast(d)] = + ac.vertex_coords[static_cast(verts[1] * tdim + d)]; + } + + geom::VectorQuantity raw; + if (tdim == 2) + { + raw = geom::segment_normal( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + true, + tol); + } + else if (tdim == 3) + { + if (host.dim == 2) + { + const auto face_normal = geom::parent_face_normal( + ac.parent_cell_type, host.id, true, tol); + if (face_normal.degenerate()) + return {{}, T(0), face_normal.degeneracy}; + raw = geom::in_face_segment_normal( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + std::span(face_normal.value.data(), face_normal.value.size()), + true, + tol); + } + else + { + auto face_normal = adjacent_zero_face_normal_for_edge( + ac, local_zero_entity_id, tol); + auto face_normal_quantity = geom::make_vector_quantity( + std::move(face_normal), + tol, + geom::Degeneracy::zero_projection); + if (!face_normal_quantity.degenerate()) + { + raw = geom::in_face_segment_normal( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + std::span( + face_normal_quantity.value.data(), + face_normal_quantity.value.size()), + true, + tol); + } + else + { + raw = std::move(face_normal_quantity); + } + } + } + else + { + return {std::vector(static_cast(tdim), T(0)), + T(0), + geom::Degeneracy::zero_projection}; + } + + if (raw.degenerate()) + return raw; + return geom::admissible_direction_in_parent_frame( + ac.parent_cell_type, + host, + std::span(raw.value.data(), raw.value.size()), + tol); + } + + if (zdim == 2 && tdim == 3) + { + auto normal = zero_entity_face_normal(ac, local_zero_entity_id); + return geom::admissible_direction_in_parent_frame( + ac.parent_cell_type, + host, + std::span(normal.data(), normal.size()), + tol); + } + + return {std::vector(static_cast(tdim), T(0)), + T(0), + geom::Degeneracy::zero_projection}; +} + template std::vector remove_component(std::vector direction, std::span tangent) @@ -1089,6 +1768,21 @@ std::vector remove_component(std::vector direction, return direction; } +template +void orient_with_level_set_gradient(std::vector& direction, + std::span grad) +{ + if (direction.size() != grad.size()) + return; + if (vec_dot( + std::span(direction.data(), direction.size()), + grad) < T(0)) + { + for (T& value : direction) + value = -value; + } +} + template std::vector physical_normal_reference_direction( const LevelSetCell& ls_cell, @@ -1257,13 +1951,6 @@ template T curving_level_set_value(const LevelSetCell& ls_cell, std::span ref) { - if (ls_cell.global_level_set != nullptr - && ls_cell.global_level_set->has_value() - && !ls_cell.parent_vertex_coords.empty()) - { - const auto x = level_set_physical_point(ls_cell, ref); - return ls_cell.global_level_set->value(x.data(), ls_cell.cell_id); - } return ls_cell.value(ref); } @@ -1272,27 +1959,6 @@ void curving_level_set_grad(const LevelSetCell& ls_cell, std::span ref, std::span grad_ref) { - if (ls_cell.global_level_set != nullptr - && ls_cell.global_level_set->has_gradient() - && !ls_cell.parent_vertex_coords.empty()) - { - std::vector jacobian; - if (affine_jacobian(ls_cell, jacobian)) - { - const auto x = level_set_physical_point(ls_cell, ref); - std::vector grad_phys(static_cast(ls_cell.gdim), T(0)); - ls_cell.global_level_set->grad(x.data(), ls_cell.cell_id, grad_phys.data()); - for (int a = 0; a < ls_cell.tdim; ++a) - { - T value = T(0); - for (int r = 0; r < ls_cell.gdim; ++r) - value += jacobian[static_cast(r * ls_cell.tdim + a)] - * grad_phys[static_cast(r)]; - grad_ref[static_cast(a)] = value; - } - return; - } - } ls_cell.grad(ref, grad_ref); } @@ -1392,12 +2058,17 @@ std::vector physical_host_gradient_reference_direction( return std::vector(static_cast(tdim), T(0)); } - return pull_back_physical_direction( + auto pulled = pull_back_physical_direction( std::span(jacobian.data(), jacobian.size()), gdim, tdim, std::span(direction_phys.data(), direction_phys.size()), fallback_ref); + return project_to_local_host_space( + ac, + local_zero_entity_id, + std::span(pulled.data(), pulled.size()), + std::numeric_limits::epsilon() * T(128)); } template @@ -1413,6 +2084,7 @@ bool accept_seed_if_level_sets_within_tolerance( if (active_cells.empty()) return false; + stats.seed.assign(seed.begin(), seed.end()); T max_abs_value = T(0); for (const LevelSetCell* ls_cell : active_cells) { @@ -1455,18 +2127,54 @@ std::vector> scalar_candidate_directions(const AdaptCell& ac, const std::uint32_t host_mask = structural_active_face_mask(ac, local_zero_entity_id); - // First try the local closest-point direction for the implicit geometry, - // restricted only by the structural parent host entity. Extra near-face - // constraints can overrestrict seeds on warped cut quads and create large - // tangential motion, so those are kept for fallback directions below. - append_unique_direction( - directions, std::vector(host_metric_grad.begin(), host_metric_grad.end()), tol); + auto append_host_gradient = [&]() + { + // The primary implicit-geometry direction is restricted only by the + // structural parent host entity. Extra near-face constraints can + // overrestrict seeds on warped cut quads and create large tangential + // motion, so those are kept for fallback directions below. + append_admissible_unique_direction( + directions, ac, local_zero_entity_id, host_metric_grad, tol); + }; + + auto append_straight_normal = [&]() -> bool + { + const auto normal = straight_zero_entity_normal_direction( + ac, local_zero_entity_id, tol); + if (!normal.degenerate()) + { + auto direction = normal.value; + orient_with_level_set_gradient(direction, grad); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(direction.data(), direction.size()), + tol); + return true; + } + return false; + }; + + if (options.direction_mode == CurvingDirectionMode::straight_zero_entity_normal) + { + append_straight_normal(); + append_host_gradient(); + } + else + { + append_host_gradient(); + } if (active_mask != host_mask) { - append_unique_direction( - directions, project_to_active_face_space( - ac, metric_grad, active_mask), tol); + auto d = project_to_active_face_space(ac, metric_grad, active_mask); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + tol); } if (zdim == 1) @@ -1475,21 +2183,43 @@ std::vector> scalar_candidate_directions(const AdaptCell& ac, auto d = project_to_active_face_space(ac, metric_grad, active_mask); d = remove_component( std::move(d), std::span(tangent.data(), tangent.size())); - append_unique_direction(directions, std::move(d), tol); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + tol); } else if (zdim == 2) { - auto normal = zero_entity_face_normal(ac, local_zero_entity_id); - append_unique_direction( + const auto normal = straight_zero_entity_normal_direction( + ac, local_zero_entity_id, tol); + if (!normal.degenerate()) + { + auto d = project_to_active_face_space( + ac, + std::span(normal.value.data(), normal.value.size()), + active_mask); + orient_with_level_set_gradient(d, grad); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + tol); + } + } + + { + auto d = project_to_active_face_space(ac, grad, active_mask); + append_admissible_unique_direction( directions, - project_to_active_face_space( - ac, std::span(normal.data(), normal.size()), active_mask), + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), tol); } - append_unique_direction( - directions, project_to_active_face_space(ac, grad, active_mask), tol); - const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); const int nv = cell::get_num_vertices(ac.parent_cell_type); for (int v = 0; v < nv; ++v) @@ -1499,24 +2229,37 @@ std::vector> scalar_candidate_directions(const AdaptCell& ac, ray[static_cast(d)] = seed[static_cast(d)] - parent_vertices[static_cast(v * ac.tdim + d)]; - append_unique_direction( + auto d = project_to_active_face_space( + ac, std::span(ray.data(), ray.size()), active_mask); + append_admissible_unique_direction( directions, - project_to_active_face_space( - ac, std::span(ray.data(), ray.size()), active_mask), + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), tol); } const auto basis = active_nullspace_basis(ac, active_mask); for (const auto& b : basis) - append_unique_direction(directions, b, tol); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(b.data(), b.size()), + tol); + + if (options.direction_mode == CurvingDirectionMode::level_set_gradient) + append_straight_normal(); return directions; } template bool try_scalar_line_search(const AdaptCell& ac, + int local_zero_entity_id, std::span seed, std::span> directions, + std::span gradient_at_seed, const CurvingOptions& options, Eval&& eval_on_point, std::vector& out, @@ -1525,18 +2268,43 @@ bool try_scalar_line_search(const AdaptCell& ac, const int tdim = ac.tdim; for (const auto& unit : directions) { + std::vector direction = unit; + if (direction.size() != static_cast(tdim)) + { + stats.failure_code = CurvingFailureCode::singular_gradient_system; + continue; + } + if (gradient_at_seed.size() == direction.size() + && vec_dot(gradient_at_seed, std::span( + direction.data(), direction.size())) < T(0)) + { + for (T& value : direction) + value = -value; + } + stats.seed.assign(seed.begin(), seed.end()); + stats.direction = direction; + stats.clip_lo = std::numeric_limits::quiet_NaN(); + stats.clip_hi = std::numeric_limits::quiet_NaN(); + stats.root_t = std::numeric_limits::quiet_NaN(); + T lo = T(0), hi = T(0); if (!host_parameter_interval( - ac, seed, std::span(unit.data(), unit.size()), + ac, + local_zero_entity_id, + seed, + std::span(direction.data(), direction.size()), options.domain_tol, lo, hi)) { stats.failure_code = CurvingFailureCode::no_host_interval; continue; } - lo = std::max(lo, -T(2)); - hi = std::min(hi, T(2)); + stats.clip_lo = lo; + stats.clip_hi = hi; if (!(lo <= T(0) && T(0) <= hi)) + { + stats.failure_code = CurvingFailureCode::no_host_interval; continue; + } auto eval_line = [&](T t) -> T { @@ -1544,71 +2312,104 @@ bool try_scalar_line_search(const AdaptCell& ac, for (int d = 0; d < tdim; ++d) x[static_cast(d)] = seed[static_cast(d)] - + t * unit[static_cast(d)]; + + t * direction[static_cast(d)]; return eval_on_point(std::span(x.data(), static_cast(tdim))); }; - const int samples = 48; - T best_a = T(0), best_b = T(0); - T best_dist = std::numeric_limits::infinity(); - bool have_bracket = false; - T t_prev = lo; - T f_prev = eval_line(t_prev); - for (int i = 1; i <= samples; ++i) - { - const T t_cur = lo + (hi - lo) * T(i) / T(samples); - const T f_cur = eval_line(t_cur); - if (std::fabs(f_cur) <= options.ftol) - { - out.assign(seed.begin(), seed.end()); - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += t_cur * unit[static_cast(d)]; - stats.iterations = i; - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = std::fabs(f_cur); - return true; - } - if (f_prev * f_cur <= T(0)) - { - const T dist = std::min(std::fabs(t_prev), std::fabs(t_cur)); - if (dist < best_dist) - { - best_a = t_prev; - best_b = t_cur; - best_dist = dist; - have_bracket = true; - } - } - t_prev = t_cur; - f_prev = f_cur; + const T f_seed = eval_line(T(0)); + if (std::fabs(f_seed) <= options.ftol) + { + out.assign(seed.begin(), seed.end()); + stats.iterations = 0; + stats.status = CurvingStatus::curved; + stats.failure_code = CurvingFailureCode::none; + stats.residual = std::fabs(f_seed); + stats.root_t = T(0); + return true; + } + + T a = T(0); + T b = T(0); + if (f_seed > T(0)) + { + a = lo; + b = T(0); } - if (!have_bracket) + else + { + a = T(0); + b = hi; + } + if (!(a < b)) + { + stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; + continue; + } + + const T fa = eval_line(a); + const T fb = (b == T(0)) ? f_seed : eval_line(b); + if (!(std::isfinite(fa) && std::isfinite(fb)) || fa * fb > T(0)) { stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; continue; } - const T fa = eval_line(best_a); - const T fb = eval_line(best_b); int iterations = 0; bool converged = false; - const T root_t = cell::edge_root::brent_solve( - eval_line, best_a, best_b, fa, fb, + const T linear_guess = cell::edge_root::linear_root_parameter(fa, fb); + const T initial_guess = a + (b - a) * std::clamp(linear_guess, T(0), T(1)); + const T root_t = cell::edge_root::newton_parameter( + eval_line, a, b, fa, fb, + initial_guess, options.max_iter, options.xtol, options.ftol, &iterations, &converged); + T accepted_root_t = root_t; stats.iterations = iterations; - const T residual = std::fabs(eval_line(root_t)); + T residual = std::fabs(eval_line(root_t)); stats.residual = residual; if (!converged && residual > options.ftol) { - stats.failure_code = CurvingFailureCode::brent_failed; - continue; + int fallback_iterations = 0; + bool fallback_converged = false; + try + { + const T fallback_root_t = cell::edge_root::itp_parameter( + eval_line, + a, + b, + fa, + fb, + initial_guess, + options.max_iter, + options.xtol, + options.ftol, + &fallback_iterations, + &fallback_converged); + const T fallback_residual = std::fabs(eval_line(fallback_root_t)); + if (fallback_converged || fallback_residual <= options.ftol) + { + accepted_root_t = fallback_root_t; + iterations += fallback_iterations; + residual = fallback_residual; + stats.iterations = iterations; + stats.residual = residual; + converged = true; + } + } + catch (const std::exception&) + { + } + if (!converged && residual > options.ftol) + { + stats.failure_code = CurvingFailureCode::brent_failed; + stats.root_t = accepted_root_t; + continue; + } } out.assign(seed.begin(), seed.end()); for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += root_t * unit[static_cast(d)]; + out[static_cast(d)] += accepted_root_t * direction[static_cast(d)]; const bool inside = cell::edge_root::is_inside_reference_domain( std::span(out.data(), out.size()), ac.parent_cell_type, @@ -1617,10 +2418,14 @@ bool try_scalar_line_search(const AdaptCell& ac, { stats.status = CurvingStatus::failed; stats.failure_code = CurvingFailureCode::outside_host_domain; + stats.root_t = accepted_root_t; return false; } stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; + stats.direction = direction; + stats.root_t = accepted_root_t; + stats.residual = residual; return true; } @@ -1630,6 +2435,7 @@ bool try_scalar_line_search(const AdaptCell& ac, template bool constrained_scalar_newton(const AdaptCell& ac, + int local_zero_entity_id, std::span seed, std::uint32_t active_mask, const CurvingOptions& options, @@ -1660,6 +2466,10 @@ bool constrained_scalar_newton(const AdaptCell& ac, stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; stats.residual = std::fabs(f); + stats.direction = normalized_delta_direction( + seed, + std::span(out.data(), out.size()), + options.xtol); return true; } @@ -1686,6 +2496,7 @@ bool constrained_scalar_newton(const AdaptCell& ac, T lo = T(0), hi = T(0); if (!host_parameter_interval( ac, + local_zero_entity_id, std::span(out.data(), out.size()), std::span(delta.data(), delta.size()), options.domain_tol, @@ -1749,6 +2560,10 @@ bool constrained_scalar_newton(const AdaptCell& ac, { stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; + stats.direction = normalized_delta_direction( + seed, + std::span(out.data(), out.size()), + options.xtol); return true; } stats.failure_code = CurvingFailureCode::constrained_newton_failed; @@ -1927,6 +2742,7 @@ bool scalar_project(const AdaptCell& ac, if (std::fabs(seed_value) <= options.ftol) { out.assign(seed.begin(), seed.end()); + stats.seed.assign(seed.begin(), seed.end()); stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; stats.residual = std::fabs(seed_value); @@ -1957,7 +2773,7 @@ bool scalar_project(const AdaptCell& ac, }; auto grad_on_point = [&](std::span x, std::span out_grad) { - ls_cell.grad(x, out_grad); + curving_level_set_grad(ls_cell, x, out_grad); }; std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); @@ -1977,8 +2793,11 @@ bool scalar_project(const AdaptCell& ac, active_mask, options); if (try_scalar_line_search( - ac, seed, + ac, + local_zero_entity_id, + seed, std::span>(directions.data(), directions.size()), + std::span(grad.data(), grad.size()), options, eval_on_point, out, @@ -2012,8 +2831,11 @@ bool scalar_project(const AdaptCell& ac, retry_mask, options); if (try_scalar_line_search( - ac, seed, + ac, + local_zero_entity_id, + seed, std::span>(directions.data(), directions.size()), + std::span(grad.data(), grad.size()), options, eval_on_point, out, @@ -2046,6 +2868,7 @@ bool scalar_project(const AdaptCell& ac, newton_stats.retry_count = (retry_mask != active_mask) ? 2 + attempt : 1 + attempt; if (constrained_scalar_newton( ac, + local_zero_entity_id, seed, newton_mask, options, @@ -2089,6 +2912,7 @@ bool fixed_ray_scalar_project(const AdaptCell& ac, if (std::fabs(seed_value) <= options.ftol) { out.assign(seed.begin(), seed.end()); + stats.seed.assign(seed.begin(), seed.end()); stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; stats.residual = std::fabs(seed_value); @@ -2112,47 +2936,102 @@ bool fixed_ray_scalar_project(const AdaptCell& ac, std::vector> directions; const T direction_tol = std::max(options.ftol, T(64) * std::numeric_limits::epsilon()); - append_unique_direction(directions, direction, direction_tol); - - auto normal = zero_entity_face_normal(ac, local_zero_entity_id); - append_unique_direction( - directions, - project_to_active_face_space( - ac, std::span(normal.data(), normal.size()), host_mask), - direction_tol); - append_unique_direction( - directions, - project_to_active_face_space( - ac, std::span(grad.data(), grad.size()), host_mask), - direction_tol); - append_unique_direction( - directions, - project_to_active_face_space( - ac, std::span(metric_grad.data(), metric_grad.size()), host_mask), - direction_tol); - const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); - const int nv = cell::get_num_vertices(ac.parent_cell_type); - for (int v = 0; v < nv; ++v) + auto append_gradient_direction = [&]() { - std::vector ray(static_cast(tdim), T(0)); - for (int d = 0; d < tdim; ++d) - ray[static_cast(d)] = - seed[static_cast(d)] - - parent_vertices[static_cast(v * tdim + d)]; - append_unique_direction( + append_admissible_unique_direction( directions, - project_to_active_face_space( - ac, std::span(ray.data(), ray.size()), host_mask), + ac, + local_zero_entity_id, + std::span(direction.data(), direction.size()), direction_tol); - } + }; + auto append_normal_direction = [&]() -> bool + { + const auto normal = straight_zero_entity_normal_direction( + ac, local_zero_entity_id, direction_tol); + if (!normal.degenerate()) + { + auto oriented = normal.value; + orient_with_level_set_gradient(oriented, std::span(grad.data(), grad.size())); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(oriented.data(), oriented.size()), + direction_tol); + return true; + } + return false; + }; + auto append_gradient_fallback_directions = [&]() + { + { + auto d = project_to_active_face_space( + ac, std::span(grad.data(), grad.size()), host_mask); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + direction_tol); + } + { + auto d = project_to_active_face_space( + ac, std::span(metric_grad.data(), metric_grad.size()), host_mask); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + direction_tol); + } - const auto basis = active_nullspace_basis(ac, host_mask); - for (const auto& b : basis) - append_unique_direction(directions, b, direction_tol); + const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); + const int nv = cell::get_num_vertices(ac.parent_cell_type); + for (int v = 0; v < nv; ++v) + { + std::vector ray(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + ray[static_cast(d)] = + seed[static_cast(d)] + - parent_vertices[static_cast(v * tdim + d)]; + auto d = project_to_active_face_space( + ac, std::span(ray.data(), ray.size()), host_mask); + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(d.data(), d.size()), + direction_tol); + } + + const auto basis = active_nullspace_basis(ac, host_mask); + for (const auto& b : basis) + append_admissible_unique_direction( + directions, + ac, + local_zero_entity_id, + std::span(b.data(), b.size()), + direction_tol); + }; + + if (options.direction_mode == CurvingDirectionMode::straight_zero_entity_normal) + { + append_normal_direction(); + append_gradient_direction(); + append_gradient_fallback_directions(); + } + else + { + append_gradient_direction(); + append_gradient_fallback_directions(); + append_normal_direction(); + } if (directions.empty()) { + stats.status = CurvingStatus::failed; stats.failure_code = CurvingFailureCode::singular_gradient_system; return false; } @@ -2162,9 +3041,6 @@ bool fixed_ray_scalar_project(const AdaptCell& ac, return curving_level_set_value(ls_cell, x); }; - bool found = false; - T best_step2 = std::numeric_limits::infinity(); - std::vector best_point; ProjectionStats best_stats = stats; for (const auto& unit : directions) { @@ -2172,47 +3048,35 @@ bool fixed_ray_scalar_project(const AdaptCell& ac, ProjectionStats candidate_stats = stats; if (!try_scalar_line_search( ac, + local_zero_entity_id, seed, std::span>(&unit, 1), + std::span(grad.data(), grad.size()), options, eval_on_point, candidate, candidate_stats)) { - if (!found) - best_stats = candidate_stats; + best_stats = candidate_stats; continue; } - T step2 = T(0); - for (int d = 0; d < tdim; ++d) - { - const T delta = candidate[static_cast(d)] - - seed[static_cast(d)]; - step2 += delta * delta; - } - if (!found || step2 < best_step2) - { - found = true; - best_step2 = step2; - best_point = std::move(candidate); - best_stats = candidate_stats; - } + out = std::move(candidate); + stats = candidate_stats; + stats.active_face_mask = host_mask; + stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); + stats.projection_mode = CurvingProjectionMode::safe_line; + return true; } - if (found) - { - out = std::move(best_point); - stats = best_stats; - } - else - { - stats = best_stats; - } + stats = best_stats; stats.active_face_mask = host_mask; stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); stats.projection_mode = CurvingProjectionMode::safe_line; - return found; + stats.status = CurvingStatus::failed; + if (stats.failure_code == CurvingFailureCode::none) + stats.failure_code = CurvingFailureCode::projection_failed; + return false; } template @@ -2275,6 +3139,10 @@ bool vector_project(const AdaptCell& ac, stats.projection_mode = (iter == 0) ? CurvingProjectionMode::none : CurvingProjectionMode::vector_newton; stats.retry_count = retry_count; + stats.direction = normalized_delta_direction( + seed, + std::span(out.data(), out.size()), + options.xtol); return true; } @@ -2387,6 +3255,7 @@ bool vector_project(const AdaptCell& ac, T lo = T(0), hi = T(0); if (!host_parameter_interval( ac, + local_zero_entity_id, std::span(out.data(), out.size()), std::span(delta.data(), delta.size()), options.domain_tol, @@ -2483,6 +3352,10 @@ bool vector_project(const AdaptCell& ac, { stats.status = CurvingStatus::curved; stats.failure_code = CurvingFailureCode::none; + stats.direction = normalized_delta_direction( + seed, + std::span(out.data(), out.size()), + options.xtol); return true; } stats.status = CurvingStatus::failed; @@ -2508,6 +3381,51 @@ bool project_seed_to_zero_entity(const AdaptCell& ac, ac, local_zero_entity_id, active_cells, seed, options, projected, stats); } +template +std::vector preferred_scalar_projection_direction( + const AdaptCell& ac, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span seed, + const CurvingOptions& options) +{ + if (local_zero_entity_id < 0 + || local_zero_entity_id >= ac.n_zero_entities()) + { + return {}; + } + + std::vector grad(static_cast(ac.tdim), T(0)); + curving_level_set_grad( + ls_cell, seed, std::span(grad.data(), grad.size())); + const auto metric_grad = physical_normal_reference_direction( + ls_cell, std::span(grad.data(), grad.size())); + const auto host_metric_grad = physical_host_gradient_reference_direction( + ls_cell, + ac, + local_zero_entity_id, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size())); + + std::uint32_t active_mask = structural_active_face_mask( + ac, local_zero_entity_id); + active_mask = add_near_active_faces( + ac, seed, active_mask, options.active_face_tol); + + auto directions = scalar_candidate_directions( + ac, + local_zero_entity_id, + seed, + std::span(grad.data(), grad.size()), + std::span(metric_grad.data(), metric_grad.size()), + std::span(host_metric_grad.data(), host_metric_grad.size()), + active_mask, + options); + if (directions.empty()) + return {}; + return directions.front(); +} + template bool build_hierarchical_face_nodes( CurvedZeroEntityState& state, @@ -2536,7 +3454,7 @@ bool build_hierarchical_face_nodes( state.ref_nodes.push_back( ac.vertex_coords[static_cast(vertex_id * ac.tdim + d)]); append_node_stats( - state, accepted_node_stats(CurvingFailureCode::exact_vertex)); + state, accepted_node_stats(CurvingFailureCode::exact_vertex), ac.tdim); }; auto straight_seed = [&](std::span weights) @@ -2588,11 +3506,11 @@ bool build_hierarchical_face_nodes( projected, stats)) { - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return false; } state.ref_nodes.insert(state.ref_nodes.end(), projected.begin(), projected.end()); - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return true; }; @@ -2605,7 +3523,7 @@ bool build_hierarchical_face_nodes( state.ref_nodes.push_back((T(1) - t) * x0 + t * x1); } append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); }; auto eval_curved_edge = [&](int v0, int v1, T t, std::vector& x) -> bool @@ -2652,7 +3570,7 @@ bool build_hierarchical_face_nodes( state.ref_nodes.end(), transfinite_projected.begin(), transfinite_projected.end()); - append_node_stats(state, transfinite_stats); + append_node_stats(state, transfinite_stats, ac.tdim); return true; } @@ -2665,14 +3583,14 @@ bool build_hierarchical_face_nodes( fallback_projected, fallback_stats)) { - append_node_stats(state, fallback_stats); + append_node_stats(state, fallback_stats, ac.tdim); return false; } state.ref_nodes.insert( state.ref_nodes.end(), fallback_projected.begin(), fallback_projected.end()); - append_node_stats(state, fallback_stats); + append_node_stats(state, fallback_stats, ac.tdim); return true; }; @@ -2778,7 +3696,7 @@ bool build_hierarchical_face_nodes( { ProjectionStats stats; stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return false; } if (!edge->use_curved_state) @@ -2789,7 +3707,7 @@ bool build_hierarchical_face_nodes( const auto x = eval_edge_state_at(*edge, options, v0, v1, t); state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); return true; } return append_triangle_interior_node(w); @@ -2871,7 +3789,7 @@ bool build_hierarchical_face_nodes( { ProjectionStats stats; stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); state.failure_reason = "missing or failed hierarchical curved face boundary edge"; return false; } @@ -2883,7 +3801,7 @@ bool build_hierarchical_face_nodes( const auto x = eval_edge_state_at(*edge, options, v0, v1, t); state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge)); + state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); continue; } @@ -2915,6 +3833,7 @@ void build_curved_state(CurvedZeroEntityState& state, state.failure_reason.clear(); state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.direction_mode = options.direction_mode; state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; @@ -2928,6 +3847,11 @@ void build_curved_state(CurvedZeroEntityState& state, state.node_safe_subspace_dim.clear(); state.node_projection_mode.clear(); state.node_retry_count.clear(); + state.node_seed.clear(); + state.node_direction.clear(); + state.node_clip_lo.clear(); + state.node_clip_hi.clear(); + state.node_root_t.clear(); std::vector seeds; const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; @@ -2935,7 +3859,7 @@ void build_curved_state(CurvedZeroEntityState& state, { state.ref_nodes = zero_entity_vertices(ac, local_zero_entity_id); append_node_stats( - state, accepted_node_stats(CurvingFailureCode::exact_vertex)); + state, accepted_node_stats(CurvingFailureCode::exact_vertex), ac.tdim); state.status = CurvingStatus::curved; return; } @@ -2954,7 +3878,7 @@ void build_curved_state(CurvedZeroEntityState& state, state.failure_reason = "unsupported zero-entity dimension"; ProjectionStats stats; stats.failure_code = CurvingFailureCode::unsupported_entity; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return; } @@ -2982,7 +3906,7 @@ void build_curved_state(CurvedZeroEntityState& state, seeds.size() / static_cast(std::max(ac.tdim, 1))); for (int i = 0; i < n_nodes; ++i) append_node_stats( - state, accepted_node_stats(CurvingFailureCode::none)); + state, accepted_node_stats(CurvingFailureCode::none), ac.tdim); state.status = CurvingStatus::curved; return; } @@ -3001,7 +3925,7 @@ void build_curved_state(CurvedZeroEntityState& state, state.failure_reason = e.what(); ProjectionStats stats; stats.failure_code = CurvingFailureCode::missing_level_set_cell; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return; } @@ -3045,7 +3969,7 @@ void build_curved_state(CurvedZeroEntityState& state, options, projected, stats); - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); if (!ok) { @@ -3085,6 +4009,128 @@ std::string_view node_family_name(NodeFamily family) return "unknown"; } +CurvingDirectionMode direction_mode_from_string(std::string_view name) +{ + if (name == "straight_zero_entity_normal" + || name == "straight-zero-entity-normal" + || name == "zero_entity_normal" + || name == "zero-entity-normal" + || name == "straight_normal" + || name == "straight-normal" + || name == "normal") + { + return CurvingDirectionMode::straight_zero_entity_normal; + } + if (name == "level_set_gradient" + || name == "level-set-gradient" + || name == "gradient") + { + return CurvingDirectionMode::level_set_gradient; + } + throw std::invalid_argument("unknown curving direction mode"); +} + +std::string_view direction_mode_name(CurvingDirectionMode mode) +{ + switch (mode) + { + case CurvingDirectionMode::straight_zero_entity_normal: + return "straight_zero_entity_normal"; + case CurvingDirectionMode::level_set_gradient: + return "level_set_gradient"; + } + return "unknown"; +} + +template +T reference_level_set_value(const LevelSetCell& ls_cell, + std::span ref) +{ + return curving_level_set_value(ls_cell, ref); +} + +template +void reference_level_set_gradient(const LevelSetCell& ls_cell, + std::span ref, + std::span grad_ref) +{ + curving_level_set_grad(ls_cell, ref, grad_ref); +} + +template +ProjectionDiagnostic make_projection_diagnostic( + bool accepted, + const std::vector& projected, + const ProjectionStats& stats) +{ + ProjectionDiagnostic diagnostic; + diagnostic.accepted = accepted; + diagnostic.status = stats.status; + diagnostic.failure_code = stats.failure_code; + diagnostic.projection_mode = stats.projection_mode; + diagnostic.iterations = stats.iterations; + diagnostic.residual = stats.residual; + diagnostic.active_face_mask = stats.active_face_mask; + diagnostic.closest_face_id = stats.closest_face_id; + diagnostic.safe_subspace_dim = stats.safe_subspace_dim; + diagnostic.retry_count = stats.retry_count; + diagnostic.projected = projected; + diagnostic.seed = stats.seed; + diagnostic.direction = stats.direction; + diagnostic.clip_lo = stats.clip_lo; + diagnostic.clip_hi = stats.clip_hi; + diagnostic.root_t = stats.root_t; + return diagnostic; +} + +template +ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell& ac, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span seed, + const CurvingOptions& options) +{ + std::array*, 1> active_cells = {&ls_cell}; + std::vector projected; + ProjectionStats stats; + bool accepted = false; + try + { + accepted = project_seed_to_zero_entity( + ac, + local_zero_entity_id, + std::span* const>( + active_cells.data(), active_cells.size()), + seed, + options, + projected, + stats); + } + catch (const std::exception&) + { + accepted = false; + if (stats.failure_code == CurvingFailureCode::none) + stats.failure_code = CurvingFailureCode::projection_failed; + stats.status = CurvingStatus::failed; + } + + if (accepted && stats.direction.empty()) + { + stats.direction = normalized_delta_direction( + seed, + std::span(projected.data(), projected.size()), + options.xtol); + } + if (stats.direction.empty()) + { + stats.direction = preferred_scalar_projection_direction( + ac, local_zero_entity_id, ls_cell, seed, options); + } + + return make_projection_diagnostic(accepted, projected, stats); +} + template void rebuild_identity(CurvingData& curving, std::span parent_cell_ids, @@ -3203,12 +4249,14 @@ const CurvedZeroEntityState& ensure_curved( state.status == CurvingStatus::curved && state.geometry_order == options.geometry_order && state.node_family == options.node_family + && state.direction_mode == options.direction_mode && state.small_entity_tol == options.small_entity_tol && state.zero_entity_version == ac.zero_entity_version; const bool failed = state.status == CurvingStatus::failed && state.geometry_order == options.geometry_order && state.node_family == options.node_family + && state.direction_mode == options.direction_mode && state.small_entity_tol == options.small_entity_tol && state.zero_entity_version == ac.zero_entity_version; if (!valid && !failed) @@ -3225,6 +4273,7 @@ const CurvedZeroEntityState& ensure_curved( state.failure_reason = "hierarchical zero-face curving requires boundary zero edges"; state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.direction_mode = options.direction_mode; state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[ @@ -3239,9 +4288,14 @@ const CurvedZeroEntityState& ensure_curved( state.node_safe_subspace_dim.clear(); state.node_projection_mode.clear(); state.node_retry_count.clear(); + state.node_seed.clear(); + state.node_direction.clear(); + state.node_clip_lo.clear(); + state.node_clip_hi.clear(); + state.node_root_t.clear(); ProjectionStats stats; stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return state; } @@ -3266,6 +4320,7 @@ const CurvedZeroEntityState& ensure_curved( state.failure_reason = "hierarchical zero-face boundary edge curving failed"; state.geometry_order = options.geometry_order; state.node_family = options.node_family; + state.direction_mode = options.direction_mode; state.small_entity_tol = options.small_entity_tol; state.zero_entity_version = ac.zero_entity_version; state.zero_mask = ac.zero_entity_zero_mask[ @@ -3280,9 +4335,14 @@ const CurvedZeroEntityState& ensure_curved( state.node_safe_subspace_dim.clear(); state.node_projection_mode.clear(); state.node_retry_count.clear(); + state.node_seed.clear(); + state.node_direction.clear(); + state.node_clip_lo.clear(); + state.node_clip_hi.clear(); + state.node_root_t.clear(); ProjectionStats stats; stats.failure_code = CurvingFailureCode::boundary_edge_failed; - append_node_stats(state, stats); + append_node_stats(state, stats, ac.tdim); return state; } } @@ -3328,6 +4388,37 @@ void ensure_all_curved(CurvingData& curving, } } +template double reference_level_set_value( + const LevelSetCell&, std::span); +template float reference_level_set_value( + const LevelSetCell&, std::span); +template double reference_level_set_value( + const LevelSetCell&, std::span); +template float reference_level_set_value( + const LevelSetCell&, std::span); + +template void reference_level_set_gradient( + const LevelSetCell&, std::span, std::span); +template void reference_level_set_gradient( + const LevelSetCell&, std::span, std::span); +template void reference_level_set_gradient( + const LevelSetCell&, std::span, std::span); +template void reference_level_set_gradient( + const LevelSetCell&, std::span, std::span); + +template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell&, int, const LevelSetCell&, + std::span, const CurvingOptions&); +template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell&, int, const LevelSetCell&, + std::span, const CurvingOptions&); +template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell&, int, const LevelSetCell&, + std::span, const CurvingOptions&); +template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell&, int, const LevelSetCell&, + std::span, const CurvingOptions&); + template void rebuild_identity(CurvingData&, std::span, std::span>); template void rebuild_identity(CurvingData&, std::span, std::span>); template void rebuild_identity(CurvingData&, std::span, std::span>); diff --git a/cpp/src/curving.h b/cpp/src/curving.h index f544a5d..75b768f 100644 --- a/cpp/src/curving.h +++ b/cpp/src/curving.h @@ -67,11 +67,19 @@ enum class CurvingProjectionMode : std::uint8_t vector_newton = 4 }; +enum class CurvingDirectionMode : std::uint8_t +{ + straight_zero_entity_normal = 0, + level_set_gradient = 1 +}; + template struct CurvingOptions { int geometry_order = 2; NodeFamily node_family = NodeFamily::gll; + CurvingDirectionMode direction_mode = + CurvingDirectionMode::level_set_gradient; int max_iter = 32; T xtol = T(1e-12); T ftol = std::sqrt(std::numeric_limits::epsilon()); @@ -91,6 +99,8 @@ struct CurvedZeroEntityState CurvingStatus status = CurvingStatus::not_built; int geometry_order = -1; NodeFamily node_family = NodeFamily::gll; + CurvingDirectionMode direction_mode = + CurvingDirectionMode::level_set_gradient; T small_entity_tol = T(0); std::uint32_t zero_entity_version = 0; std::uint64_t zero_mask = 0; @@ -112,6 +122,11 @@ struct CurvedZeroEntityState std::vector node_safe_subspace_dim; std::vector node_projection_mode; std::vector node_retry_count; + std::vector node_seed; + std::vector node_direction; + std::vector node_clip_lo; + std::vector node_clip_hi; + std::vector node_root_t; }; struct CurvingIdentity @@ -124,6 +139,27 @@ struct CurvingIdentity std::uint64_t zero_mask = 0; }; +template +struct ProjectionDiagnostic +{ + bool accepted = false; + CurvingStatus status = CurvingStatus::failed; + CurvingFailureCode failure_code = CurvingFailureCode::projection_failed; + CurvingProjectionMode projection_mode = CurvingProjectionMode::none; + int iterations = 0; + T residual = std::numeric_limits::infinity(); + std::uint32_t active_face_mask = 0; + int closest_face_id = -1; + int safe_subspace_dim = -1; + int retry_count = 0; + std::vector projected; + std::vector seed; + std::vector direction; + T clip_lo = std::numeric_limits::quiet_NaN(); + T clip_hi = std::numeric_limits::quiet_NaN(); + T root_t = std::numeric_limits::quiet_NaN(); +}; + template struct CurvingData { @@ -151,6 +187,25 @@ struct CurvingData NodeFamily node_family_from_string(std::string_view name); std::string_view node_family_name(NodeFamily family); +CurvingDirectionMode direction_mode_from_string(std::string_view name); +std::string_view direction_mode_name(CurvingDirectionMode mode); + +template +T reference_level_set_value(const LevelSetCell& ls_cell, + std::span ref); + +template +void reference_level_set_gradient(const LevelSetCell& ls_cell, + std::span ref, + std::span grad_ref); + +template +ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( + const AdaptCell& ac, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span seed, + const CurvingOptions& options); template void rebuild_identity(CurvingData& curving, diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index bddb568..e6cc19c 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -281,6 +281,54 @@ void refresh_adapt_cell_semantics( recompute_active_level_set_masks(ac, total_num_level_sets); } +template +void merge_graph_diagnostics(ReadyCellGraphDiagnostics& dst, + const ReadyCellGraphDiagnostics& src) +{ + dst.accepted = dst.accepted && src.accepted; + dst.checked_cells += src.checked_cells; + dst.checked_edges += src.checked_edges; + dst.checked_faces += src.checked_faces; + dst.failed_checks += src.failed_checks; + dst.graph_refinements += src.graph_refinements; + if (dst.first_failed_cell < 0 && src.first_failed_cell >= 0) + { + dst.first_failed_cell = src.first_failed_cell; + dst.first_failure_reason = src.first_failure_reason; + } + if (dst.first_failed_projection_seed.empty() + && !src.first_failed_projection_seed.empty()) + { + dst.first_failed_projection_seed = src.first_failed_projection_seed; + dst.first_failed_projection_direction = + src.first_failed_projection_direction; + dst.first_failed_projection_clip_lo = + src.first_failed_projection_clip_lo; + dst.first_failed_projection_clip_hi = + src.first_failed_projection_clip_hi; + dst.first_failed_projection_root_t = + src.first_failed_projection_root_t; + } + dst.min_true_transversality = + std::min(dst.min_true_transversality, src.min_true_transversality); + dst.min_host_normal_alignment = + std::min(dst.min_host_normal_alignment, src.min_host_normal_alignment); + dst.max_drift_amplification = + std::max(dst.max_drift_amplification, src.max_drift_amplification); + dst.max_relative_correction_distance = + std::max(dst.max_relative_correction_distance, + src.max_relative_correction_distance); + dst.max_relative_tangential_shift = + std::max(dst.max_relative_tangential_shift, + src.max_relative_tangential_shift); + dst.min_edge_gap_ratio = + std::min(dst.min_edge_gap_ratio, src.min_edge_gap_ratio); + dst.min_face_area_ratio = + std::min(dst.min_face_area_ratio, src.min_face_area_ratio); + dst.zero_entities.insert( + dst.zero_entities.end(), src.zero_entities.begin(), src.zero_entities.end()); +} + } // anonymous namespace // ===================================================================== @@ -291,7 +339,8 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const LevelSetFunction& ls, - bool triangulate_cut_parts) + bool triangulate_cut_parts, + const ReadyCellGraphOptions& graph_options) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -344,10 +393,11 @@ cut(const MeshView& mesh, // AdaptCell AdaptCell ac = make_adapt_cell(mesh, ci); - certify_refine_and_process_ready_cells( + ReadyCellGraphDiagnostics graph_diag = + certify_refine_graph_check_and_process_ready_cells( ac, hc.level_set_cells.back(), /*level_set_id=*/0, /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20, - triangulate_cut_parts); + triangulate_cut_parts, graph_options); { const std::array processed_ids = {0}; const auto* processed_cell = &hc.level_set_cells.back(); @@ -364,10 +414,17 @@ cut(const MeshView& mesh, if (ac.tdim == 3) build_faces(ac); rebuild_zero_entity_inventory(ac); + populate_committed_zero_entity_graph_diagnostics( + ac, + hc.level_set_cells.back(), + /*level_set_id=*/0, + graph_options, + graph_diag); hc.adapt_cells.push_back(std::move(ac)); // Single LS: bit 0 is always set. hc.active_level_set_mask.push_back(std::uint64_t(1)); + hc.graph_diagnostics.push_back(graph_diag); hc.parent_cell_ids.push_back(ci); } @@ -383,7 +440,8 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const std::vector>& level_sets, - bool triangulate_cut_parts) + bool triangulate_cut_parts, + const ReadyCellGraphOptions& graph_options) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -479,13 +537,16 @@ cut(const MeshView& mesh, // Process intersecting level sets recursively (input order). // std::uint64_t cell_active_mask = 0; + ReadyCellGraphDiagnostics graph_diag; for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) { const int li = intersected_ls_indices[k]; - certify_refine_and_process_ready_cells( - ac, intersected_ls_cells[k], li, - /*max_iterations=*/8, T(1e-12), T(1e-12), - /*edge_max_depth=*/20, triangulate_cut_parts); + const ReadyCellGraphDiagnostics one_ls_graph = + certify_refine_graph_check_and_process_ready_cells( + ac, intersected_ls_cells[k], li, + /*max_iterations=*/8, T(1e-12), T(1e-12), + /*edge_max_depth=*/20, triangulate_cut_parts, graph_options); + merge_graph_diagnostics(graph_diag, one_ls_graph); // New vertices created while processing level set li must be // reclassified for all already-processed level sets. @@ -511,6 +572,21 @@ cut(const MeshView& mesh, nls, T(1e-12)); rebuild_zero_entity_inventory(ac); + graph_diag.zero_entities.clear(); + for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) + { + ReadyCellGraphDiagnostics zero_entity_graph; + populate_committed_zero_entity_graph_diagnostics( + ac, + intersected_ls_cells[k], + intersected_ls_indices[k], + graph_options, + zero_entity_graph); + graph_diag.zero_entities.insert( + graph_diag.zero_entities.end(), + zero_entity_graph.zero_entities.begin(), + zero_entity_graph.zero_entities.end()); + } // Persist only level sets actively changing sign in this parent cell. // Non-active level sets may still touch a vertex/face, but they are not @@ -520,6 +596,7 @@ cut(const MeshView& mesh, hc.ls_offsets.push_back( static_cast(hc.level_set_cells.size())); hc.active_level_set_mask.push_back(cell_active_mask); + hc.graph_diagnostics.push_back(graph_diag); hc.adapt_cells.push_back(std::move(ac)); hc.parent_cell_ids.push_back(ci); @@ -644,29 +721,37 @@ HOMeshPart select_part(const HOCutCells& cut_cells, // cut() single LS template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool); +cut(const MeshView&, const LevelSetFunction&, bool, + const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool); +cut(const MeshView&, const LevelSetFunction&, bool, + const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool); +cut(const MeshView&, const LevelSetFunction&, bool, + const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool); +cut(const MeshView&, const LevelSetFunction&, bool, + const ReadyCellGraphOptions&); // cut() multi LS template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&, bool); +cut(const MeshView&, const std::vector>&, + bool, const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&, bool); +cut(const MeshView&, const std::vector>&, + bool, const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&, bool); +cut(const MeshView&, const std::vector>&, + bool, const ReadyCellGraphOptions&); template std::pair, BackgroundMeshData> -cut(const MeshView&, const std::vector>&, bool); +cut(const MeshView&, const std::vector>&, + bool, const ReadyCellGraphOptions&); // select_part() template HOMeshPart diff --git a/cpp/src/ho_cut_mesh.h b/cpp/src/ho_cut_mesh.h index cf14297..23337b2 100644 --- a/cpp/src/ho_cut_mesh.h +++ b/cpp/src/ho_cut_mesh.h @@ -13,6 +13,7 @@ #include "adapt_cell.h" #include "cell_flags.h" +#include "cell_certification.h" #include "curving.h" #include "level_set.h" #include "level_set_cell.h" @@ -89,6 +90,10 @@ struct HOCutCells /// Size = num_cut_cells. std::vector active_level_set_mask; + /// Aggregated graph preflight diagnostics for each cut cell. These are + /// collected before the committed cut decomposition is built. + std::vector> graph_diagnostics; + /// Central curved zero-entity identity/cache data. /// Mutable because HOMeshPart is a const view but curving is a lazy cache. mutable curving::CurvingData curving; @@ -148,7 +153,8 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const LevelSetFunction& ls, - bool triangulate_cut_parts = false); + bool triangulate_cut_parts = false, + const ReadyCellGraphOptions& graph_options = {}); /// Build HOCutCells and BackgroundMeshData from a mesh and multiple level sets. /// @@ -159,7 +165,8 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const std::vector>& level_sets, - bool triangulate_cut_parts = true); + bool triangulate_cut_parts = true, + const ReadyCellGraphOptions& graph_options = {}); // ===================================================================== // select_part() — builds HOMeshPart diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index c8d8325..9f89f1a 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -1429,6 +1429,46 @@ void vertex_state_for_level_set(const AdaptCell& ac, is_positive = !is_zero && !is_negative; } +template +bool graph_checks_allow_curving(const HOMeshPart& part, int cut_cell_id) +{ + if (!part.cut_cells) + return true; + + const auto& diagnostics = part.cut_cells->graph_diagnostics; + if (cut_cell_id < 0 + || cut_cell_id >= static_cast(diagnostics.size())) + { + return true; + } + return diagnostics[static_cast(cut_cell_id)].accepted; +} + +template +bool graph_checks_allow_zero_entity_curving(const HOMeshPart& part, + int cut_cell_id, + int local_zero_entity_id) +{ + if (!part.cut_cells) + return true; + + const auto& diagnostics = part.cut_cells->graph_diagnostics; + if (cut_cell_id < 0 + || cut_cell_id >= static_cast(diagnostics.size())) + { + return true; + } + + const auto& cell_diag = diagnostics[static_cast(cut_cell_id)]; + for (const auto& record : cell_diag.zero_entities) + { + if (record.local_zero_entity_id == local_zero_entity_id) + return record.accepted; + } + + return true; +} + template const curving::CurvedZeroEntityState* accepted_curved_state( const HOMeshPart& part, @@ -1436,6 +1476,10 @@ const curving::CurvedZeroEntityState* accepted_curved_state( int local_zero_entity_id, const curving::CurvingOptions& options) { + if (!graph_checks_allow_zero_entity_curving( + part, cut_cell_id, local_zero_entity_id)) + return nullptr; + const auto& state = curving::ensure_curved( part.cut_cells->curving, std::span(part.cut_cells->parent_cell_ids), @@ -1794,6 +1838,51 @@ std::vector selected_entities(const HOMeshPart& part, return entities; } +} // namespace + +template +std::vector selected_zero_entity_infos( + const HOMeshPart& part) +{ + if (!part.cut_cells) + throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); + + std::vector out; + for (std::int32_t cut_id : part.cut_cell_ids) + { + if (cut_id < 0 + || cut_id >= static_cast(part.cut_cells->adapt_cells.size())) + { + continue; + } + + const auto& adapt_cell = + part.cut_cells->adapt_cells[static_cast(cut_id)]; + const auto entities = selected_entities(part, adapt_cell, cut_id); + const auto parent_cell_id = + part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + for (const auto& entity : entities) + { + if (entity.zero_entity_index < 0) + continue; + + SelectedZeroEntityInfo info; + info.cut_cell_id = cut_id; + info.parent_cell_id = static_cast(parent_cell_id); + info.local_zero_entity_id = + static_cast(entity.zero_entity_index); + info.dimension = static_cast( + adapt_cell.zero_entity_dim[ + static_cast(entity.zero_entity_index)]); + out.push_back(info); + } + } + return out; +} + +namespace +{ + template std::vector entity_reference_coords(const AdaptCell& adapt_cell, std::span entity_vertices) @@ -2705,16 +2794,24 @@ void append_curved_entity(CurvedVTUGrid& out, int status = 0; if (entity.zero_entity_index >= 0) { - const auto& state = curving::ensure_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - cut_cell_id, - entity.zero_entity_index, - options); - status = static_cast(state.status); + if (graph_checks_allow_zero_entity_curving( + part, cut_cell_id, entity.zero_entity_index)) + { + const auto& state = curving::ensure_curved( + part.cut_cells->curving, + std::span(part.cut_cells->parent_cell_ids), + std::span>(part.cut_cells->adapt_cells), + std::span>(part.cut_cells->level_set_cells), + std::span(part.cut_cells->ls_offsets), + cut_cell_id, + entity.zero_entity_index, + options); + status = static_cast(state.status); + } + else + { + status = static_cast(curving::CurvingStatus::failed); + } } else if (!map.curved_faces.empty() || !map.curved_edges.empty()) { @@ -2985,23 +3082,11 @@ template mesh::CutMesh visualization_mesh(const HOMeshPart& part, bool include_uncut_cells, int geometry_order, - curving::NodeFamily node_family) + curving::NodeFamily node_family, + curving::CurvingDirectionMode direction_mode) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - if (geometry_order > 1) - { - curving::CurvingOptions options; - options.geometry_order = geometry_order; - options.node_family = node_family; - curving::ensure_all_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - options); - } mesh::CutMesh out; out._gdim = part.bg->mesh->gdim; @@ -3030,7 +3115,8 @@ quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, bool include_uncut_cells, int geometry_order, - curving::NodeFamily node_family) + curving::NodeFamily node_family, + curving::CurvingDirectionMode direction_mode) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); @@ -3040,13 +3126,7 @@ quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, { options.geometry_order = geometry_order; options.node_family = construction_node_family(geometry_order, node_family); - curving::ensure_all_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - options); + options.direction_mode = direction_mode; } mesh::CutMesh unused_mesh; @@ -3072,7 +3152,8 @@ template CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, bool include_uncut_cells, int geometry_order, - curving::NodeFamily node_family) + curving::NodeFamily node_family, + curving::CurvingDirectionMode direction_mode) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); @@ -3082,13 +3163,7 @@ CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, curving::CurvingOptions options; options.geometry_order = geometry_order; options.node_family = construction_node_family(geometry_order, node_family); - curving::ensure_all_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - options); + options.direction_mode = direction_mode; CurvedVTUGrid out; out.gdim = part.bg->mesh->gdim; @@ -3128,30 +3203,51 @@ CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, } template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); + +template std::vector selected_zero_entity_infos( + const HOMeshPart&); +template std::vector selected_zero_entity_infos( + const HOMeshPart&); +template std::vector selected_zero_entity_infos( + const HOMeshPart&); +template std::vector selected_zero_entity_infos( + const HOMeshPart&); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily); + const HOMeshPart&, int, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily); + const HOMeshPart&, int, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily); + const HOMeshPart&, int, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily); + const HOMeshPart&, int, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily); + const HOMeshPart&, bool, int, curving::NodeFamily, + curving::CurvingDirectionMode); } // namespace cutcells::output diff --git a/cpp/src/ho_mesh_part_output.h b/cpp/src/ho_mesh_part_output.h index 4f5516a..2fb37b3 100644 --- a/cpp/src/ho_mesh_part_output.h +++ b/cpp/src/ho_mesh_part_output.h @@ -35,23 +35,41 @@ struct CurvedVTUGrid std::vector curving_status; }; +struct SelectedZeroEntityInfo +{ + std::int32_t cut_cell_id = -1; + std::int32_t parent_cell_id = -1; + std::int32_t local_zero_entity_id = -1; + std::int32_t dimension = -1; +}; + +template +std::vector selected_zero_entity_infos( + const HOMeshPart& part); + template mesh::CutMesh visualization_mesh(const HOMeshPart& part, bool include_uncut_cells, int geometry_order = -1, - curving::NodeFamily node_family = curving::NodeFamily::gll); + curving::NodeFamily node_family = curving::NodeFamily::gll, + curving::CurvingDirectionMode direction_mode = + curving::CurvingDirectionMode::straight_zero_entity_normal); template quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, bool include_uncut_cells, int geometry_order = -1, - curving::NodeFamily node_family = curving::NodeFamily::gll); + curving::NodeFamily node_family = curving::NodeFamily::gll, + curving::CurvingDirectionMode direction_mode = + curving::CurvingDirectionMode::straight_zero_entity_normal); template CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, bool include_uncut_cells, int geometry_order, - curving::NodeFamily node_family = curving::NodeFamily::lagrange); + curving::NodeFamily node_family = curving::NodeFamily::lagrange, + curving::CurvingDirectionMode direction_mode = + curving::CurvingDirectionMode::straight_zero_entity_normal); } // namespace cutcells::output diff --git a/cpp/src/level_set_cell.cpp b/cpp/src/level_set_cell.cpp index 60513fc..a67a83b 100644 --- a/cpp/src/level_set_cell.cpp +++ b/cpp/src/level_set_cell.cpp @@ -518,12 +518,20 @@ T LevelSetCell::value(std::span xi) const std::span(bernstein_coeffs), xi); } - // Fallback: delegate to the global level set value function (in physical - // coordinates). This path requires an external mapping from reference to - // physical space, which is not yet wired up here. + // Analytical fallback: delegate to the global level-set value in physical + // coordinates after mapping xi to x. + if (global_level_set != nullptr + && global_level_set->has_value() + && !parent_vertex_coords.empty()) + { + const auto x = cell::push_forward_affine_map( + cell_type, parent_vertex_coords, gdim, xi); + return global_level_set->value(x.data(), cell_id); + } + throw std::runtime_error( "LevelSetCell::value: no Bernstein coefficients available " - "and analytical fallback not yet implemented"); + "and no analytical fallback available"); } // --------------------------------------------------------------------------- @@ -533,7 +541,14 @@ T LevelSetCell::value(std::span xi) const template void LevelSetCell::grad(std::span xi, std::span g) const { - // Polynomial backend: gradient of the Bernstein expansion + if (static_cast(g.size()) != tdim) + { + throw std::runtime_error( + "LevelSetCell::grad: output gradient has wrong dimension"); + } + + // Polynomial backend: gradient of the Bernstein expansion in reference + // coordinates. This is the gradient of the local approximation phi_h. if (!bernstein_coeffs.empty()) { bernstein::gradient( @@ -542,9 +557,43 @@ void LevelSetCell::grad(std::span xi, std::span g) const return; } + // Analytical fallback: LevelSetFunction::grad is physical dphi/dx. + // Pull it back to the parent reference element as dphi/dxi = J^T dphi/dx. + if (global_level_set != nullptr + && global_level_set->has_gradient() + && !parent_vertex_coords.empty()) + { + const auto x = cell::push_forward_affine_map( + cell_type, parent_vertex_coords, gdim, xi); + std::vector grad_phys(static_cast(gdim), T(0)); + global_level_set->grad(x.data(), cell_id, grad_phys.data()); + + const auto cols = cell::jacobian_col_indices(cell_type); + for (int a = 0; a < tdim; ++a) + { + const int va = cols[static_cast(a)]; + if (va < 0) + { + throw std::runtime_error( + "LevelSetCell::grad: invalid affine Jacobian column"); + } + T value = T(0); + for (int r = 0; r < gdim; ++r) + { + const T j_ra = + parent_vertex_coords[ + static_cast(va * gdim + r)] + - parent_vertex_coords[static_cast(r)]; + value += j_ra * grad_phys[static_cast(r)]; + } + g[static_cast(a)] = value; + } + return; + } + throw std::runtime_error( "LevelSetCell::grad: no Bernstein coefficients available " - "and analytical fallback not yet implemented"); + "and no analytical fallback available"); } // --------------------------------------------------------------------------- diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index d8032d9..5630988 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -221,12 +221,19 @@ def _load_cpp_module(): fill_all_vertex_signs_from_level_set = _cutcellscpp.fill_all_vertex_signs_from_level_set classify_leaf_cells = _cutcellscpp.classify_leaf_cells process_ready_to_cut_cells = _cutcellscpp.process_ready_to_cut_cells +check_ready_to_cut_cell_graphs = _cutcellscpp.check_ready_to_cut_cell_graphs +refine_ready_cell_on_largest_midpoint_value = ( + _cutcellscpp.refine_ready_cell_on_largest_midpoint_value +) refine_green_on_multiple_root_edges = _cutcellscpp.refine_green_on_multiple_root_edges refine_red_on_ambiguous_cells = _cutcellscpp.refine_red_on_ambiguous_cells certify_and_refine = _cutcellscpp.certify_and_refine certify_refine_and_process_ready_cells = ( _cutcellscpp.certify_refine_and_process_ready_cells ) +certify_refine_graph_check_and_process_ready_cells = ( + _cutcellscpp.certify_refine_graph_check_and_process_ready_cells +) cut = _cutcellscpp.cut higher_order_cut = _cutcellscpp.higher_order_cut create_cut_mesh = _cutcellscpp.create_cut_mesh @@ -257,6 +264,7 @@ def _load_cpp_module(): ) try: + from . import triangulation_analysis as triangulation_analysis from .triangulation_analysis import ( CellCase, analyze_all_cases, diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index d16cebd..c5f265a 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "../../cpp/src/cell_types.h" #include "../../cpp/src/cut_cell.h" @@ -56,6 +57,111 @@ T default_sqrt_epsilon_tol() return std::sqrt(std::numeric_limits::epsilon()); } +cutcells::GraphProjectionMode graph_projection_mode_from_string( + std::string_view name) +{ + if (name == "straight_zero_entity_normal" + || name == "zero_entity_normal" + || name == "straight_normal" + || name == "normal") + { + return cutcells::GraphProjectionMode::straight_zero_entity_normal; + } + if (name == "level_set_gradient" + || name == "level-set-gradient" + || name == "gradient") + { + return cutcells::GraphProjectionMode::level_set_gradient; + } + throw std::invalid_argument("unknown graph projection mode"); +} + +cutcells::GraphRefinementMode graph_refinement_mode_from_string( + std::string_view name) +{ + if (name == "green_edge" + || name == "green" + || name == "edge") + { + return cutcells::GraphRefinementMode::green_edge; + } + if (name == "red_failed_cell" + || name == "red" + || name == "cell") + { + return cutcells::GraphRefinementMode::red_failed_cell; + } + throw std::invalid_argument("unknown graph refinement mode"); +} + +template +cutcells::ReadyCellGraphOptions make_graph_options( + std::string_view projection_direction, + std::string_view refinement_mode, + int max_refinements, + T max_relative_correction_distance, + T max_relative_tangential_shift, + T max_drift_amplification, + T min_host_normal_alignment, + T min_level_set_gradient_host_alignment, + bool enabled) +{ + cutcells::ReadyCellGraphOptions options; + options.enabled = enabled; + options.projection_mode = graph_projection_mode_from_string(projection_direction); + options.refinement_mode = graph_refinement_mode_from_string(refinement_mode); + options.max_refinements = max_refinements; + options.criteria.max_relative_correction_distance = + max_relative_correction_distance; + options.criteria.max_relative_tangential_shift = + max_relative_tangential_shift; + options.criteria.max_drift_amplification = max_drift_amplification; + options.criteria.min_host_normal_alignment = min_host_normal_alignment; + options.min_level_set_gradient_host_alignment = + min_level_set_gradient_host_alignment; + return options; +} + +template +nb::dict graph_diagnostics_to_dict( + const cutcells::ReadyCellGraphDiagnostics& diagnostics) +{ + nb::dict out; + out["accepted"] = diagnostics.accepted; + out["checked_cells"] = diagnostics.checked_cells; + out["checked_edges"] = diagnostics.checked_edges; + out["checked_faces"] = diagnostics.checked_faces; + out["failed_checks"] = diagnostics.failed_checks; + out["graph_refinements"] = diagnostics.graph_refinements; + out["first_failed_cell"] = diagnostics.first_failed_cell; + out["first_failure_reason"] = std::string( + cutcells::graph_criteria::failure_reason_name( + diagnostics.first_failure_reason)); + out["min_true_transversality"] = diagnostics.min_true_transversality; + out["min_host_normal_alignment"] = diagnostics.min_host_normal_alignment; + out["max_drift_amplification"] = diagnostics.max_drift_amplification; + out["max_relative_correction_distance"] = + diagnostics.max_relative_correction_distance; + out["max_relative_tangential_shift"] = + diagnostics.max_relative_tangential_shift; + out["min_edge_gap_ratio"] = diagnostics.min_edge_gap_ratio; + out["min_face_area_ratio"] = diagnostics.min_face_area_ratio; + out["min_surface_jacobian_ratio"] = diagnostics.min_face_area_ratio; + out["min_level_set_gradient_host_alignment"] = + diagnostics.min_level_set_gradient_host_alignment; + out["first_failed_face_triangle_index"] = + diagnostics.first_failed_face_triangle_index; + out["first_failed_face_area_ratio"] = + diagnostics.first_failed_face_area_ratio; + out["first_failed_surface_jacobian_ratio"] = + diagnostics.first_failed_face_area_ratio; + out["first_requested_refinement_entity_dim"] = + diagnostics.first_requested_refinement_entity_dim; + out["first_requested_refinement_entity_id"] = + diagnostics.first_requested_refinement_entity_id; + return out; +} + const std::string& cell_domain_to_str(cell::domain domain_id) { static const std::map type_to_name @@ -733,12 +839,15 @@ cutcells::mesh::CutMesh part_visualization_mesh( const cutcells::HOMeshPart& part, std::string_view mode, int geometry_order, - std::string_view node_family) + std::string_view node_family, + std::string_view projection_direction) { const bool cut_only = part_mode_is_cut_only(mode); const auto family = cutcells::curving::node_family_from_string(node_family); + const auto direction = + cutcells::curving::direction_mode_from_string(projection_direction); return cutcells::output::visualization_mesh( - part, /*include_uncut_cells=*/!cut_only, geometry_order, family); + part, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); } template @@ -747,12 +856,15 @@ cutcells::quadrature::QuadratureRules part_quadrature( int order, std::string_view mode, int geometry_order, - std::string_view node_family) + std::string_view node_family, + std::string_view projection_direction) { const bool cut_only = part_mode_is_cut_only(mode); const auto family = cutcells::curving::node_family_from_string(node_family); + const auto direction = + cutcells::curving::direction_mode_from_string(projection_direction); return cutcells::output::quadrature_rules( - part, order, /*include_uncut_cells=*/!cut_only, geometry_order, family); + part, order, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); } template @@ -760,14 +872,17 @@ void part_write_vtu(const cutcells::HOMeshPart& part, const std::string& filename, std::string_view mode, int geometry_order, - std::string_view node_family) + std::string_view node_family, + std::string_view projection_direction) { const bool cut_only = part_mode_is_cut_only(mode); auto family = cutcells::curving::node_family_from_string(node_family); + const auto direction = + cutcells::curving::direction_mode_from_string(projection_direction); if (geometry_order > 1) { const auto grid = cutcells::output::curved_lagrange_grid( - part, /*include_uncut_cells=*/!cut_only, geometry_order, family); + part, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); cutcells::io::write_lagrange_vtk( filename, std::span(grid.points.data(), grid.points.size()), @@ -782,7 +897,8 @@ void part_write_vtu(const cutcells::HOMeshPart& part, return; } - auto vis = part_visualization_mesh(part, mode, geometry_order, node_family); + auto vis = part_visualization_mesh( + part, mode, geometry_order, node_family, projection_direction); cutcells::io::write_vtk(filename, vis); } @@ -1883,6 +1999,87 @@ void declare_ho_cut(nb::module_& m, const std::string& type) }, nb::rv_policy::reference_internal, "Per-level-set domain classification, shape (num_level_sets, num_cells).") + .def( + "graph_check_summary", + [](const HOCutResult& self) + { + const auto& diagnostics = self.cut_cells.graph_diagnostics; + std::vector accepted; + std::vector checked_cells; + std::vector checked_edges; + std::vector checked_faces; + std::vector failed_checks; + std::vector graph_refinements; + std::vector min_true_transversality; + std::vector min_host_normal_alignment; + std::vector max_drift_amplification; + std::vector max_relative_correction_distance; + std::vector max_relative_tangential_shift; + std::vector min_edge_gap_ratio; + std::vector min_face_area_ratio; + std::vector min_level_set_gradient_host_alignment; + + accepted.reserve(diagnostics.size()); + checked_cells.reserve(diagnostics.size()); + checked_edges.reserve(diagnostics.size()); + checked_faces.reserve(diagnostics.size()); + failed_checks.reserve(diagnostics.size()); + graph_refinements.reserve(diagnostics.size()); + min_true_transversality.reserve(diagnostics.size()); + min_host_normal_alignment.reserve(diagnostics.size()); + max_drift_amplification.reserve(diagnostics.size()); + max_relative_correction_distance.reserve(diagnostics.size()); + max_relative_tangential_shift.reserve(diagnostics.size()); + min_edge_gap_ratio.reserve(diagnostics.size()); + min_face_area_ratio.reserve(diagnostics.size()); + min_level_set_gradient_host_alignment.reserve(diagnostics.size()); + + for (const auto& d : diagnostics) + { + accepted.push_back(d.accepted ? 1 : 0); + checked_cells.push_back(d.checked_cells); + checked_edges.push_back(d.checked_edges); + checked_faces.push_back(d.checked_faces); + failed_checks.push_back(d.failed_checks); + graph_refinements.push_back(d.graph_refinements); + min_true_transversality.push_back(d.min_true_transversality); + min_host_normal_alignment.push_back(d.min_host_normal_alignment); + max_drift_amplification.push_back(d.max_drift_amplification); + max_relative_correction_distance.push_back( + d.max_relative_correction_distance); + max_relative_tangential_shift.push_back( + d.max_relative_tangential_shift); + min_edge_gap_ratio.push_back(d.min_edge_gap_ratio); + min_face_area_ratio.push_back(d.min_face_area_ratio); + min_level_set_gradient_host_alignment.push_back( + d.min_level_set_gradient_host_alignment); + } + + nb::dict out; + out["accepted"] = as_nbarray(std::move(accepted)); + out["checked_cells"] = as_nbarray(std::move(checked_cells)); + out["checked_edges"] = as_nbarray(std::move(checked_edges)); + out["checked_faces"] = as_nbarray(std::move(checked_faces)); + out["failed_checks"] = as_nbarray(std::move(failed_checks)); + out["graph_refinements"] = as_nbarray(std::move(graph_refinements)); + out["min_true_transversality"] = + as_nbarray(std::move(min_true_transversality)); + out["min_host_normal_alignment"] = + as_nbarray(std::move(min_host_normal_alignment)); + out["max_drift_amplification"] = + as_nbarray(std::move(max_drift_amplification)); + out["max_relative_correction_distance"] = + as_nbarray(std::move(max_relative_correction_distance)); + out["max_relative_tangential_shift"] = + as_nbarray(std::move(max_relative_tangential_shift)); + out["min_edge_gap_ratio"] = as_nbarray(std::move(min_edge_gap_ratio)); + out["min_face_area_ratio"] = as_nbarray(std::move(min_face_area_ratio)); + out["min_surface_jacobian_ratio"] = out["min_face_area_ratio"]; + out["min_level_set_gradient_host_alignment"] = + as_nbarray(std::move(min_level_set_gradient_host_alignment)); + return out; + }, + "Return per-cut-cell graph preflight diagnostics.") .def("__getitem__", [](const HOCutResult& self, const std::string& expr_str) { return cutcells::select_part( @@ -1912,28 +2109,89 @@ void declare_ho_cut(nb::module_& m, const std::string& type) [](HOCutResult& self, int geometry_order, const std::string& node_family, - T small_entity_tol) + T small_entity_tol, + const std::string& projection_direction) { cutcells::curving::CurvingOptions options; options.geometry_order = geometry_order; options.node_family = cutcells::curving::node_family_from_string(node_family); + options.direction_mode = + cutcells::curving::direction_mode_from_string(projection_direction); options.small_entity_tol = small_entity_tol; try { - cutcells::curving::ensure_all_curved( - self.cut_cells.curving, - std::span(self.cut_cells.parent_cell_ids), - std::span>(self.cut_cells.adapt_cells), - std::span>(self.cut_cells.level_set_cells), - std::span(self.cut_cells.ls_offsets), - options); + auto parent_cell_ids = + std::span(self.cut_cells.parent_cell_ids); + auto adapt_cells = + std::span>(self.cut_cells.adapt_cells); + auto level_set_cells = + std::span>( + self.cut_cells.level_set_cells); + auto ls_offsets = std::span(self.cut_cells.ls_offsets); + + if (!self.cut_cells.curving.identity_valid + || self.cut_cells.curving.num_cut_cells + != static_cast(adapt_cells.size())) + { + cutcells::curving::rebuild_identity( + self.cut_cells.curving, parent_cell_ids, adapt_cells); + } + + for (std::size_t i = 0; + i < self.cut_cells.curving.identities.size(); ++i) + { + const auto& ident = self.cut_cells.curving.identities[i]; + bool graph_ok = true; + if (ident.cut_cell_id >= 0 + && ident.cut_cell_id + < static_cast( + self.cut_cells.graph_diagnostics.size())) + { + const auto& graph_diag = + self.cut_cells.graph_diagnostics[ + static_cast(ident.cut_cell_id)]; + for (const auto& record : graph_diag.zero_entities) + { + if (record.local_zero_entity_id + == ident.local_zero_entity_id) + { + graph_ok = record.accepted; + break; + } + } + } + + if (!graph_ok) + { + auto& state = self.cut_cells.curving.states[i]; + state = cutcells::curving::CurvedZeroEntityState{}; + state.status = cutcells::curving::CurvingStatus::failed; + state.geometry_order = options.geometry_order; + state.node_family = options.node_family; + state.direction_mode = options.direction_mode; + state.small_entity_tol = options.small_entity_tol; + state.zero_mask = ident.zero_mask; + state.failure_reason = "graph check failed before curving"; + continue; + } + + (void)cutcells::curving::ensure_curved( + self.cut_cells.curving, + parent_cell_ids, + adapt_cells, + level_set_cells, + ls_offsets, + ident.cut_cell_id, + ident.local_zero_entity_id, + options); + } } catch (const std::exception& e) { throw std::runtime_error( - std::string("curved_zero_nodes: ensure_all_curved failed: ") + std::string("curved_zero_nodes: ensure_curved failed: ") + e.what()); } @@ -1945,6 +2203,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) std::vector dim; std::vector parent_dim; std::vector parent_id; + std::vector direction_mode; std::vector zero_mask; std::vector stats_offsets; std::vector node_iterations; @@ -1956,6 +2215,11 @@ void declare_ho_cut(nb::module_& m, const std::string& type) std::vector node_safe_subspace_dim; std::vector node_projection_mode; std::vector node_retry_count; + std::vector node_seed; + std::vector node_direction; + std::vector node_clip_lo; + std::vector node_clip_hi; + std::vector node_root_t; nb::list failure_reason; std::vector total_iterations; std::vector max_iterations; @@ -1979,6 +2243,8 @@ void declare_ho_cut(nb::module_& m, const std::string& type) dim.push_back(static_cast(ident.dim)); parent_dim.push_back(ident.parent_dim); parent_id.push_back(ident.parent_id); + direction_mode.push_back( + static_cast(state.direction_mode)); zero_mask.push_back(ident.zero_mask); node_iterations.insert( node_iterations.end(), @@ -2016,6 +2282,26 @@ void declare_ho_cut(nb::module_& m, const std::string& type) node_retry_count.end(), state.node_retry_count.begin(), state.node_retry_count.end()); + node_seed.insert( + node_seed.end(), + state.node_seed.begin(), + state.node_seed.end()); + node_direction.insert( + node_direction.end(), + state.node_direction.begin(), + state.node_direction.end()); + node_clip_lo.insert( + node_clip_lo.end(), + state.node_clip_lo.begin(), + state.node_clip_lo.end()); + node_clip_hi.insert( + node_clip_hi.end(), + state.node_clip_hi.begin(), + state.node_clip_hi.end()); + node_root_t.insert( + node_root_t.end(), + state.node_root_t.begin(), + state.node_root_t.end()); stats_offsets.push_back(static_cast(node_iterations.size())); failure_reason.append(state.failure_reason); int total = 0; @@ -2053,6 +2339,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) result["dim"] = as_nbarray(std::move(dim)); result["parent_dim"] = as_nbarray(std::move(parent_dim)); result["parent_id"] = as_nbarray(std::move(parent_id)); + result["direction_mode"] = as_nbarray(std::move(direction_mode)); result["zero_mask"] = as_nbarray(std::move(zero_mask)); result["stats_offsets"] = as_nbarray(std::move(stats_offsets)); result["node_iterations"] = as_nbarray(std::move(node_iterations)); @@ -2064,6 +2351,16 @@ void declare_ho_cut(nb::module_& m, const std::string& type) result["node_safe_subspace_dim"] = as_nbarray(std::move(node_safe_subspace_dim)); result["node_projection_mode"] = as_nbarray(std::move(node_projection_mode)); result["node_retry_count"] = as_nbarray(std::move(node_retry_count)); + const std::size_t nstats = node_root_t.size(); + result["node_seed"] = as_nbarray( + std::move(node_seed), + {nstats, static_cast(self.cut_cells.tdim)}); + result["node_direction"] = as_nbarray( + std::move(node_direction), + {nstats, static_cast(self.cut_cells.tdim)}); + result["node_clip_lo"] = as_nbarray(std::move(node_clip_lo)); + result["node_clip_hi"] = as_nbarray(std::move(node_clip_hi)); + result["node_root_t"] = as_nbarray(std::move(node_root_t)); result["failure_reason"] = std::move(failure_reason); nb::list failure_code_names; @@ -2100,6 +2397,12 @@ void declare_ho_cut(nb::module_& m, const std::string& type) "vector_newton"}) projection_mode_names.append(name); result["projection_mode_names"] = std::move(projection_mode_names); + nb::list direction_mode_names; + for (const char* name : { + "straight_zero_entity_normal", + "level_set_gradient"}) + direction_mode_names.append(name); + result["direction_mode_names"] = std::move(direction_mode_names); result["total_iterations"] = as_nbarray(std::move(total_iterations)); result["max_iterations"] = as_nbarray(std::move(max_iterations)); return result; @@ -2107,6 +2410,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::arg("geometry_order") = 2, nb::arg("node_family") = "gll", nb::arg("small_entity_tol") = default_sqrt_epsilon_tol(), + nb::arg("projection_direction") = "level_set_gradient", "Return cached curved zero-entity nodes in parent reference coordinates."); // --- HOMeshPart --- @@ -2145,13 +2449,16 @@ void declare_ho_cut(nb::module_& m, const std::string& type) [](const PartT& self, const std::string& mode, int geometry_order, - const std::string& node_family) { + const std::string& node_family, + const std::string& projection_direction) { nb::gil_scoped_release release; - return part_visualization_mesh(self, mode, geometry_order, node_family); + return part_visualization_mesh( + self, mode, geometry_order, node_family, projection_direction); }, nb::arg("mode") = "full", nb::arg("geometry_order") = -1, nb::arg("node_family") = "gll", + nb::arg("projection_direction") = "level_set_gradient", "Return a visualization mesh for an HOMeshPart, preserving AdaptCell topology.") .def( "quadrature", @@ -2159,73 +2466,592 @@ void declare_ho_cut(nb::module_& m, const std::string& type) int order, const std::string& mode, int geometry_order, - const std::string& node_family) { + const std::string& node_family, + const std::string& projection_direction) { nb::gil_scoped_release release; - return part_quadrature(self, order, mode, geometry_order, node_family); + return part_quadrature( + self, order, mode, geometry_order, node_family, projection_direction); }, nb::arg("order") = 3, nb::arg("mode") = "full", nb::arg("geometry_order") = -1, nb::arg("node_family") = "gll", + nb::arg("projection_direction") = "level_set_gradient", "Return quadrature rules for an HOMeshPart, preserving AdaptCell topology. For curved geometry this is the construction node family.") + .def( + "graph_check_zero_entity_data", + [](const PartT& self) + { + if (self.cut_cells == nullptr) + throw std::runtime_error( + "HOMeshPart is not attached to cut-cell storage"); + + const auto infos = + cutcells::output::selected_zero_entity_infos(self); + const T nan = std::numeric_limits::quiet_NaN(); + + std::vector cut_cell_id; + std::vector parent_cell_id; + std::vector local_zero_entity_id; + std::vector level_set_id; + std::vector zero_entity_dim; + std::vector graph_accepted; + std::vector graph_failed_checks; + std::vector graph_checked_edges; + std::vector graph_checked_faces; + std::vector graph_failure_reason_code; + nb::list graph_failure_reason; + std::vector min_true_transversality; + std::vector min_host_normal_alignment; + std::vector max_drift_amplification; + std::vector max_relative_correction_distance; + std::vector max_relative_tangential_shift; + std::vector min_edge_gap_ratio; + std::vector min_face_area_ratio; + std::vector min_level_set_gradient_host_alignment; + std::vector failed_face_triangle_index; + std::vector failed_face_area_ratio; + std::vector graph_failed_projection_seed; + std::vector graph_failed_projection_direction; + std::vector graph_failed_projection_clip_lo; + std::vector graph_failed_projection_clip_hi; + std::vector graph_failed_projection_root_t; + std::vector requested_refinement_entity_dim; + std::vector requested_refinement_entity_id; + + cut_cell_id.reserve(infos.size()); + parent_cell_id.reserve(infos.size()); + local_zero_entity_id.reserve(infos.size()); + level_set_id.reserve(infos.size()); + zero_entity_dim.reserve(infos.size()); + graph_accepted.reserve(infos.size()); + graph_failed_checks.reserve(infos.size()); + graph_checked_edges.reserve(infos.size()); + graph_checked_faces.reserve(infos.size()); + graph_failure_reason_code.reserve(infos.size()); + min_true_transversality.reserve(infos.size()); + min_host_normal_alignment.reserve(infos.size()); + max_drift_amplification.reserve(infos.size()); + max_relative_correction_distance.reserve(infos.size()); + max_relative_tangential_shift.reserve(infos.size()); + min_edge_gap_ratio.reserve(infos.size()); + min_face_area_ratio.reserve(infos.size()); + min_level_set_gradient_host_alignment.reserve(infos.size()); + failed_face_triangle_index.reserve(infos.size()); + failed_face_area_ratio.reserve(infos.size()); + graph_failed_projection_seed.reserve( + infos.size() * static_cast(self.cut_cells->tdim)); + graph_failed_projection_direction.reserve( + infos.size() * static_cast(self.cut_cells->tdim)); + graph_failed_projection_clip_lo.reserve(infos.size()); + graph_failed_projection_clip_hi.reserve(infos.size()); + graph_failed_projection_root_t.reserve(infos.size()); + requested_refinement_entity_dim.reserve(infos.size()); + requested_refinement_entity_id.reserve(infos.size()); + + auto append_projection_vector = + [&](std::vector& out, const std::vector& values) + { + for (int d = 0; d < self.cut_cells->tdim; ++d) + { + out.push_back( + d < static_cast(values.size()) + ? values[static_cast(d)] + : nan); + } + }; + + for (const auto& info : infos) + { + cut_cell_id.push_back(info.cut_cell_id); + parent_cell_id.push_back(info.parent_cell_id); + local_zero_entity_id.push_back(info.local_zero_entity_id); + zero_entity_dim.push_back(info.dimension); + + const cutcells::ZeroEntityGraphDiagnostics* record = nullptr; + if (info.cut_cell_id >= 0 + && info.cut_cell_id + < static_cast( + self.cut_cells->graph_diagnostics.size())) + { + const auto& diag = + self.cut_cells->graph_diagnostics[ + static_cast(info.cut_cell_id)]; + for (const auto& candidate : diag.zero_entities) + { + const std::uint64_t candidate_bit = + candidate.level_set_id >= 0 + ? (std::uint64_t(1) << candidate.level_set_id) + : std::uint64_t(0); + if (candidate.local_zero_entity_id + == info.local_zero_entity_id + && (self.expr.zero_required == 0 + || (self.expr.zero_required & candidate_bit) != 0)) + { + record = &candidate; + break; + } + } + } + + if (record == nullptr) + { + level_set_id.push_back(-1); + graph_accepted.push_back(-1); + graph_failed_checks.push_back(-1); + graph_checked_edges.push_back(-1); + graph_checked_faces.push_back(-1); + graph_failure_reason_code.push_back(-1); + graph_failure_reason.append("missing"); + min_true_transversality.push_back(nan); + min_host_normal_alignment.push_back(nan); + max_drift_amplification.push_back(nan); + max_relative_correction_distance.push_back(nan); + max_relative_tangential_shift.push_back(nan); + min_edge_gap_ratio.push_back(nan); + min_face_area_ratio.push_back(nan); + min_level_set_gradient_host_alignment.push_back(nan); + failed_face_triangle_index.push_back(-1); + failed_face_area_ratio.push_back(nan); + append_projection_vector( + graph_failed_projection_seed, std::vector{}); + append_projection_vector( + graph_failed_projection_direction, std::vector{}); + graph_failed_projection_clip_lo.push_back(nan); + graph_failed_projection_clip_hi.push_back(nan); + graph_failed_projection_root_t.push_back(nan); + requested_refinement_entity_dim.push_back(-1); + requested_refinement_entity_id.push_back(-1); + continue; + } + + level_set_id.push_back(record->level_set_id); + graph_accepted.push_back(record->accepted ? 1 : 0); + graph_failed_checks.push_back(record->failed_checks); + graph_checked_edges.push_back(record->checked_edges); + graph_checked_faces.push_back(record->checked_faces); + graph_failure_reason_code.push_back( + static_cast(record->failure_reason)); + graph_failure_reason.append(std::string( + cutcells::graph_criteria::failure_reason_name( + record->failure_reason))); + min_true_transversality.push_back( + record->min_true_transversality); + min_host_normal_alignment.push_back( + record->min_host_normal_alignment); + max_drift_amplification.push_back( + record->max_drift_amplification); + max_relative_correction_distance.push_back( + record->max_relative_correction_distance); + max_relative_tangential_shift.push_back( + record->max_relative_tangential_shift); + min_edge_gap_ratio.push_back(record->min_edge_gap_ratio); + min_face_area_ratio.push_back(record->min_face_area_ratio); + min_level_set_gradient_host_alignment.push_back( + record->min_level_set_gradient_host_alignment); + failed_face_triangle_index.push_back( + record->failed_face_triangle_index); + failed_face_area_ratio.push_back( + record->failed_face_area_ratio); + append_projection_vector( + graph_failed_projection_seed, + record->failed_projection_seed); + append_projection_vector( + graph_failed_projection_direction, + record->failed_projection_direction); + graph_failed_projection_clip_lo.push_back( + record->failed_projection_clip_lo); + graph_failed_projection_clip_hi.push_back( + record->failed_projection_clip_hi); + graph_failed_projection_root_t.push_back( + record->failed_projection_root_t); + requested_refinement_entity_dim.push_back( + record->requested_refinement_entity_dim); + requested_refinement_entity_id.push_back( + record->requested_refinement_entity_id); + } + + nb::dict out; + out["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); + out["parent_cell_id"] = as_nbarray(std::move(parent_cell_id)); + out["local_zero_entity_id"] = + as_nbarray(std::move(local_zero_entity_id)); + out["level_set_id"] = as_nbarray(std::move(level_set_id)); + out["zero_entity_dim"] = as_nbarray(std::move(zero_entity_dim)); + out["graph_accepted"] = as_nbarray(std::move(graph_accepted)); + out["graph_failed_checks"] = + as_nbarray(std::move(graph_failed_checks)); + out["graph_checked_edges"] = + as_nbarray(std::move(graph_checked_edges)); + out["graph_checked_faces"] = + as_nbarray(std::move(graph_checked_faces)); + out["graph_failure_reason_code"] = + as_nbarray(std::move(graph_failure_reason_code)); + out["graph_failure_reason"] = graph_failure_reason; + out["graph_min_transversality"] = + as_nbarray(std::move(min_true_transversality)); + out["graph_min_host_alignment"] = + as_nbarray(std::move(min_host_normal_alignment)); + out["graph_max_drift"] = + as_nbarray(std::move(max_drift_amplification)); + out["graph_max_correction"] = + as_nbarray(std::move(max_relative_correction_distance)); + out["graph_max_tangential_shift"] = + as_nbarray(std::move(max_relative_tangential_shift)); + out["graph_min_edge_gap"] = + as_nbarray(std::move(min_edge_gap_ratio)); + out["graph_min_face_area"] = + as_nbarray(std::move(min_face_area_ratio)); + out["graph_min_surface_jacobian_ratio"] = + out["graph_min_face_area"]; + out["graph_min_level_set_gradient_host_alignment"] = + as_nbarray(std::move(min_level_set_gradient_host_alignment)); + out["graph_failed_face_triangle_index"] = + as_nbarray(std::move(failed_face_triangle_index)); + out["graph_failed_face_area_ratio"] = + as_nbarray(std::move(failed_face_area_ratio)); + out["graph_failed_surface_jacobian_ratio"] = + out["graph_failed_face_area_ratio"]; + out["graph_failed_projection_seed"] = as_nbarray( + std::move(graph_failed_projection_seed), + {infos.size(), static_cast(self.cut_cells->tdim)}); + out["graph_failed_projection_direction"] = as_nbarray( + std::move(graph_failed_projection_direction), + {infos.size(), static_cast(self.cut_cells->tdim)}); + out["graph_failed_projection_clip_lo"] = + as_nbarray(std::move(graph_failed_projection_clip_lo)); + out["graph_failed_projection_clip_hi"] = + as_nbarray(std::move(graph_failed_projection_clip_hi)); + out["graph_failed_projection_root_t"] = + as_nbarray(std::move(graph_failed_projection_root_t)); + out["graph_requested_refinement_entity_dim"] = + as_nbarray(std::move(requested_refinement_entity_dim)); + out["graph_requested_refinement_entity_id"] = + as_nbarray(std::move(requested_refinement_entity_id)); + return out; + }, + "Return graph-check values aligned with the cells of the straight phi = 0 visualization mesh.") + .def( + "graph_check_node_data", + [](const PartT& self) + { + if (self.cut_cells == nullptr) + throw std::runtime_error( + "HOMeshPart is not attached to cut-cell storage"); + + const auto infos = + cutcells::output::selected_zero_entity_infos(self); + const int tdim = self.cut_cells->tdim; + const T nan = std::numeric_limits::quiet_NaN(); + + std::vector cut_cell_id; + std::vector parent_cell_id; + std::vector local_zero_entity_id; + std::vector zero_entity_dim; + std::vector node_index; + std::vector node_kind; + std::vector node_accepted; + std::vector selected_direction_kind; + std::vector fallback_used; + std::vector failure_reason_code; + nb::list failure_reason; + std::vector parent_entity_dim; + std::vector parent_entity_id; + std::vector requested_refinement_entity_dim; + std::vector requested_refinement_entity_id; + std::vector gradient_host_alignment; + std::vector gradient_angle_to_tangent_deg; + std::vector selected_host_alignment; + std::vector drift_amplification; + std::vector relative_correction_distance; + std::vector relative_tangential_shift; + std::vector true_transversality; + std::vector seed; + std::vector corrected; + std::vector selected_direction; + std::vector level_set_gradient_direction; + std::vector straight_helper_normal; + + auto append_vector = [&](std::vector& out, const std::vector& values) + { + for (int d = 0; d < tdim; ++d) + { + out.push_back( + d < static_cast(values.size()) + ? values[static_cast(d)] + : nan); + } + }; + + for (const auto& info : infos) + { + const cutcells::ZeroEntityGraphDiagnostics* record = nullptr; + if (info.cut_cell_id >= 0 + && info.cut_cell_id + < static_cast( + self.cut_cells->graph_diagnostics.size())) + { + const auto& diag = + self.cut_cells->graph_diagnostics[ + static_cast(info.cut_cell_id)]; + for (const auto& candidate : diag.zero_entities) + { + const std::uint64_t candidate_bit = + candidate.level_set_id >= 0 + ? (std::uint64_t(1) << candidate.level_set_id) + : std::uint64_t(0); + if (candidate.local_zero_entity_id + == info.local_zero_entity_id + && (self.expr.zero_required == 0 + || (self.expr.zero_required & candidate_bit) != 0)) + { + record = &candidate; + break; + } + } + } + if (record == nullptr) + continue; + + for (const auto& node : record->nodes) + { + cut_cell_id.push_back(info.cut_cell_id); + parent_cell_id.push_back(info.parent_cell_id); + local_zero_entity_id.push_back(info.local_zero_entity_id); + zero_entity_dim.push_back(info.dimension); + node_index.push_back(node.node_index); + node_kind.push_back(static_cast(node.node_kind)); + node_accepted.push_back(node.accepted ? 1 : 0); + selected_direction_kind.push_back( + static_cast(node.selected_direction_kind)); + fallback_used.push_back(node.fallback_used ? 1 : 0); + failure_reason_code.push_back( + static_cast(node.failure_reason)); + failure_reason.append(std::string( + cutcells::graph_criteria::failure_reason_name( + node.failure_reason))); + parent_entity_dim.push_back(node.parent_entity_dim); + parent_entity_id.push_back(node.parent_entity_id); + requested_refinement_entity_dim.push_back( + node.requested_refinement_entity_dim); + requested_refinement_entity_id.push_back( + node.requested_refinement_entity_id); + gradient_host_alignment.push_back( + node.level_set_gradient_host_alignment); + gradient_angle_to_tangent_deg.push_back( + node.level_set_gradient_angle_to_tangent_deg); + selected_host_alignment.push_back( + node.selected_host_alignment); + drift_amplification.push_back(node.drift_amplification); + relative_correction_distance.push_back( + node.relative_correction_distance); + relative_tangential_shift.push_back( + node.relative_tangential_shift); + true_transversality.push_back(node.true_transversality); + append_vector(seed, node.seed); + append_vector(corrected, node.corrected); + append_vector(selected_direction, node.selected_direction); + append_vector( + level_set_gradient_direction, + node.level_set_gradient_direction); + append_vector(straight_helper_normal, node.straight_helper_normal); + } + } + + const std::size_t n = node_index.size(); + nb::dict out; + out["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); + out["parent_cell_id"] = as_nbarray(std::move(parent_cell_id)); + out["local_zero_entity_id"] = + as_nbarray(std::move(local_zero_entity_id)); + out["zero_entity_dim"] = as_nbarray(std::move(zero_entity_dim)); + out["node_index"] = as_nbarray(std::move(node_index)); + out["node_kind"] = as_nbarray(std::move(node_kind)); + out["node_accepted"] = as_nbarray(std::move(node_accepted)); + out["selected_direction_kind"] = + as_nbarray(std::move(selected_direction_kind)); + out["fallback_used"] = as_nbarray(std::move(fallback_used)); + out["failure_reason_code"] = + as_nbarray(std::move(failure_reason_code)); + out["failure_reason"] = failure_reason; + out["parent_entity_dim"] = + as_nbarray(std::move(parent_entity_dim)); + out["parent_entity_id"] = as_nbarray(std::move(parent_entity_id)); + out["requested_refinement_entity_dim"] = + as_nbarray(std::move(requested_refinement_entity_dim)); + out["requested_refinement_entity_id"] = + as_nbarray(std::move(requested_refinement_entity_id)); + out["level_set_gradient_host_alignment"] = + as_nbarray(std::move(gradient_host_alignment)); + out["level_set_gradient_angle_to_tangent_deg"] = + as_nbarray(std::move(gradient_angle_to_tangent_deg)); + out["selected_host_alignment"] = + as_nbarray(std::move(selected_host_alignment)); + out["drift_amplification"] = + as_nbarray(std::move(drift_amplification)); + out["relative_correction_distance"] = + as_nbarray(std::move(relative_correction_distance)); + out["relative_tangential_shift"] = + as_nbarray(std::move(relative_tangential_shift)); + out["true_transversality"] = + as_nbarray(std::move(true_transversality)); + out["seed"] = as_nbarray( + std::move(seed), {n, static_cast(tdim)}); + out["corrected"] = as_nbarray( + std::move(corrected), {n, static_cast(tdim)}); + out["selected_direction"] = as_nbarray( + std::move(selected_direction), + {n, static_cast(tdim)}); + out["level_set_gradient_direction"] = as_nbarray( + std::move(level_set_gradient_direction), + {n, static_cast(tdim)}); + out["straight_helper_normal"] = as_nbarray( + std::move(straight_helper_normal), + {n, static_cast(tdim)}); + return out; + }, + "Return flat per-node graph-check diagnostics for the selected zero interface.") .def( "write_vtu", [](const PartT& self, const std::string& filename, const std::string& mode, int geometry_order, - const std::string& node_family) { + const std::string& node_family, + const std::string& projection_direction) { nb::gil_scoped_release release; - part_write_vtu(self, filename, mode, geometry_order, node_family); + part_write_vtu( + self, filename, mode, geometry_order, node_family, projection_direction); }, nb::arg("filename"), nb::arg("mode") = "full", nb::arg("geometry_order") = -1, nb::arg("node_family") = "gll", + nb::arg("projection_direction") = "level_set_gradient", "Write a VTU file for an HOMeshPart. geometry_order > 1 samples the requested construction node family into curved VTK Lagrange cells."); // --- ho_cut() factory --- m.def("ho_cut", - [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { + [](const MeshViewT& mesh, const LevelSetT& ls, + bool triangulate, int graph_max_refinements, + const std::string& graph_projection_direction, + const std::string& graph_refinement_mode, + T min_level_set_gradient_host_alignment, + bool graph_enabled) { nb::gil_scoped_release release; + cutcells::ReadyCellGraphOptions graph_options; + graph_options.enabled = graph_enabled; + graph_options.max_refinements = graph_max_refinements; + graph_options.projection_mode = + graph_projection_mode_from_string(graph_projection_direction); + graph_options.refinement_mode = + graph_refinement_mode_from_string(graph_refinement_mode); + graph_options.min_level_set_gradient_host_alignment = + min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); + auto [hc, bg] = cutcells::cut( + mesh, *owned_ls, triangulate, graph_options); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, + nb::arg("graph_max_refinements") = 5, + nb::arg("graph_projection_direction") = "level_set_gradient", + nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("min_level_set_gradient_host_alignment") = T(0.9), + nb::arg("graph_enabled") = true, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("ho_cut", - [](const MeshViewT& mesh, const std::vector& level_sets, bool triangulate) { + [](const MeshViewT& mesh, const std::vector& level_sets, + bool triangulate, int graph_max_refinements, + const std::string& graph_projection_direction, + const std::string& graph_refinement_mode, + T min_level_set_gradient_host_alignment, + bool graph_enabled) { nb::gil_scoped_release release; + cutcells::ReadyCellGraphOptions graph_options; + graph_options.enabled = graph_enabled; + graph_options.max_refinements = graph_max_refinements; + graph_options.projection_mode = + graph_projection_mode_from_string(graph_projection_direction); + graph_options.refinement_mode = + graph_refinement_mode_from_string(graph_refinement_mode); + graph_options.min_level_set_gradient_host_alignment = + min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); + auto [hc, bg] = cutcells::cut( + mesh, *owned_ls, triangulate, graph_options); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, + nb::arg("graph_max_refinements") = 5, + nb::arg("graph_projection_direction") = "level_set_gradient", + nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("min_level_set_gradient_host_alignment") = T(0.9), + nb::arg("graph_enabled") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); m.def("cut", - [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { + [](const MeshViewT& mesh, const LevelSetT& ls, + bool triangulate, int graph_max_refinements, + const std::string& graph_projection_direction, + const std::string& graph_refinement_mode, + T min_level_set_gradient_host_alignment, + bool graph_enabled) { nb::gil_scoped_release release; + cutcells::ReadyCellGraphOptions graph_options; + graph_options.enabled = graph_enabled; + graph_options.max_refinements = graph_max_refinements; + graph_options.projection_mode = + graph_projection_mode_from_string(graph_projection_direction); + graph_options.refinement_mode = + graph_refinement_mode_from_string(graph_refinement_mode); + graph_options.min_level_set_gradient_host_alignment = + min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); + auto [hc, bg] = cutcells::cut( + mesh, *owned_ls, triangulate, graph_options); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, + nb::arg("graph_max_refinements") = 5, + nb::arg("graph_projection_direction") = "level_set_gradient", + nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("min_level_set_gradient_host_alignment") = T(0.9), + nb::arg("graph_enabled") = true, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("cut", - [](const MeshViewT& mesh, const std::vector& level_sets, bool triangulate) { + [](const MeshViewT& mesh, const std::vector& level_sets, + bool triangulate, int graph_max_refinements, + const std::string& graph_projection_direction, + const std::string& graph_refinement_mode, + T min_level_set_gradient_host_alignment, + bool graph_enabled) { nb::gil_scoped_release release; + cutcells::ReadyCellGraphOptions graph_options; + graph_options.enabled = graph_enabled; + graph_options.max_refinements = graph_max_refinements; + graph_options.projection_mode = + graph_projection_mode_from_string(graph_projection_direction); + graph_options.refinement_mode = + graph_refinement_mode_from_string(graph_refinement_mode); + graph_options.min_level_set_gradient_host_alignment = + min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); + auto [hc, bg] = cutcells::cut( + mesh, *owned_ls, triangulate, graph_options); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, + nb::arg("graph_max_refinements") = 5, + nb::arg("graph_projection_direction") = "level_set_gradient", + nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("min_level_set_gradient_host_alignment") = T(0.9), + nb::arg("graph_enabled") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); @@ -2729,6 +3555,62 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("edge_max_depth") = 20, nb::arg("triangulate_cut_parts") = false); + m.def( + "check_ready_to_cut_cell_graphs", + [](const AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + const std::string& projection_direction, + const std::string& refinement_mode, + int graph_max_refinements, + T max_relative_correction_distance, + T max_relative_tangential_shift, + T max_drift_amplification, + T min_host_normal_alignment, + T min_level_set_gradient_host_alignment) + { + auto options = make_graph_options( + projection_direction, + refinement_mode, + graph_max_refinements, + max_relative_correction_distance, + max_relative_tangential_shift, + max_drift_amplification, + min_host_normal_alignment, + min_level_set_gradient_host_alignment, + true); + cutcells::ReadyCellGraphDiagnostics diagnostics; + { + nb::gil_scoped_release release; + diagnostics = cutcells::check_ready_to_cut_cell_graphs( + adapt_cell, ls_cell, level_set_id, options); + } + return graph_diagnostics_to_dict(diagnostics); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("projection_direction") = "level_set_gradient", + nb::arg("refinement_mode") = "green_edge", + nb::arg("graph_max_refinements") = 5, + nb::arg("max_relative_correction_distance") = T(0.5), + nb::arg("max_relative_tangential_shift") = T(0.25), + nb::arg("max_drift_amplification") = T(4), + nb::arg("min_host_normal_alignment") = T(0.25), + nb::arg("min_level_set_gradient_host_alignment") = T(0.9)); + + m.def( + "refine_ready_cell_on_largest_midpoint_value", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, + int level_set_id, int cell_id) + { + nb::gil_scoped_release release; + return cutcells::refine_ready_cell_on_largest_midpoint_value( + adapt_cell, ls_cell, level_set_id, cell_id); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("cell_id")); + m.def( "refine_green_on_multiple_root_edges", [](AdaptCellT& adapt_cell, int level_set_id) @@ -2770,6 +3652,59 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("edge_max_depth") = 20, nb::arg("triangulate_cut_parts") = false); + m.def( + "certify_refine_graph_check_and_process_ready_cells", + [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, + int max_iterations, T zero_tol, T sign_tol, int edge_max_depth, + bool triangulate_cut_parts, const std::string& projection_direction, + const std::string& refinement_mode, + int graph_max_refinements, + T max_relative_correction_distance, + T max_relative_tangential_shift, + T max_drift_amplification, + T min_host_normal_alignment, + T min_level_set_gradient_host_alignment, + bool graph_enabled) + { + auto options = make_graph_options( + projection_direction, + refinement_mode, + graph_max_refinements, + max_relative_correction_distance, + max_relative_tangential_shift, + max_drift_amplification, + min_host_normal_alignment, + min_level_set_gradient_host_alignment, + graph_enabled); + cutcells::ReadyCellGraphDiagnostics diagnostics; + { + nb::gil_scoped_release release; + diagnostics = + cutcells::certify_refine_graph_check_and_process_ready_cells( + adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, edge_max_depth, + triangulate_cut_parts, options); + } + return graph_diagnostics_to_dict(diagnostics); + }, + nb::arg("adapt_cell"), + nb::arg("level_set_cell"), + nb::arg("level_set_id"), + nb::arg("max_iterations") = 8, + nb::arg("zero_tol") = T(1e-12), + nb::arg("sign_tol") = T(1e-12), + nb::arg("edge_max_depth") = 20, + nb::arg("triangulate_cut_parts") = false, + nb::arg("projection_direction") = "level_set_gradient", + nb::arg("refinement_mode") = "green_edge", + nb::arg("graph_max_refinements") = 5, + nb::arg("max_relative_correction_distance") = T(0.5), + nb::arg("max_relative_tangential_shift") = T(0.25), + nb::arg("max_drift_amplification") = T(4), + nb::arg("min_host_normal_alignment") = T(0.25), + nb::arg("min_level_set_gradient_host_alignment") = T(0.9), + nb::arg("graph_enabled") = true); + m.def( "certify_and_refine", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, diff --git a/python/pyproject.toml b/python/pyproject.toml index fcb12ac..1512fcc 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -25,5 +25,6 @@ ci = ["mypy", "pytest-xdist", "cutcells[test]"] [tool.scikit-build] minimum-version = "0.5" cmake.minimum-version = "3.21.0" +build-dir = "build/{wheel_tag}" wheel.packages = ["cutcells"] wheel.license-files = ["../LICENSE"] diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index 2bd285a..544c831 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -27,6 +27,30 @@ def _single_tetra_mesh(): return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) +def _tetra_line_interval(seed, direction): + lo = -np.inf + hi = np.inf + constraints = [ + (np.array([1.0, 0.0, 0.0]), 0.0), + (np.array([0.0, 1.0, 0.0]), 0.0), + (np.array([0.0, 0.0, 1.0]), 0.0), + (np.array([-1.0, -1.0, -1.0]), -1.0), + ] + for normal, lower in constraints: + value = float(np.dot(normal, seed) - lower) + slope = float(np.dot(normal, direction)) + if abs(slope) <= 1.0e-14: + if value < -1.0e-14: + return np.nan, np.nan + continue + bound = -value / slope + if slope > 0.0: + lo = max(lo, bound) + else: + hi = min(hi, bound) + return lo, hi + + class CertificationRefinementTests(unittest.TestCase): def test_restrict_edge_bernstein_exact_matches_parent_evaluation(self): mesh = _single_triangle_mesh() @@ -256,6 +280,44 @@ def test_ready_to_cut_triangle_uses_exact_one_root_vertices(self): self.assertGreaterEqual(int(source_edges[curved_id]), 0) self.assertGreaterEqual(int(source_edges[second_id]), 0) + def test_graph_check_failure_refines_original_uncut_ready_triangle(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] - 0.25, + degree=2, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + diagnostics = cutcells.certify_refine_graph_check_and_process_ready_cells( + adapt, + ls_cell, + 0, + projection_direction="straight_zero_entity_normal", + graph_max_refinements=2, + max_relative_correction_distance=0.01, + ) + + self.assertFalse(bool(diagnostics["accepted"])) + self.assertEqual(int(diagnostics["graph_refinements"]), 2) + self.assertEqual( + str(diagnostics["first_failure_reason"]), + "excessive_correction_distance", + ) + self.assertGreater(adapt.num_cells(), 2) + + tags = np.asarray(adapt.cell_cert_tags(0)) + self.assertEqual( + sorted(np.unique(tags).tolist()), + sorted([ + cutcells.CellCertTag.negative.value, + cutcells.CellCertTag.positive.value, + cutcells.CellCertTag.cut.value, + ]), + ) + def test_ho_cut_smoke_uses_certified_triangle_pipeline(self): mesh = _single_triangle_mesh() ls = cutcells.create_level_set( @@ -265,7 +327,7 @@ def test_ho_cut_smoke_uses_certified_triangle_pipeline(self): name="phi", ) - result = cutcells.ho_cut(mesh, ls) + result = cutcells.ho_cut(mesh, ls, graph_enabled=False) self.assertEqual(result.num_cut_cells, 1) np.testing.assert_array_equal(np.asarray(result.parent_cell_ids), np.array([0])) @@ -292,6 +354,193 @@ def test_cut_overload_supports_mesh_part_selection(self): self.assertGreater(adapt.num_cells(), 0) self.assertGreater(adapt.num_vertices(), 3) + def test_graph_check_data_is_attached_to_zero_interface_cells(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] - 0.25, + degree=2, + name="phi", + ) + + result = cutcells.cut( + mesh, + ls, + graph_max_refinements=0, + min_level_set_gradient_host_alignment=0.0, + ) + interface = result["phi = 0"] + zero_mesh = interface.visualization_mesh(mode="cut_only", geometry_order=1) + data = interface.graph_check_zero_entity_data() + summary = result.graph_check_summary() + + num_zero_cells = np.asarray(zero_mesh.offset).size - 1 + self.assertGreater(num_zero_cells, 0) + self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) == 0)) + self.assertEqual(np.asarray(data["local_zero_entity_id"]).shape, (num_zero_cells,)) + self.assertEqual(np.asarray(data["graph_accepted"]).shape, (num_zero_cells,)) + self.assertTrue(np.all(np.asarray(data["zero_entity_dim"]) == interface.dim)) + self.assertTrue(np.all(np.asarray(data["graph_accepted"]) >= 0)) + self.assertTrue(np.all(np.isfinite(np.asarray(data["graph_max_correction"])))) + for key in ( + "graph_failed_projection_seed", + "graph_failed_projection_direction", + "graph_failed_projection_clip_lo", + "graph_failed_projection_clip_hi", + "graph_failed_projection_root_t", + ): + self.assertIn(key, data) + self.assertEqual( + np.asarray(data["graph_failed_projection_seed"]).shape, + (num_zero_cells, 2), + ) + self.assertEqual( + np.asarray(data["graph_failed_projection_direction"]).shape, + (num_zero_cells, 2), + ) + self.assertEqual( + np.asarray(data["graph_failed_projection_clip_lo"]).shape, + (num_zero_cells,), + ) + + def test_sphere_graph_check_reports_surface_jacobian_and_node_data(self): + grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) + mesh = cutcells.mesh_from_pyvista(grid) + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + result = cutcells.cut(mesh, ls, graph_max_refinements=0) + interface = result["phi = 0"] + data = interface.graph_check_zero_entity_data() + node_data = interface.graph_check_node_data() + + self.assertIn("graph_min_level_set_gradient_host_alignment", data) + self.assertIn("graph_min_surface_jacobian_ratio", data) + self.assertIn("graph_failed_surface_jacobian_ratio", data) + + node_accepted = np.asarray(node_data["node_accepted"], dtype=np.int32) + self.assertGreater(node_accepted.size, 0) + gradient_alignment = np.asarray( + node_data["level_set_gradient_host_alignment"], dtype=np.float64) + finite = np.isfinite(gradient_alignment) + self.assertTrue(np.any(finite)) + self.assertTrue(np.all(gradient_alignment[finite] >= 0.0)) + + def test_cut_graph_check_accepts_level_set_gradient_mode(self): + mesh = _single_triangle_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] - 0.25, + degree=2, + name="phi", + ) + + result = cutcells.cut( + mesh, + ls, + graph_max_refinements=0, + graph_projection_direction="level_set_gradient", + ) + data = result["phi = 0"].graph_check_zero_entity_data() + + self.assertTrue(np.all(np.asarray(result.graph_check_summary()["graph_refinements"]) == 0)) + self.assertGreater(np.asarray(data["graph_accepted"]).size, 0) + + def test_cut_graph_check_accepts_red_failed_cell_refinement_mode(self): + grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) + mesh = cutcells.mesh_from_pyvista(grid) + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + result = cutcells.cut( + mesh, + ls, + graph_max_refinements=1, + graph_refinement_mode="red_failed_cell", + ) + summary = result.graph_check_summary() + + self.assertGreater(np.asarray(summary["accepted"]).size, 0) + self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) + + def test_scalar_projection_records_parent_clipped_tetra_bracket(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + result = cutcells.ho_cut( + mesh, + ls, + min_level_set_gradient_host_alignment=0.0, + ) + curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") + + seeds = np.asarray(curved["node_seed"], dtype=np.float64) + directions = np.asarray(curved["node_direction"], dtype=np.float64) + clip_lo = np.asarray(curved["node_clip_lo"], dtype=np.float64) + clip_hi = np.asarray(curved["node_clip_hi"], dtype=np.float64) + root_t = np.asarray(curved["node_root_t"], dtype=np.float64) + + projected = np.flatnonzero(np.isfinite(root_t)) + self.assertGreater(projected.size, 0) + for idx in projected: + seed = seeds[idx] + direction = directions[idx] + lo, hi = _tetra_line_interval(seed, direction) + np.testing.assert_allclose([clip_lo[idx], clip_hi[idx]], [lo, hi], atol=1e-12) + self.assertFalse(np.isclose(clip_lo[idx], -2.0)) + self.assertFalse(np.isclose(clip_hi[idx], 2.0)) + self.assertLessEqual(clip_lo[idx] - 1e-12, root_t[idx]) + self.assertLessEqual(root_t[idx], clip_hi[idx] + 1e-12) + + phi_seed = float(np.dot(seed, seed) - 0.36) + grad_seed = 2.0 * seed + self.assertGreaterEqual(float(np.dot(grad_seed, direction)), -1e-12) + if phi_seed > 0.0: + self.assertLessEqual(root_t[idx], 1e-12) + elif phi_seed < 0.0: + self.assertGreaterEqual(root_t[idx], -1e-12) + + def test_straight_normal_projection_is_gradient_oriented(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + result = cutcells.ho_cut( + mesh, + ls, + min_level_set_gradient_host_alignment=0.0, + ) + curved = result.curved_zero_nodes( + geometry_order=2, + node_family="gll", + projection_direction="straight_zero_entity_normal", + ) + + seeds = np.asarray(curved["node_seed"], dtype=np.float64) + directions = np.asarray(curved["node_direction"], dtype=np.float64) + root_t = np.asarray(curved["node_root_t"], dtype=np.float64) + projected = np.flatnonzero(np.isfinite(root_t)) + self.assertGreater(projected.size, 0) + for idx in projected: + self.assertGreaterEqual(float(np.dot(2.0 * seeds[idx], directions[idx])), -1e-12) + def test_curving_accepts_near_zero_multi_level_set_node_without_projection(self): mesh = _single_triangle_mesh() @@ -339,7 +588,7 @@ def phi(X): return X[0] + X[1] - eps ls = cutcells.create_level_set(mesh, phi, degree=1, name="phi") - result = cutcells.ho_cut(mesh, ls) + result = cutcells.ho_cut(mesh, ls, graph_enabled=False) curved = result.curved_zero_nodes( geometry_order=2, node_family="gll", From 36fc259e851a5c3bba008f89842d73530cc8d4ed Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Mon, 4 May 2026 19:31:19 +0200 Subject: [PATCH 18/23] adding graph check files --- cpp/src/adapt_cell.h | 39 ++ cpp/src/geometric_quantity.h | 947 +++++++++++++++++++++++++++++++++++ cpp/src/graph_criteria.h | 640 +++++++++++++++++++++++ cpp/src/tangent_shift.h | 873 ++++++++++++++++++++++++++++++++ 4 files changed, 2499 insertions(+) create mode 100644 cpp/src/geometric_quantity.h create mode 100644 cpp/src/graph_criteria.h create mode 100644 cpp/src/tangent_shift.h diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h index 2018771..6d5347f 100644 --- a/cpp/src/adapt_cell.h +++ b/cpp/src/adapt_cell.h @@ -54,6 +54,17 @@ enum class FaceCertTag : std::uint8_t ambiguous = 5 }; +/// Why a leaf cell was created from an earlier leaf cell. +enum class CellRefinementReason : std::uint8_t +{ + none = 0, + green_edge = 1, + red_cell = 2, + graph_green_edge = 3, + graph_red_cell = 4, + cut_level_set = 5 +}; + /// Compact Sparse Row (CSR) adjacency map between entities. /// /// Stores a variable-length list of adjacent entity indices for each source entity. @@ -117,6 +128,14 @@ struct AdaptCell /// bit i set ↔ level set i intersects that leaf cell. std::vector cell_active_level_set_mask; + /// Persistent leaf-cell provenance. These arrays are parallel to the + /// top-dimensional entity pool and are preserved through repeated + /// adaptcell refinements. + std::vector cell_source_cell_id; + std::vector cell_refinement_generation; + std::vector cell_refinement_reason; + std::vector cell_host_parent_cell_id; + // --------------------------------------------------------------- // Vertices (dimension 0) // @@ -179,6 +198,16 @@ struct AdaptCell /// This is CSR-style: offsets + flat indices. std::array entity_to_vertex; + /// Structured host provenance for entities. For zero faces this stores + /// the uncut adaptcell leaf cell that produced the extracted surface + /// element. For zero edges on a zero face boundary, host_face_id is filled + /// by the face context when available. + std::array, max_dim> entity_host_cell_id; + std::array, max_dim> entity_host_cell_type; + std::array, max_dim> entity_host_face_id; + std::array, max_dim> entity_source_level_set; + std::array entity_host_cell_vertices; + // --------------------------------------------------------------- // Connectivity cache (d0 → d1) // @@ -213,6 +242,16 @@ struct AdaptCell std::vector zero_entity_parent_dim; ///< -1 if not on a shared parent entity std::vector zero_entity_parent_id; ///< -1 if not on a shared parent entity + /// Provenance back to the uncut adaptcell leaf cell used to generate the + /// zero entity. These arrays mirror entity_host_* for the zero-entity + /// inventory and survive inventory rebuilds as long as the underlying + /// entity provenance is available. + std::vector zero_entity_host_cell_id; + std::vector zero_entity_host_cell_type; + std::vector zero_entity_host_face_id; + std::vector zero_entity_source_level_set; + EntityAdjacency zero_entity_host_cell_vertices; + std::uint32_t zero_entity_version = 0; ///< bumped when zero-entity inventory changes // --------------------------------------------------------------- diff --git a/cpp/src/geometric_quantity.h b/cpp/src/geometric_quantity.h new file mode 100644 index 0000000..076dbe6 --- /dev/null +++ b/cpp/src/geometric_quantity.h @@ -0,0 +1,947 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "cell_topology.h" +#include "reference_cell.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cutcells::geom +{ + +/// Local parent entity in the reference parent cell. +/// dim = 0, 1, 2, tdim denotes vertex, edge, face, cell interior. +/// id is local to that entity dimension, except for cell interiors where it is +/// conventionally -1. +struct ParentEntity +{ + int dim = -1; + int id = -1; + + bool valid() const noexcept { return dim >= 0; } +}; + +enum class Degeneracy +{ + none = 0, + zero_input, + zero_frame, + zero_projection, + invalid_parent_entity +}; + +template +struct VectorQuantity +{ + std::vector value; + T norm = T(0); + Degeneracy degeneracy = Degeneracy::none; + + bool degenerate() const noexcept + { + return degeneracy != Degeneracy::none; + } +}; + +template +struct DisplacementComponents +{ + std::vector normal; + std::vector tangential; + T signed_normal_magnitude = T(0); + bool degenerate_normal = false; +}; + +template +struct Alignment +{ + T cosine = T(0); + T angle = T(0); + bool degenerate = false; +}; + +template +struct LineInterval +{ + bool valid = false; + T t0 = T(0); + T t1 = T(0); +}; + +template +inline T default_tolerance() +{ + return T(128) * std::numeric_limits::epsilon(); +} + +template +inline void require_same_size(std::span a, std::span b, + const char* name) +{ + if (a.size() != b.size()) + throw std::invalid_argument(std::string(name) + ": dimension mismatch"); +} + +template +inline void require_size(std::span a, std::size_t n, const char* name) +{ + if (a.size() != n) + throw std::invalid_argument(std::string(name) + ": unexpected dimension"); +} + +template +inline T dot(std::span a, std::span b) +{ + require_same_size(a, b, "dot"); + T out = T(0); + for (std::size_t i = 0; i < a.size(); ++i) + out += a[i] * b[i]; + return out; +} + +template +inline T squared_norm(std::span a) +{ + return dot(a, a); +} + +template +inline T norm(std::span a) +{ + return std::sqrt(squared_norm(a)); +} + +template +inline std::vector subtract(std::span a, std::span b) +{ + require_same_size(a, b, "subtract"); + std::vector out(a.size(), T(0)); + for (std::size_t i = 0; i < a.size(); ++i) + out[i] = a[i] - b[i]; + return out; +} + +template +inline std::array cross(std::span a, std::span b) +{ + require_size(a, 3, "cross"); + require_size(b, 3, "cross"); + return {a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]}; +} + +template +inline VectorQuantity make_vector_quantity(std::vector value, + T tol, + Degeneracy zero_reason) +{ + const T n = norm(std::span(value.data(), value.size())); + return {std::move(value), n, n <= tol ? zero_reason : Degeneracy::none}; +} + +template +inline std::vector reference_vertex(cell::type parent_cell_type, + int vertex_id) +{ + const int tdim = cell::get_tdim(parent_cell_type); + const int nverts = cell::get_num_vertices(parent_cell_type); + if (vertex_id < 0 || vertex_id >= nverts) + throw std::invalid_argument("reference_vertex: vertex id out of bounds"); + if (parent_cell_type == cell::type::point) + return {}; + + const auto vertices = cell::reference_vertices(parent_cell_type); + std::vector out(static_cast(tdim), T(0)); + for (int d = 0; d < tdim; ++d) + out[static_cast(d)] = + vertices[static_cast(vertex_id * tdim + d)]; + return out; +} + +template +inline VectorQuantity segment_tangent(std::span a, + std::span b, + bool unit = true, + T tol = default_tolerance()) +{ + auto out = subtract(b, a); + const T n = norm(std::span(out.data(), out.size())); + if (n <= tol) + return {std::move(out), n, Degeneracy::zero_frame}; + + if (unit) + { + for (T& x : out) + x /= n; + return {std::move(out), T(1), Degeneracy::none}; + } + + return {std::move(out), n, Degeneracy::none}; +} + +template +inline VectorQuantity parent_edge_tangent(cell::type parent_cell_type, + int parent_edge_id, + bool unit = true, + T tol = default_tolerance()) +{ + const auto edges = cell::edges(parent_cell_type); + if (parent_edge_id < 0 || parent_edge_id >= static_cast(edges.size())) + return {{}, T(0), Degeneracy::invalid_parent_entity}; + + const auto edge = edges[static_cast(parent_edge_id)]; + const auto a = reference_vertex(parent_cell_type, edge[0]); + const auto b = reference_vertex(parent_cell_type, edge[1]); + return segment_tangent( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + unit, + tol); +} + +template +inline VectorQuantity face_normal(std::span a, + std::span b, + std::span c, + bool unit = true, + T tol = default_tolerance()) +{ + require_size(a, 3, "face_normal"); + require_size(b, 3, "face_normal"); + require_size(c, 3, "face_normal"); + + const auto ab = subtract(b, a); + const auto ac = subtract(c, a); + const auto n_array = cross( + std::span(ab.data(), ab.size()), + std::span(ac.data(), ac.size())); + + std::vector out(n_array.begin(), n_array.end()); + const T n = norm(std::span(out.data(), out.size())); + if (n <= tol) + return {std::move(out), n, Degeneracy::zero_frame}; + + if (unit) + { + for (T& x : out) + x /= n; + return {std::move(out), T(1), Degeneracy::none}; + } + + return {std::move(out), n, Degeneracy::none}; +} + +template +inline VectorQuantity parent_face_normal(cell::type parent_cell_type, + int parent_face_id, + bool unit = true, + T tol = default_tolerance()) +{ + if (cell::get_tdim(parent_cell_type) != 3) + return {{}, T(0), Degeneracy::invalid_parent_entity}; + if (parent_face_id < 0 || parent_face_id >= cell::num_faces(parent_cell_type)) + return {{}, T(0), Degeneracy::invalid_parent_entity}; + + const auto face_vertices = cell::face_vertices(parent_cell_type, parent_face_id); + if (face_vertices.size() < 3) + return {{}, T(0), Degeneracy::invalid_parent_entity}; + + const auto a = reference_vertex(parent_cell_type, face_vertices[0]); + const auto b = reference_vertex(parent_cell_type, face_vertices[1]); + const auto c = reference_vertex(parent_cell_type, face_vertices[2]); + return face_normal( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + std::span(c.data(), c.size()), + unit, + tol); +} + +template +inline VectorQuantity segment_normal_2d(std::span a, + std::span b, + bool unit = true, + T tol = default_tolerance()) +{ + require_size(a, 2, "segment_normal_2d"); + require_size(b, 2, "segment_normal_2d"); + + std::vector out = {-(b[1] - a[1]), b[0] - a[0]}; + const T n = norm(std::span(out.data(), out.size())); + if (n <= tol) + return {std::move(out), n, Degeneracy::zero_frame}; + + if (unit) + { + out[0] /= n; + out[1] /= n; + return {std::move(out), T(1), Degeneracy::none}; + } + + return {std::move(out), n, Degeneracy::none}; +} + +template +inline VectorQuantity segment_normal(std::span a, + std::span b, + bool unit = true, + T tol = default_tolerance()) +{ + return segment_normal_2d(a, b, unit, tol); +} + +/// Unit normal to a segment, constrained to a 3D face tangent plane. +/// The orientation is face_normal x segment_tangent. +template +inline VectorQuantity in_face_segment_normal(std::span a, + std::span b, + std::span face_normal_vector, + bool unit = true, + T tol = default_tolerance()) +{ + require_size(a, 3, "in_face_segment_normal"); + require_size(b, 3, "in_face_segment_normal"); + require_size(face_normal_vector, 3, "in_face_segment_normal"); + + auto tangent = segment_tangent(a, b, false, tol); + if (tangent.degenerate()) + return {std::move(tangent.value), tangent.norm, tangent.degeneracy}; + + const auto n_array = cross( + face_normal_vector, + std::span(tangent.value.data(), tangent.value.size())); + std::vector out(n_array.begin(), n_array.end()); + const T n = norm(std::span(out.data(), out.size())); + if (n <= tol) + return {std::move(out), n, Degeneracy::zero_frame}; + + if (unit) + { + for (T& x : out) + x /= n; + return {std::move(out), T(1), Degeneracy::none}; + } + + return {std::move(out), n, Degeneracy::none}; +} + +template +inline VectorQuantity project_onto_line(std::span vector, + std::span line_direction, + T tol = default_tolerance()) +{ + require_same_size(vector, line_direction, "project_onto_line"); + const T dd = squared_norm(line_direction); + std::vector out(vector.size(), T(0)); + if (dd <= tol * tol) + return {std::move(out), T(0), Degeneracy::zero_frame}; + + const T scale = dot(vector, line_direction) / dd; + for (std::size_t i = 0; i < vector.size(); ++i) + out[i] = scale * line_direction[i]; + return make_vector_quantity(std::move(out), tol, Degeneracy::zero_projection); +} + +template +inline VectorQuantity project_into_plane(std::span vector, + std::span plane_normal, + T tol = default_tolerance()) +{ + require_same_size(vector, plane_normal, "project_into_plane"); + const T nn = squared_norm(plane_normal); + std::vector out(vector.begin(), vector.end()); + if (nn <= tol * tol) + { + std::fill(out.begin(), out.end(), T(0)); + return {std::move(out), T(0), Degeneracy::zero_frame}; + } + + const T scale = dot(vector, plane_normal) / nn; + for (std::size_t i = 0; i < vector.size(); ++i) + out[i] -= scale * plane_normal[i]; + return make_vector_quantity(std::move(out), tol, Degeneracy::zero_projection); +} + +template +inline VectorQuantity project_into_parent_face_tangent(cell::type parent_cell_type, + int parent_face_id, + std::span vector, + T tol = default_tolerance()) +{ + const auto normal = parent_face_normal(parent_cell_type, parent_face_id, false, tol); + if (normal.degenerate()) + return {{}, T(0), normal.degeneracy}; + return project_into_plane( + vector, + std::span(normal.value.data(), normal.value.size()), + tol); +} + +template +inline VectorQuantity project_onto_parent_edge(cell::type parent_cell_type, + int parent_edge_id, + std::span vector, + T tol = default_tolerance()) +{ + const auto tangent = parent_edge_tangent(parent_cell_type, parent_edge_id, false, tol); + if (tangent.degenerate()) + return {{}, T(0), tangent.degeneracy}; + return project_onto_line( + vector, + std::span(tangent.value.data(), tangent.value.size()), + tol); +} + +/// Project a raw direction into the tangent frame admitted by the smallest +/// parent entity that hosts the point. The caller supplies that host entity; +/// use smallest_parent_entity_containing_point when it must be inferred from +/// reference coordinates. +template +inline VectorQuantity admissible_direction_in_parent_frame( + cell::type parent_cell_type, + ParentEntity host, + std::span raw_direction, + T tol = default_tolerance()) +{ + const int tdim = cell::get_tdim(parent_cell_type); + if (static_cast(raw_direction.size()) != tdim) + throw std::invalid_argument( + "admissible_direction_in_parent_frame: direction dimension mismatch"); + + if (!host.valid() || host.dim > tdim) + return {{}, T(0), Degeneracy::invalid_parent_entity}; + + if (host.dim == tdim) + { + std::vector out(raw_direction.begin(), raw_direction.end()); + return make_vector_quantity(std::move(out), tol, Degeneracy::zero_input); + } + + if (host.dim == 2 && tdim == 3) + return project_into_parent_face_tangent( + parent_cell_type, host.id, raw_direction, tol); + + if (host.dim == 1) + return project_onto_parent_edge( + parent_cell_type, host.id, raw_direction, tol); + + if (host.dim == 0) + return {std::vector(raw_direction.size(), T(0)), + T(0), + Degeneracy::zero_projection}; + + return {{}, T(0), Degeneracy::invalid_parent_entity}; +} + +template +inline VectorQuantity restricted_level_set_gradient_in_parent_frame( + cell::type parent_cell_type, + ParentEntity host, + std::span raw_gradient, + T tol = default_tolerance()) +{ + return admissible_direction_in_parent_frame( + parent_cell_type, host, raw_gradient, tol); +} + +template +inline DisplacementComponents decompose_displacement( + std::span displacement, + std::span normal_vector, + T tol = default_tolerance()) +{ + require_same_size(displacement, normal_vector, "decompose_displacement"); + DisplacementComponents out; + out.normal.assign(displacement.size(), T(0)); + out.tangential.assign(displacement.begin(), displacement.end()); + + const T nn = squared_norm(normal_vector); + if (nn <= tol * tol) + { + out.degenerate_normal = true; + return out; + } + + const T scale = dot(displacement, normal_vector) / nn; + const T n = std::sqrt(nn); + out.signed_normal_magnitude = dot(displacement, normal_vector) / n; + for (std::size_t i = 0; i < displacement.size(); ++i) + { + out.normal[i] = scale * normal_vector[i]; + out.tangential[i] = displacement[i] - out.normal[i]; + } + return out; +} + +template +inline Alignment alignment(std::span a, + std::span b, + T tol = default_tolerance()) +{ + require_same_size(a, b, "alignment"); + const T na = norm(a); + const T nb = norm(b); + if (na <= tol || nb <= tol) + return {T(0), T(0), true}; + + const T c = std::clamp(dot(a, b) / (na * nb), T(-1), T(1)); + return {c, std::acos(c), false}; +} + +template +inline T cosine_alignment(std::span a, + std::span b, + T tol = default_tolerance()) +{ + return alignment(a, b, tol).cosine; +} + +template +inline T absolute_alignment(std::span a, + std::span b, + T tol = default_tolerance()) +{ + return std::fabs(cosine_alignment(a, b, tol)); +} + +template +inline T angle_between(std::span a, + std::span b, + T tol = default_tolerance()) +{ + return alignment(a, b, tol).angle; +} + +template +inline bool point_in_parent_cell(cell::type parent_cell_type, + std::span x, + T tol = default_tolerance()) +{ + const int tdim = cell::get_tdim(parent_cell_type); + if (static_cast(x.size()) != tdim) + throw std::invalid_argument("point_in_parent_cell: point dimension mismatch"); + + switch (parent_cell_type) + { + case cell::type::point: + return x.empty(); + case cell::type::interval: + return x[0] >= -tol && x[0] <= T(1) + tol; + case cell::type::triangle: + return x[0] >= -tol && x[1] >= -tol && x[0] + x[1] <= T(1) + tol; + case cell::type::quadrilateral: + return x[0] >= -tol && x[0] <= T(1) + tol + && x[1] >= -tol && x[1] <= T(1) + tol; + case cell::type::tetrahedron: + return x[0] >= -tol && x[1] >= -tol && x[2] >= -tol + && x[0] + x[1] + x[2] <= T(1) + tol; + case cell::type::hexahedron: + return x[0] >= -tol && x[0] <= T(1) + tol + && x[1] >= -tol && x[1] <= T(1) + tol + && x[2] >= -tol && x[2] <= T(1) + tol; + case cell::type::prism: + return x[0] >= -tol && x[1] >= -tol && x[0] + x[1] <= T(1) + tol + && x[2] >= -tol && x[2] <= T(1) + tol; + case cell::type::pyramid: + return x[0] >= -tol && x[1] >= -tol && x[2] >= -tol + && x[0] + x[2] <= T(1) + tol + && x[1] + x[2] <= T(1) + tol; + default: + return false; + } +} + +template +inline bool point_on_segment(std::span x, + std::span a, + std::span b, + T tol = default_tolerance(), + T* parameter = nullptr) +{ + require_same_size(x, a, "point_on_segment"); + require_same_size(a, b, "point_on_segment"); + + const auto ab = subtract(b, a); + const T ab2 = squared_norm(std::span(ab.data(), ab.size())); + if (ab2 <= tol * tol) + return false; + + T ax_ab = T(0); + for (std::size_t i = 0; i < x.size(); ++i) + ax_ab += (x[i] - a[i]) * ab[i]; + const T t = ax_ab / ab2; + if (parameter != nullptr) + *parameter = t; + + if (t < -tol || t > T(1) + tol) + return false; + + T distance2 = T(0); + for (std::size_t i = 0; i < x.size(); ++i) + { + const T closest = a[i] + t * ab[i]; + const T r = x[i] - closest; + distance2 += r * r; + } + return distance2 <= tol * tol; +} + +template +inline bool point_on_parent_edge(cell::type parent_cell_type, + int parent_edge_id, + std::span x, + T tol = default_tolerance(), + T* edge_parameter = nullptr) +{ + const auto edges = cell::edges(parent_cell_type); + if (parent_edge_id < 0 || parent_edge_id >= static_cast(edges.size())) + return false; + const auto edge = edges[static_cast(parent_edge_id)]; + const auto a = reference_vertex(parent_cell_type, edge[0]); + const auto b = reference_vertex(parent_cell_type, edge[1]); + return point_on_segment( + x, + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + tol, + edge_parameter); +} + +template +inline bool point_on_parent_face(cell::type parent_cell_type, + int parent_face_id, + std::span x, + T tol = default_tolerance()) +{ + if (cell::get_tdim(parent_cell_type) != 3) + return false; + if (parent_face_id < 0 || parent_face_id >= cell::num_faces(parent_cell_type)) + return false; + + const auto fv = cell::face_vertices(parent_cell_type, parent_face_id); + if (fv.size() < 3) + return false; + + const auto a = reference_vertex(parent_cell_type, fv[0]); + const auto normal = parent_face_normal(parent_cell_type, parent_face_id, false, tol); + if (normal.degenerate()) + return false; + + T signed_distance_num = T(0); + for (std::size_t i = 0; i < x.size(); ++i) + signed_distance_num += (x[i] - a[i]) * normal.value[i]; + if (std::fabs(signed_distance_num) > tol * normal.norm) + return false; + + return point_in_parent_cell(parent_cell_type, x, tol); +} + +template +inline bool point_in_parent_entity(cell::type parent_cell_type, + ParentEntity entity, + std::span x, + T tol = default_tolerance()) +{ + const int tdim = cell::get_tdim(parent_cell_type); + if (!entity.valid() || entity.dim > tdim) + return false; + + if (entity.dim == tdim) + return point_in_parent_cell(parent_cell_type, x, tol); + + if (entity.dim == 2 && tdim == 3) + return point_on_parent_face(parent_cell_type, entity.id, x, tol); + + if (entity.dim == 1) + return point_on_parent_edge(parent_cell_type, entity.id, x, tol); + + if (entity.dim == 0) + { + const auto v = reference_vertex(parent_cell_type, entity.id); + const auto delta = subtract(x, std::span(v.data(), v.size())); + return norm(std::span(delta.data(), delta.size())) <= tol; + } + + return false; +} + +template +inline ParentEntity smallest_parent_entity_containing_point( + cell::type parent_cell_type, + std::span x, + T tol = default_tolerance()) +{ + const int nverts = cell::get_num_vertices(parent_cell_type); + for (int v = 0; v < nverts; ++v) + { + const ParentEntity entity{0, v}; + if (point_in_parent_entity(parent_cell_type, entity, x, tol)) + return entity; + } + + if (cell::get_tdim(parent_cell_type) >= 1) + { + const auto edges = cell::edges(parent_cell_type); + for (int e = 0; e < static_cast(edges.size()); ++e) + { + if (point_on_parent_edge(parent_cell_type, e, x, tol)) + return {1, e}; + } + } + + if (cell::get_tdim(parent_cell_type) == 3) + { + for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) + { + if (point_on_parent_face(parent_cell_type, f, x, tol)) + return {2, f}; + } + } + + if (point_in_parent_cell(parent_cell_type, x, tol)) + return {cell::get_tdim(parent_cell_type), -1}; + + return {-1, -1}; +} + +template +inline bool clip_halfspace(std::span x0, + std::span direction, + std::span normal, + T rhs, + LineInterval& interval, + T tol) +{ + const T value0 = dot(normal, x0); + const T rate = dot(normal, direction); + if (std::fabs(rate) <= tol) + return value0 <= rhs + tol; + + const T hit = (rhs - value0) / rate; + if (rate > T(0)) + interval.t1 = std::min(interval.t1, hit); + else + interval.t0 = std::max(interval.t0, hit); + + return interval.t0 <= interval.t1 + tol; +} + +template +inline bool clip_coordinate_lower(int axis, + std::span x0, + std::span direction, + LineInterval& interval, + T tol) +{ + std::vector n(x0.size(), T(0)); + n[static_cast(axis)] = T(-1); + return clip_halfspace( + x0, direction, std::span(n.data(), n.size()), T(0), interval, tol); +} + +template +inline bool clip_coordinate_upper(int axis, + T upper, + std::span x0, + std::span direction, + LineInterval& interval, + T tol) +{ + std::vector n(x0.size(), T(0)); + n[static_cast(axis)] = T(1); + return clip_halfspace( + x0, direction, std::span(n.data(), n.size()), upper, interval, tol); +} + +template +inline bool clip_sum_upper(std::span axes, + T upper, + std::span x0, + std::span direction, + LineInterval& interval, + T tol) +{ + std::vector n(x0.size(), T(0)); + for (const int axis : axes) + n[static_cast(axis)] = T(1); + return clip_halfspace( + x0, direction, std::span(n.data(), n.size()), upper, interval, tol); +} + +template +inline LineInterval clip_line_interval_in_parent_cell( + cell::type parent_cell_type, + std::span x0, + std::span direction, + T t0 = -std::numeric_limits::infinity(), + T t1 = std::numeric_limits::infinity(), + T tol = default_tolerance()) +{ + const int tdim = cell::get_tdim(parent_cell_type); + if (static_cast(x0.size()) != tdim + || static_cast(direction.size()) != tdim) + { + throw std::invalid_argument( + "clip_line_interval_in_parent_cell: dimension mismatch"); + } + + LineInterval interval{true, t0, t1}; + auto lower = [&](int axis) + { + return clip_coordinate_lower(axis, x0, direction, interval, tol); + }; + auto upper = [&](int axis, T value) + { + return clip_coordinate_upper(axis, value, x0, direction, interval, tol); + }; + auto sum_upper = [&](std::span axes, T value) + { + return clip_sum_upper(axes, value, x0, direction, interval, tol); + }; + + switch (parent_cell_type) + { + case cell::type::point: + interval.valid = x0.empty() && direction.empty(); + return interval; + case cell::type::interval: + interval.valid = lower(0) && upper(0, T(1)); + return interval; + case cell::type::triangle: + { + const std::array axes = {0, 1}; + interval.valid = lower(0) && lower(1) + && sum_upper(std::span(axes), T(1)); + return interval; + } + case cell::type::quadrilateral: + interval.valid = lower(0) && upper(0, T(1)) + && lower(1) && upper(1, T(1)); + return interval; + case cell::type::tetrahedron: + { + const std::array axes = {0, 1, 2}; + interval.valid = lower(0) && lower(1) && lower(2) + && sum_upper(std::span(axes), T(1)); + return interval; + } + case cell::type::hexahedron: + interval.valid = lower(0) && upper(0, T(1)) + && lower(1) && upper(1, T(1)) + && lower(2) && upper(2, T(1)); + return interval; + case cell::type::prism: + { + const std::array axes = {0, 1}; + interval.valid = lower(0) && lower(1) + && sum_upper(std::span(axes), T(1)) + && lower(2) && upper(2, T(1)); + return interval; + } + case cell::type::pyramid: + { + const std::array xz = {0, 2}; + const std::array yz = {1, 2}; + interval.valid = lower(0) && lower(1) && lower(2) + && sum_upper(std::span(xz), T(1)) + && sum_upper(std::span(yz), T(1)); + return interval; + } + default: + interval.valid = false; + return interval; + } +} + +template +inline LineInterval clip_line_interval_in_parent_entity( + cell::type parent_cell_type, + ParentEntity entity, + std::span x0, + std::span direction, + T t0 = -std::numeric_limits::infinity(), + T t1 = std::numeric_limits::infinity(), + T tol = default_tolerance()) +{ + const int tdim = cell::get_tdim(parent_cell_type); + if (!entity.valid() || entity.dim > tdim) + return {false, T(0), T(0)}; + + if (entity.dim == tdim) + return clip_line_interval_in_parent_cell( + parent_cell_type, x0, direction, t0, t1, tol); + + if (entity.dim == 2 && tdim == 3) + { + if (!point_on_parent_face(parent_cell_type, entity.id, x0, tol)) + return {false, T(0), T(0)}; + const auto normal = parent_face_normal(parent_cell_type, entity.id, false, tol); + if (normal.degenerate()) + return {false, T(0), T(0)}; + const T normal_rate = dot( + direction, + std::span(normal.value.data(), normal.value.size())); + if (std::fabs(normal_rate) > tol * std::max(T(1), normal.norm)) + return {false, T(0), T(0)}; + return clip_line_interval_in_parent_cell( + parent_cell_type, x0, direction, t0, t1, tol); + } + + if (entity.dim == 1) + { + T edge_s = T(0); + if (!point_on_parent_edge(parent_cell_type, entity.id, x0, tol, &edge_s)) + return {false, T(0), T(0)}; + + const auto tangent = parent_edge_tangent(parent_cell_type, entity.id, false, tol); + if (tangent.degenerate()) + return {false, T(0), T(0)}; + + const T tt = squared_norm( + std::span(tangent.value.data(), tangent.value.size())); + const T ds = dot( + direction, + std::span(tangent.value.data(), tangent.value.size())) / tt; + std::vector parallel(direction.size(), T(0)); + for (std::size_t i = 0; i < direction.size(); ++i) + parallel[i] = ds * tangent.value[i]; + const auto residual = subtract( + direction, std::span(parallel.data(), parallel.size())); + if (norm(std::span(residual.data(), residual.size())) > tol) + return {false, T(0), T(0)}; + + if (std::fabs(ds) <= tol) + return {true, t0, t1}; + const T a = (T(0) - edge_s) / ds; + const T b = (T(1) - edge_s) / ds; + return {std::max(t0, std::min(a, b)) <= std::min(t1, std::max(a, b)) + tol, + std::max(t0, std::min(a, b)), + std::min(t1, std::max(a, b))}; + } + + if (entity.dim == 0) + { + if (!point_in_parent_entity(parent_cell_type, entity, x0, tol)) + return {false, T(0), T(0)}; + if (norm(direction) <= tol) + return {true, t0, t1}; + return {true, T(0), T(0)}; + } + + return {false, T(0), T(0)}; +} + +} // namespace cutcells::geom diff --git a/cpp/src/graph_criteria.h b/cpp/src/graph_criteria.h new file mode 100644 index 0000000..d778d08 --- /dev/null +++ b/cpp/src/graph_criteria.h @@ -0,0 +1,640 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "geometric_quantity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cutcells::graph_criteria +{ + +enum class HostDimension : std::uint8_t +{ + edge = 1, + face = 2 +}; + +enum class DirectionKind : std::uint8_t +{ + projected_straight_host_normal = 0, + projected_level_set_gradient = 1 +}; + +enum class FailureReason : std::uint8_t +{ + none = 0, + invalid_input, + invalid_parent_entity, + host_point_outside_parent_entity, + invalid_host_frame, + weak_restricted_gradient, + degenerate_direction, + direction_not_admissible, + tangent_to_zero_set, + root_not_on_search_line, + root_segment_leaves_parent_entity, + excessive_correction_distance, + excessive_tangential_shift, + excessive_drift_amplification, + direction_too_tangential_to_host, + edge_ordering_fold, + face_degenerate, + surface_jacobian_not_positive, + refinement_requested +}; + +inline std::string_view failure_reason_name(FailureReason reason) +{ + switch (reason) + { + case FailureReason::none: + return "none"; + case FailureReason::invalid_input: + return "invalid_input"; + case FailureReason::invalid_parent_entity: + return "invalid_parent_entity"; + case FailureReason::host_point_outside_parent_entity: + return "host_point_outside_parent_entity"; + case FailureReason::invalid_host_frame: + return "invalid_host_frame"; + case FailureReason::weak_restricted_gradient: + return "weak_restricted_gradient"; + case FailureReason::degenerate_direction: + return "degenerate_direction"; + case FailureReason::direction_not_admissible: + return "direction_not_admissible"; + case FailureReason::tangent_to_zero_set: + return "tangent_to_zero_set"; + case FailureReason::root_not_on_search_line: + return "root_not_on_search_line"; + case FailureReason::root_segment_leaves_parent_entity: + return "root_segment_leaves_parent_entity"; + case FailureReason::excessive_correction_distance: + return "excessive_correction_distance"; + case FailureReason::excessive_tangential_shift: + return "excessive_tangential_shift"; + case FailureReason::excessive_drift_amplification: + return "excessive_drift_amplification"; + case FailureReason::direction_too_tangential_to_host: + return "direction_too_tangential_to_host"; + case FailureReason::edge_ordering_fold: + return "edge_ordering_fold"; + case FailureReason::face_degenerate: + return "face_degenerate"; + case FailureReason::surface_jacobian_not_positive: + return "surface_jacobian_not_positive"; + case FailureReason::refinement_requested: + return "refinement_requested"; + } + return "unknown"; +} + +template +struct Options +{ + T tolerance = geom::default_tolerance(); + T min_restricted_gradient_strength = T(64) * std::numeric_limits::epsilon(); + T min_transversality = T(1.0e-6); + T min_host_normal_alignment = T(0.25); + T max_drift_amplification = T(4); + T max_relative_correction_distance = T(0.5); + T max_relative_tangential_shift = T(0.25); + T max_root_line_residual = std::sqrt(std::numeric_limits::epsilon()); + T min_edge_ordering_fraction = std::sqrt(std::numeric_limits::epsilon()); + T min_surface_jacobian_ratio = std::sqrt(std::numeric_limits::epsilon()); +}; + +template +struct HostFrame +{ + HostDimension dimension = HostDimension::edge; + std::vector normal; + std::vector tangent; + T h = T(1); +}; + +template +struct Metrics +{ + T restricted_gradient_strength = T(0); + T true_transversality = T(0); + T host_normal_alignment = T(0); + T drift_amplification = std::numeric_limits::infinity(); + T root_alpha = T(0); + T root_line_residual = T(0); + bool root_segment_contained = false; + T relative_correction_distance = T(0); + T relative_tangential_shift = T(0); +}; + +template +struct DirectionReport +{ + bool accepted = false; + DirectionKind kind = DirectionKind::projected_straight_host_normal; + FailureReason failure_reason = FailureReason::none; + Metrics metrics; +}; + +template +struct SelectionReport +{ + bool accepted = false; + bool request_refinement = false; + DirectionKind selected_kind = DirectionKind::projected_straight_host_normal; + FailureReason failure_reason = FailureReason::none; + DirectionReport straight_host_normal; + DirectionReport level_set_gradient; + std::vector selected_direction; + std::vector selected_root; +}; + +template +struct EdgeOrderingReport +{ + bool accepted = false; + FailureReason failure_reason = FailureReason::none; + T minimum_gap_ratio = std::numeric_limits::infinity(); +}; + +template +struct FaceQualityReport +{ + bool accepted = false; + FailureReason failure_reason = FailureReason::none; + T minimum_surface_jacobian_ratio = std::numeric_limits::infinity(); + int failed_triangle_index = -1; + T failed_surface_jacobian_ratio = std::numeric_limits::quiet_NaN(); +}; + +template +inline std::span vector_span(const std::vector& v) +{ + return std::span(v.data(), v.size()); +} + +template +inline bool same_dimension(std::size_t n, std::span a) +{ + return a.size() == n; +} + +template +inline bool same_dimension(std::size_t n, std::span a, Spans... rest) +{ + return a.size() == n && same_dimension(n, rest...); +} + +template +inline T safe_scale(T a, T b, T c, T d) +{ + return std::max({T(1), std::fabs(a), std::fabs(b), std::fabs(c), std::fabs(d)}); +} + +template +inline T absolute_cosine(std::span a, + std::span b, + T tol) +{ + const auto alignment = geom::alignment(a, b, tol); + if (alignment.degenerate) + return T(0); + return std::fabs(alignment.cosine); +} + +template +inline T drift_from_alignment(T abs_cosine, T tol) +{ + if (abs_cosine <= tol) + return std::numeric_limits::infinity(); + const T tangential2 = std::max(T(0), T(1) - abs_cosine * abs_cosine); + return std::sqrt(tangential2) / abs_cosine; +} + +template +inline bool valid_host_frame(const HostFrame& host, + std::size_t tdim, + T tol) +{ + if (!(host.h > tol)) + return false; + if (host.normal.size() != tdim + || geom::norm(vector_span(host.normal)) <= tol) + { + return false; + } + if (host.dimension == HostDimension::edge) + { + if (host.tangent.size() != tdim + || geom::norm(vector_span(host.tangent)) <= tol) + { + return false; + } + } + return host.dimension == HostDimension::edge + || host.dimension == HostDimension::face; +} + +template +inline bool direction_admissible_in_parent_entity( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + std::span x_h, + std::span direction, + T tol) +{ + if (!geom::point_in_parent_entity( + parent_cell_type, admissible_parent_entity, x_h, tol)) + { + return false; + } + if (admissible_parent_entity.dim == 0 + && geom::norm(direction) > tol) + { + return false; + } + + const auto interval = geom::clip_line_interval_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + x_h, + direction, + -std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + tol); + + return interval.valid && interval.t0 <= tol && interval.t1 >= -tol; +} + +template +inline bool root_segment_contained_in_parent_entity( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + std::span x_h, + std::span direction, + T alpha, + T tol) +{ + const auto interval = geom::clip_line_interval_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + x_h, + direction, + -std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + tol); + if (!interval.valid) + return false; + + const T lo = std::min(T(0), alpha); + const T hi = std::max(T(0), alpha); + return lo >= interval.t0 - tol && hi <= interval.t1 + tol; +} + +template +inline T relative_tangential_shift(std::span delta, + const HostFrame& host, + T tol) +{ + if (host.dimension == HostDimension::edge) + { + const T tangent_norm = geom::norm(vector_span(host.tangent)); + if (tangent_norm <= tol) + return std::numeric_limits::infinity(); + return std::fabs(geom::dot(delta, vector_span(host.tangent))) + / (tangent_norm * host.h); + } + + const auto tangential = geom::project_into_plane( + delta, vector_span(host.normal), tol); + if (tangential.degenerate() && tangential.degeneracy == geom::Degeneracy::zero_frame) + return std::numeric_limits::infinity(); + return tangential.norm / host.h; +} + +template +inline DirectionReport evaluate_direction( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + const HostFrame& host, + std::span x_h, + std::span raw_level_set_gradient, + std::span candidate_direction, + std::span x_c, + DirectionKind kind, + const Options& options = {}) +{ + DirectionReport report; + report.kind = kind; + + const int tdim = cell::get_tdim(parent_cell_type); + if (tdim <= 0 + || !admissible_parent_entity.valid() + || admissible_parent_entity.dim > tdim) + { + report.failure_reason = FailureReason::invalid_parent_entity; + return report; + } + + if (!same_dimension( + static_cast(tdim), + x_h, + raw_level_set_gradient, + candidate_direction, + x_c)) + { + report.failure_reason = FailureReason::invalid_input; + return report; + } + + const T tol = options.tolerance; + if (!valid_host_frame(host, static_cast(tdim), tol)) + { + report.failure_reason = FailureReason::invalid_host_frame; + return report; + } + + if (!geom::point_in_parent_entity( + parent_cell_type, admissible_parent_entity, x_h, tol)) + { + report.failure_reason = FailureReason::host_point_outside_parent_entity; + return report; + } + + const auto restricted_gradient = + geom::restricted_level_set_gradient_in_parent_frame( + parent_cell_type, + admissible_parent_entity, + raw_level_set_gradient, + tol); + report.metrics.restricted_gradient_strength = restricted_gradient.norm; + if (restricted_gradient.degenerate() + || restricted_gradient.norm < options.min_restricted_gradient_strength) + { + report.failure_reason = FailureReason::weak_restricted_gradient; + return report; + } + + const T direction_norm = geom::norm(candidate_direction); + if (direction_norm <= tol) + { + report.failure_reason = FailureReason::degenerate_direction; + return report; + } + + if (!direction_admissible_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + x_h, + candidate_direction, + tol)) + { + report.failure_reason = FailureReason::direction_not_admissible; + return report; + } + + report.metrics.true_transversality = + std::fabs(geom::dot( + candidate_direction, + vector_span(restricted_gradient.value))) + / (direction_norm * restricted_gradient.norm); + + report.metrics.host_normal_alignment = + absolute_cosine(candidate_direction, vector_span(host.normal), tol); + report.metrics.drift_amplification = + drift_from_alignment(report.metrics.host_normal_alignment, tol); + + const auto delta = geom::subtract(x_c, x_h); + const T delta_norm = geom::norm(vector_span(delta)); + const T dd = direction_norm * direction_norm; + report.metrics.root_alpha = + geom::dot(vector_span(delta), candidate_direction) / dd; + + std::vector residual(delta.size(), T(0)); + for (std::size_t i = 0; i < delta.size(); ++i) + { + residual[i] = delta[i] + - report.metrics.root_alpha * candidate_direction[i]; + } + const T residual_norm = geom::norm(vector_span(residual)); + const T residual_scale = safe_scale( + host.h, + delta_norm, + std::fabs(report.metrics.root_alpha) * direction_norm, + direction_norm); + report.metrics.root_line_residual = residual_norm / residual_scale; + + report.metrics.root_segment_contained = + root_segment_contained_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + x_h, + candidate_direction, + report.metrics.root_alpha, + tol) + && geom::point_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + x_c, + tol); + + report.metrics.relative_correction_distance = delta_norm / host.h; + report.metrics.relative_tangential_shift = + relative_tangential_shift(vector_span(delta), host, tol); + + if (report.metrics.true_transversality < options.min_transversality) + { + report.failure_reason = FailureReason::tangent_to_zero_set; + return report; + } + if (report.metrics.root_line_residual > options.max_root_line_residual) + { + report.failure_reason = FailureReason::root_not_on_search_line; + return report; + } + if (!report.metrics.root_segment_contained) + { + report.failure_reason = FailureReason::root_segment_leaves_parent_entity; + return report; + } + if (report.metrics.relative_correction_distance + > options.max_relative_correction_distance) + { + report.failure_reason = FailureReason::excessive_correction_distance; + return report; + } + if (report.metrics.relative_tangential_shift + > options.max_relative_tangential_shift) + { + report.failure_reason = FailureReason::excessive_tangential_shift; + return report; + } + if (report.metrics.drift_amplification > options.max_drift_amplification) + { + report.failure_reason = FailureReason::excessive_drift_amplification; + return report; + } + if (report.metrics.host_normal_alignment < options.min_host_normal_alignment) + { + report.failure_reason = FailureReason::direction_too_tangential_to_host; + return report; + } + + report.accepted = true; + report.failure_reason = FailureReason::none; + return report; +} + +template +inline SelectionReport select_preferred_direction( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + const HostFrame& host, + std::span x_h, + std::span raw_level_set_gradient, + std::span projected_level_set_gradient_direction, + std::span projected_level_set_gradient_root, + std::span projected_straight_host_normal_direction, + std::span projected_straight_host_normal_root, + const Options& options = {}) +{ + SelectionReport report; + report.straight_host_normal = evaluate_direction( + parent_cell_type, + admissible_parent_entity, + host, + x_h, + raw_level_set_gradient, + projected_straight_host_normal_direction, + projected_straight_host_normal_root, + DirectionKind::projected_straight_host_normal, + options); + report.level_set_gradient = evaluate_direction( + parent_cell_type, + admissible_parent_entity, + host, + x_h, + raw_level_set_gradient, + projected_level_set_gradient_direction, + projected_level_set_gradient_root, + DirectionKind::projected_level_set_gradient, + options); + + if (report.straight_host_normal.accepted) + { + report.accepted = true; + report.selected_kind = DirectionKind::projected_straight_host_normal; + report.failure_reason = FailureReason::none; + report.selected_direction.assign( + projected_straight_host_normal_direction.begin(), + projected_straight_host_normal_direction.end()); + report.selected_root.assign( + projected_straight_host_normal_root.begin(), + projected_straight_host_normal_root.end()); + return report; + } + + if (report.level_set_gradient.accepted + && report.level_set_gradient.metrics.drift_amplification + <= options.max_drift_amplification) + { + report.accepted = true; + report.selected_kind = DirectionKind::projected_level_set_gradient; + report.failure_reason = FailureReason::none; + report.selected_direction.assign( + projected_level_set_gradient_direction.begin(), + projected_level_set_gradient_direction.end()); + report.selected_root.assign( + projected_level_set_gradient_root.begin(), + projected_level_set_gradient_root.end()); + return report; + } + + report.accepted = false; + report.request_refinement = true; + report.failure_reason = FailureReason::refinement_requested; + return report; +} + +template +inline EdgeOrderingReport evaluate_projected_edge_ordering( + std::span host_points, + std::span corrected_points, + int point_dim, + std::span host_tangent, + T h_edge, + const Options& options = {}) +{ + EdgeOrderingReport report; + const T tol = options.tolerance; + + if (point_dim <= 0 + || host_points.size() != corrected_points.size() + || host_points.size() % static_cast(point_dim) != 0 + || host_tangent.size() != static_cast(point_dim) + || !(h_edge > tol)) + { + report.failure_reason = FailureReason::invalid_input; + return report; + } + + const std::size_t n = + host_points.size() / static_cast(point_dim); + if (n < 2 || geom::norm(host_tangent) <= tol) + { + report.failure_reason = FailureReason::invalid_host_frame; + return report; + } + + std::vector host_s(n, T(0)); + std::vector corr_s(n, T(0)); + for (std::size_t i = 0; i < n; ++i) + { + const auto hp = host_points.subspan( + i * static_cast(point_dim), + static_cast(point_dim)); + const auto cp = corrected_points.subspan( + i * static_cast(point_dim), + static_cast(point_dim)); + host_s[i] = geom::dot(hp, host_tangent); + corr_s[i] = geom::dot(cp, host_tangent); + } + + const T orientation = (host_s.back() >= host_s.front()) ? T(1) : T(-1); + report.minimum_gap_ratio = std::numeric_limits::infinity(); + for (std::size_t i = 1; i < n; ++i) + { + const T host_gap = orientation * (host_s[i] - host_s[i - 1]); + const T corr_gap = orientation * (corr_s[i] - corr_s[i - 1]); + if (host_gap <= tol * h_edge) + { + report.failure_reason = FailureReason::invalid_host_frame; + return report; + } + const T gap_ratio = corr_gap / host_gap; + report.minimum_gap_ratio = std::min(report.minimum_gap_ratio, gap_ratio); + if (gap_ratio <= options.min_edge_ordering_fraction) + { + report.failure_reason = FailureReason::edge_ordering_fold; + return report; + } + } + + report.accepted = true; + report.failure_reason = FailureReason::none; + return report; +} + +} // namespace cutcells::graph_criteria diff --git a/cpp/src/tangent_shift.h b/cpp/src/tangent_shift.h new file mode 100644 index 0000000..1443bb3 --- /dev/null +++ b/cpp/src/tangent_shift.h @@ -0,0 +1,873 @@ +// Copyright (c) 2026 ONERA +// Authors: Susanne Claus +// This file is part of CutCells +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "geometric_quantity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cutcells::tangent_shift +{ + +enum class FailureReason : std::uint8_t +{ + none = 0, + invalid_input, + invalid_parent_entity, + invalid_curve, + node_index_out_of_range, + target_arclength_out_of_range, + current_point_outside_parent_entity, + correction_direction_not_admissible, + correction_left_parent_entity, + correction_failed, + phi_residual_too_large, + ordering_destroyed, + spacing_too_small, + arclength_error_too_large, + refinement_requested +}; + +inline std::string_view failure_reason_name(FailureReason reason) +{ + switch (reason) + { + case FailureReason::none: + return "none"; + case FailureReason::invalid_input: + return "invalid_input"; + case FailureReason::invalid_parent_entity: + return "invalid_parent_entity"; + case FailureReason::invalid_curve: + return "invalid_curve"; + case FailureReason::node_index_out_of_range: + return "node_index_out_of_range"; + case FailureReason::target_arclength_out_of_range: + return "target_arclength_out_of_range"; + case FailureReason::current_point_outside_parent_entity: + return "current_point_outside_parent_entity"; + case FailureReason::correction_direction_not_admissible: + return "correction_direction_not_admissible"; + case FailureReason::correction_left_parent_entity: + return "correction_left_parent_entity"; + case FailureReason::correction_failed: + return "correction_failed"; + case FailureReason::phi_residual_too_large: + return "phi_residual_too_large"; + case FailureReason::ordering_destroyed: + return "ordering_destroyed"; + case FailureReason::spacing_too_small: + return "spacing_too_small"; + case FailureReason::arclength_error_too_large: + return "arclength_error_too_large"; + case FailureReason::refinement_requested: + return "refinement_requested"; + } + return "unknown"; +} + +template +struct Options +{ + T tolerance = geom::default_tolerance(); + T phi_tolerance = std::sqrt(std::numeric_limits::epsilon()); + T min_direction_norm = T(64) * std::numeric_limits::epsilon(); + + T max_relative_arclength_drift = T(1.0e-3); + T max_absolute_arclength_drift = std::sqrt(std::numeric_limits::epsilon()); + T max_relative_final_arclength_error = T(1.0e-3); + T max_absolute_final_arclength_error = std::sqrt(std::numeric_limits::epsilon()); + + T min_relative_spacing = std::sqrt(std::numeric_limits::epsilon()); + T min_absolute_spacing = T(0); + + int max_correction_iterations = 16; + int max_line_search_iterations = 16; +}; + +template +struct Metrics +{ + T curve_length = T(0); + T desired_arclength = T(0); + T actual_arclength = T(0); + T initial_arclength_error = T(0); + T final_arclength = T(0); + T final_arclength_error = T(0); + T phi_residual = std::numeric_limits::infinity(); + T minimum_spacing = std::numeric_limits::infinity(); + T minimum_spacing_ratio = std::numeric_limits::infinity(); +}; + +template +struct CorrectionReport +{ + bool accepted = false; + bool left_parent_entity = false; + FailureReason failure_reason = FailureReason::none; + std::vector point; + T residual = std::numeric_limits::infinity(); + int iterations = 0; +}; + +template +struct ShiftReport +{ + bool accepted = false; + bool corrected = false; + bool request_refinement = false; + FailureReason failure_reason = FailureReason::none; + + Metrics metrics; + int correction_iterations = 0; + + std::vector shifted_seed; + std::vector corrected_node; + std::vector corrected_edge_points; +}; + +template +struct ArclengthPoint +{ + bool valid = false; + std::vector point; + std::size_t segment = 0; + T segment_parameter = T(0); +}; + +template +struct ClosestArclength +{ + bool valid = false; + T arclength = T(0); + T distance = std::numeric_limits::infinity(); + std::size_t segment = 0; + T segment_parameter = T(0); +}; + +template +inline std::span vector_span(const std::vector& v) +{ + return std::span(v.data(), v.size()); +} + +template +inline std::span point_span(std::span points, + int point_dim, + std::size_t i) +{ + return points.subspan( + i * static_cast(point_dim), + static_cast(point_dim)); +} + +template +inline std::span mutable_point_span(std::span points, + int point_dim, + std::size_t i) +{ + return points.subspan( + i * static_cast(point_dim), + static_cast(point_dim)); +} + +template +inline bool flat_point_array_valid(std::span points, int point_dim) +{ + return point_dim > 0 + && !points.empty() + && points.size() % static_cast(point_dim) == 0; +} + +template +inline T point_distance(std::span a, std::span b) +{ + const auto delta = geom::subtract(a, b); + return geom::norm(vector_span(delta)); +} + +template +inline T arclength_tolerance(T length, T absolute_tolerance, T relative_tolerance) +{ + return absolute_tolerance + relative_tolerance * length; +} + +template +inline bool cumulative_arclength(std::span points, + int point_dim, + std::vector& cumulative, + T tol = geom::default_tolerance()) +{ + if (!flat_point_array_valid(points, point_dim)) + return false; + + const std::size_t n = points.size() / static_cast(point_dim); + if (n < 2) + return false; + + cumulative.assign(n, T(0)); + for (std::size_t i = 1; i < n; ++i) + { + const T ds = point_distance( + point_span(points, point_dim, i - 1), + point_span(points, point_dim, i)); + if (!std::isfinite(ds)) + return false; + cumulative[i] = cumulative[i - 1] + ds; + } + + return cumulative.back() > tol; +} + +template +inline ArclengthPoint point_at_arclength(std::span points, + int point_dim, + T target_arclength, + T tol = geom::default_tolerance()) +{ + ArclengthPoint out; + std::vector cumulative; + if (!cumulative_arclength(points, point_dim, cumulative, tol)) + return out; + + const T length = cumulative.back(); + if (target_arclength < -tol || target_arclength > length + tol) + return out; + + const std::size_t n = cumulative.size(); + if (target_arclength <= tol) + { + out.valid = true; + out.point.assign(points.begin(), points.begin() + point_dim); + out.segment = 0; + out.segment_parameter = T(0); + return out; + } + if (length - target_arclength <= tol) + { + out.valid = true; + out.point.assign( + points.end() - static_cast(point_dim), + points.end()); + out.segment = n - 2; + out.segment_parameter = T(1); + return out; + } + + for (std::size_t i = 1; i < n; ++i) + { + if (target_arclength > cumulative[i] + tol) + continue; + + const T segment_length = cumulative[i] - cumulative[i - 1]; + if (segment_length <= tol) + continue; + + T u = (target_arclength - cumulative[i - 1]) / segment_length; + if (u < -tol || u > T(1) + tol) + return out; + if (u < T(0)) + u = T(0); + if (u > T(1)) + u = T(1); + + const auto a = point_span(points, point_dim, i - 1); + const auto b = point_span(points, point_dim, i); + out.point.assign(static_cast(point_dim), T(0)); + for (int d = 0; d < point_dim; ++d) + out.point[static_cast(d)] = + (T(1) - u) * a[static_cast(d)] + + u * b[static_cast(d)]; + out.valid = true; + out.segment = i - 1; + out.segment_parameter = u; + return out; + } + + return out; +} + +template +inline ClosestArclength closest_arclength_on_polyline( + std::span points, + int point_dim, + std::span x, + T tol = geom::default_tolerance()) +{ + ClosestArclength out; + if (!flat_point_array_valid(points, point_dim) + || x.size() != static_cast(point_dim)) + { + return out; + } + + std::vector cumulative; + if (!cumulative_arclength(points, point_dim, cumulative, tol)) + return out; + + const std::size_t n = cumulative.size(); + T best_distance2 = std::numeric_limits::infinity(); + for (std::size_t i = 1; i < n; ++i) + { + const auto a = point_span(points, point_dim, i - 1); + const auto b = point_span(points, point_dim, i); + const auto ab = geom::subtract(b, a); + const T ab2 = geom::squared_norm(vector_span(ab)); + if (ab2 <= tol * tol) + continue; + + T u = T(0); + for (int d = 0; d < point_dim; ++d) + u += (x[static_cast(d)] - a[static_cast(d)]) + * ab[static_cast(d)]; + u /= ab2; + const T u_closest = std::clamp(u, T(0), T(1)); + + T distance2 = T(0); + for (int d = 0; d < point_dim; ++d) + { + const T closest = + a[static_cast(d)] + + u_closest * ab[static_cast(d)]; + const T r = x[static_cast(d)] - closest; + distance2 += r * r; + } + + if (distance2 < best_distance2) + { + best_distance2 = distance2; + out.valid = true; + out.distance = std::sqrt(distance2); + out.segment = i - 1; + out.segment_parameter = u_closest; + out.arclength = + cumulative[i - 1] + + u_closest * (cumulative[i] - cumulative[i - 1]); + } + } + + return out; +} + +template +struct LayoutReport +{ + bool accepted = false; + FailureReason failure_reason = FailureReason::none; + std::vector arclengths; + T minimum_spacing = std::numeric_limits::infinity(); + T minimum_spacing_ratio = std::numeric_limits::infinity(); +}; + +template +inline LayoutReport evaluate_ordering_and_spacing( + std::span provisional_curve_points, + std::span edge_points, + int point_dim, + const Options& options = {}) +{ + LayoutReport report; + if (!flat_point_array_valid(provisional_curve_points, point_dim) + || !flat_point_array_valid(edge_points, point_dim)) + { + report.failure_reason = FailureReason::invalid_input; + return report; + } + + const std::size_t n = + edge_points.size() / static_cast(point_dim); + if (n < 2) + { + report.failure_reason = FailureReason::invalid_input; + return report; + } + + std::vector cumulative; + if (!cumulative_arclength( + provisional_curve_points, point_dim, cumulative, options.tolerance)) + { + report.failure_reason = FailureReason::invalid_curve; + return report; + } + const T length = cumulative.back(); + const T min_spacing = + options.min_absolute_spacing + options.min_relative_spacing * length; + const T ordering_tol = options.tolerance * std::max(T(1), length); + + report.arclengths.assign(n, T(0)); + for (std::size_t i = 0; i < n; ++i) + { + const auto closest = closest_arclength_on_polyline( + provisional_curve_points, + point_dim, + point_span(edge_points, point_dim, i), + options.tolerance); + if (!closest.valid) + { + report.failure_reason = FailureReason::invalid_curve; + return report; + } + report.arclengths[i] = closest.arclength; + } + + for (std::size_t i = 1; i < n; ++i) + { + const T gap = report.arclengths[i] - report.arclengths[i - 1]; + if (gap < -ordering_tol) + { + report.failure_reason = FailureReason::ordering_destroyed; + return report; + } + + report.minimum_spacing = std::min(report.minimum_spacing, gap); + if (gap <= min_spacing) + { + report.minimum_spacing_ratio = length > options.tolerance + ? gap / length + : std::numeric_limits::infinity(); + report.failure_reason = FailureReason::spacing_too_small; + return report; + } + } + + report.accepted = true; + report.failure_reason = FailureReason::none; + report.minimum_spacing_ratio = length > options.tolerance + ? report.minimum_spacing / length + : std::numeric_limits::infinity(); + return report; +} + +template +inline CorrectionReport correct_seed_to_zero( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + std::span seed, + Phi&& phi, + Grad&& grad, + const Options& options = {}) +{ + CorrectionReport report; + const int tdim = cell::get_tdim(parent_cell_type); + if (!admissible_parent_entity.valid() + || admissible_parent_entity.dim > tdim + || static_cast(seed.size()) != tdim) + { + report.failure_reason = FailureReason::invalid_parent_entity; + return report; + } + + if (!geom::point_in_parent_entity( + parent_cell_type, admissible_parent_entity, seed, options.tolerance)) + { + report.failure_reason = FailureReason::correction_left_parent_entity; + report.left_parent_entity = true; + return report; + } + + report.point.assign(seed.begin(), seed.end()); + T f = static_cast(std::invoke( + phi, + std::span(report.point.data(), report.point.size()))); + report.residual = std::fabs(f); + if (!std::isfinite(report.residual)) + { + report.failure_reason = FailureReason::correction_failed; + return report; + } + if (report.residual <= options.phi_tolerance) + { + report.accepted = true; + report.failure_reason = FailureReason::none; + return report; + } + + std::vector g(static_cast(tdim), T(0)); + bool full_step_left_parent = false; + for (int iter = 0; iter < options.max_correction_iterations; ++iter) + { + std::fill(g.begin(), g.end(), T(0)); + std::invoke( + grad, + std::span(report.point.data(), report.point.size()), + std::span(g.data(), g.size())); + + const auto direction = geom::admissible_direction_in_parent_frame( + parent_cell_type, + admissible_parent_entity, + std::span(g.data(), g.size()), + options.tolerance); + if (direction.degenerate() || direction.norm <= options.min_direction_norm) + { + report.failure_reason = FailureReason::correction_direction_not_admissible; + return report; + } + + const T denom = geom::dot( + std::span(g.data(), g.size()), + vector_span(direction.value)); + if (std::fabs(denom) <= options.min_direction_norm * direction.norm) + { + report.failure_reason = FailureReason::correction_direction_not_admissible; + return report; + } + + std::vector step(static_cast(tdim), T(0)); + const T scale = -f / denom; + for (int d = 0; d < tdim; ++d) + step[static_cast(d)] = + scale * direction.value[static_cast(d)]; + if (geom::norm(vector_span(step)) <= options.tolerance) + break; + + std::vector full_step_point(report.point); + for (int d = 0; d < tdim; ++d) + full_step_point[static_cast(d)] += + step[static_cast(d)]; + if (!geom::point_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + vector_span(full_step_point), + options.tolerance)) + { + full_step_left_parent = true; + report.left_parent_entity = true; + } + + bool accepted_step = false; + std::vector candidate(static_cast(tdim), T(0)); + T candidate_f = f; + T alpha = T(1); + for (int ls = 0; ls < options.max_line_search_iterations; ++ls) + { + for (int d = 0; d < tdim; ++d) + candidate[static_cast(d)] = + report.point[static_cast(d)] + + alpha * step[static_cast(d)]; + + if (!geom::point_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + vector_span(candidate), + options.tolerance)) + { + alpha *= T(0.5); + continue; + } + + candidate_f = static_cast(std::invoke( + phi, + std::span(candidate.data(), candidate.size()))); + if (!std::isfinite(candidate_f)) + { + alpha *= T(0.5); + continue; + } + if (std::fabs(candidate_f) <= options.phi_tolerance + || std::fabs(candidate_f) < report.residual) + { + accepted_step = true; + break; + } + alpha *= T(0.5); + } + + if (!accepted_step) + { + report.failure_reason = full_step_left_parent + ? FailureReason::correction_left_parent_entity + : FailureReason::correction_failed; + return report; + } + + report.point = std::move(candidate); + f = candidate_f; + report.residual = std::fabs(f); + report.iterations = iter + 1; + if (report.residual <= options.phi_tolerance) + { + report.accepted = true; + report.failure_reason = FailureReason::none; + return report; + } + } + + report.failure_reason = full_step_left_parent + ? FailureReason::correction_left_parent_entity + : FailureReason::phi_residual_too_large; + return report; +} + +template +inline void set_failure(ShiftReport& report, + FailureReason reason, + bool request_refinement = true) +{ + report.accepted = false; + report.request_refinement = request_refinement; + report.failure_reason = reason == FailureReason::none + ? FailureReason::refinement_requested + : reason; +} + +template +inline bool validate_candidate( + ShiftReport& report, + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + std::span provisional_curve_points, + int point_dim, + int node_index, + T desired_arclength, + Phi&& phi, + const Options& options) +{ + if (!geom::point_in_parent_entity( + parent_cell_type, + admissible_parent_entity, + vector_span(report.corrected_node), + options.tolerance)) + { + set_failure(report, FailureReason::correction_left_parent_entity); + return false; + } + + const T residual = std::fabs(static_cast(std::invoke( + phi, + std::span( + report.corrected_node.data(), + report.corrected_node.size())))); + report.metrics.phi_residual = residual; + if (!std::isfinite(residual) || residual > options.phi_tolerance) + { + set_failure(report, FailureReason::phi_residual_too_large); + return false; + } + + const auto layout = evaluate_ordering_and_spacing( + provisional_curve_points, + std::span( + report.corrected_edge_points.data(), + report.corrected_edge_points.size()), + point_dim, + options); + report.metrics.minimum_spacing = layout.minimum_spacing; + report.metrics.minimum_spacing_ratio = layout.minimum_spacing_ratio; + if (!layout.accepted) + { + set_failure(report, layout.failure_reason); + return false; + } + + const auto final_closest = closest_arclength_on_polyline( + provisional_curve_points, + point_dim, + vector_span(report.corrected_node), + options.tolerance); + if (!final_closest.valid) + { + set_failure(report, FailureReason::invalid_curve); + return false; + } + + report.metrics.final_arclength = final_closest.arclength; + report.metrics.final_arclength_error = + std::fabs(final_closest.arclength - desired_arclength); + const T final_tol = arclength_tolerance( + report.metrics.curve_length, + options.max_absolute_final_arclength_error, + options.max_relative_final_arclength_error); + if (report.metrics.final_arclength_error > final_tol) + { + set_failure(report, FailureReason::arclength_error_too_large); + return false; + } + + if (node_index >= 0 + && static_cast(node_index) < layout.arclengths.size()) + { + report.metrics.final_arclength = + layout.arclengths[static_cast(node_index)]; + } + + report.accepted = true; + report.request_refinement = false; + report.failure_reason = FailureReason::none; + return true; +} + +template +inline ShiftReport correct_projected_edge_node( + cell::type parent_cell_type, + geom::ParentEntity admissible_parent_entity, + std::span provisional_curve_points, + std::span projected_edge_points, + int point_dim, + std::span desired_arclength_fractions, + int node_index, + Phi&& phi, + Grad&& grad, + const Options& options = {}) +{ + ShiftReport report; + if (!flat_point_array_valid(provisional_curve_points, point_dim) + || !flat_point_array_valid(projected_edge_points, point_dim) + || point_dim != cell::get_tdim(parent_cell_type)) + { + set_failure(report, FailureReason::invalid_input, false); + return report; + } + + const std::size_t n_nodes = + projected_edge_points.size() / static_cast(point_dim); + if (desired_arclength_fractions.size() != n_nodes) + { + set_failure(report, FailureReason::invalid_input, false); + return report; + } + if (node_index < 0 || static_cast(node_index) >= n_nodes) + { + set_failure(report, FailureReason::node_index_out_of_range, false); + return report; + } + + const int tdim = cell::get_tdim(parent_cell_type); + if (!admissible_parent_entity.valid() + || admissible_parent_entity.dim > tdim) + { + set_failure(report, FailureReason::invalid_parent_entity, false); + return report; + } + + std::vector cumulative; + if (!cumulative_arclength( + provisional_curve_points, point_dim, cumulative, options.tolerance)) + { + set_failure(report, FailureReason::invalid_curve); + return report; + } + report.metrics.curve_length = cumulative.back(); + + const auto current = point_span( + projected_edge_points, + point_dim, + static_cast(node_index)); + if (!geom::point_in_parent_entity( + parent_cell_type, admissible_parent_entity, current, options.tolerance)) + { + set_failure(report, FailureReason::current_point_outside_parent_entity); + return report; + } + + const T fraction = desired_arclength_fractions[static_cast(node_index)]; + if (fraction < -options.tolerance || fraction > T(1) + options.tolerance) + { + set_failure(report, FailureReason::target_arclength_out_of_range); + return report; + } + + report.metrics.desired_arclength = fraction * report.metrics.curve_length; + const auto actual = closest_arclength_on_polyline( + provisional_curve_points, + point_dim, + current, + options.tolerance); + if (!actual.valid) + { + set_failure(report, FailureReason::invalid_curve); + return report; + } + report.metrics.actual_arclength = actual.arclength; + report.metrics.initial_arclength_error = + std::fabs(actual.arclength - report.metrics.desired_arclength); + + report.corrected_edge_points.assign( + projected_edge_points.begin(), projected_edge_points.end()); + report.corrected_node.assign(current.begin(), current.end()); + + const T drift_tol = arclength_tolerance( + report.metrics.curve_length, + options.max_absolute_arclength_drift, + options.max_relative_arclength_drift); + if (report.metrics.initial_arclength_error <= drift_tol) + { + validate_candidate( + report, + parent_cell_type, + admissible_parent_entity, + provisional_curve_points, + point_dim, + node_index, + report.metrics.desired_arclength, + phi, + options); + return report; + } + + const auto target = point_at_arclength( + provisional_curve_points, + point_dim, + report.metrics.desired_arclength, + options.tolerance); + if (!target.valid) + { + set_failure(report, FailureReason::target_arclength_out_of_range); + return report; + } + report.shifted_seed = target.point; + + auto correction = correct_seed_to_zero( + parent_cell_type, + admissible_parent_entity, + vector_span(report.shifted_seed), + phi, + grad, + options); + report.correction_iterations = correction.iterations; + report.metrics.phi_residual = correction.residual; + if (!correction.accepted) + { + set_failure(report, correction.failure_reason); + return report; + } + + report.corrected = true; + report.corrected_node = std::move(correction.point); + auto corrected_span = std::span( + report.corrected_edge_points.data(), + report.corrected_edge_points.size()); + const auto node = mutable_point_span( + corrected_span, + point_dim, + static_cast(node_index)); + for (int d = 0; d < point_dim; ++d) + node[static_cast(d)] = + report.corrected_node[static_cast(d)]; + + validate_candidate( + report, + parent_cell_type, + admissible_parent_entity, + provisional_curve_points, + point_dim, + node_index, + report.metrics.desired_arclength, + phi, + options); + return report; +} + +} // namespace cutcells::tangent_shift From 0d12ccb47ee0eeb6ef3c6a4373c0066ef4776eff Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Tue, 5 May 2026 00:06:17 +0200 Subject: [PATCH 19/23] found good refinement rule to improve surface jacobian, triangulated surface still a lot of refinements --- cpp/src/adapt_cell.cpp | 144 +- cpp/src/cell_certification.cpp | 1154 ++++++++++++++++- cpp/src/cell_certification.h | 13 +- cpp/src/curving.cpp | 514 +++++++- cpp/src/refine_cell.cpp | 217 +++- cpp/src/refine_cell.h | 4 +- python/cutcells/wrapper.cpp | 118 +- python/tests/test_certification_refinement.py | 77 ++ .../tests/test_geometric_quantity_header.py | 114 ++ python/tests/test_graph_criteria_header.py | 217 ++++ python/tests/test_tangent_shift_header.py | 207 +++ 11 files changed, 2731 insertions(+), 48 deletions(-) create mode 100644 python/tests/test_geometric_quantity_header.py create mode 100644 python/tests/test_graph_criteria_header.py create mode 100644 python/tests/test_tangent_shift_header.py diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp index c637d9f..1f2cba6 100644 --- a/cpp/src/adapt_cell.cpp +++ b/cpp/src/adapt_cell.cpp @@ -18,6 +18,51 @@ namespace cutcells namespace { +template +void clear_entity_host_provenance(AdaptCell& ac, int dim) +{ + ac.entity_host_cell_id[dim].clear(); + ac.entity_host_cell_type[dim].clear(); + ac.entity_host_face_id[dim].clear(); + ac.entity_source_level_set[dim].clear(); + ac.entity_host_cell_vertices[dim].offsets.clear(); + ac.entity_host_cell_vertices[dim].indices.clear(); + ac.entity_host_cell_vertices[dim].offsets.push_back(std::int32_t(0)); +} + +template +std::span host_vertices_for_entity(const AdaptCell& ac, + int dim, + int entity_id) +{ + if (dim >= 0 && dim < AdaptCell::max_dim + && entity_id >= 0 + && entity_id < ac.entity_host_cell_vertices[dim].size()) + { + return ac.entity_host_cell_vertices[dim][static_cast(entity_id)]; + } + return {}; +} + +template +void append_entity_host_provenance(AdaptCell& ac, + int dim, + int host_cell_id, + cell::type host_cell_type, + int host_face_id, + int source_level_set, + std::span host_vertices) +{ + ac.entity_host_cell_id[dim].push_back(static_cast(host_cell_id)); + ac.entity_host_cell_type[dim].push_back(host_cell_type); + ac.entity_host_face_id[dim].push_back(static_cast(host_face_id)); + ac.entity_source_level_set[dim].push_back(static_cast(source_level_set)); + for (const auto v : host_vertices) + ac.entity_host_cell_vertices[dim].indices.push_back(v); + ac.entity_host_cell_vertices[dim].offsets.push_back( + static_cast(ac.entity_host_cell_vertices[dim].indices.size())); +} + template T squared_distance_to_segment(std::span p, std::span a, @@ -146,7 +191,7 @@ infer_zero_entity_parent_host(const AdaptCell& ac, } // namespace - template +template void build_faces(AdaptCell& ac) { if (ac.tdim < 3) @@ -157,6 +202,7 @@ void build_faces(AdaptCell& ac) ac.entity_to_vertex[2].offsets.clear(); ac.entity_to_vertex[2].indices.clear(); ac.entity_to_vertex[2].offsets.push_back(std::int32_t(0)); + clear_entity_host_provenance(ac, 2); // Invalidate face cert tag storage: face count is about to change and // faces may be reordered, so all old tags are stale. @@ -197,6 +243,27 @@ void build_faces(AdaptCell& ac) ac.entity_to_vertex[2].indices.push_back(global_fv[static_cast(j)]); ac.entity_to_vertex[2].offsets.push_back( static_cast(ac.entity_to_vertex[2].indices.size())); + + const int host_cell_id = + (c < static_cast(ac.cell_source_cell_id.size())) + ? ac.cell_source_cell_id[static_cast(c)] + : c; + const cell::type host_cell_type = + (ac.tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_host_cell_type[ac.tdim].size())) + ? ac.entity_host_cell_type[ac.tdim][static_cast(c)] + : ctype; + const int source_level_set = + (ac.tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_source_level_set[ac.tdim].size())) + ? ac.entity_source_level_set[ac.tdim][static_cast(c)] + : -1; + const auto host_vertices = + host_vertices_for_entity(ac, ac.tdim, c); + append_entity_host_provenance( + ac, 2, host_cell_id, host_cell_type, -1, source_level_set, + host_vertices.empty() ? std::span(cell_verts) + : host_vertices); } } } @@ -216,6 +283,7 @@ void build_edges(AdaptCell& ac) ac.entity_to_vertex[1].offsets.clear(); ac.entity_to_vertex[1].indices.clear(); ac.entity_to_vertex[1].offsets.push_back(std::int32_t(0)); + clear_entity_host_provenance(ac, 1); // Track unique edges as (min_v, max_v) → edge_id. std::map, int> edge_map; @@ -241,6 +309,27 @@ void build_edges(AdaptCell& ac) ac.entity_to_vertex[1].indices.push_back(lv1); ac.entity_to_vertex[1].offsets.push_back( static_cast(ac.entity_to_vertex[1].indices.size())); + + const int host_cell_id = + (c < static_cast(ac.cell_source_cell_id.size())) + ? ac.cell_source_cell_id[static_cast(c)] + : c; + const cell::type host_cell_type = + (tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_host_cell_type[tdim].size())) + ? ac.entity_host_cell_type[tdim][static_cast(c)] + : ctype; + const int source_level_set = + (tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_source_level_set[tdim].size())) + ? ac.entity_source_level_set[tdim][static_cast(c)] + : -1; + const auto host_vertices = + host_vertices_for_entity(ac, tdim, c); + append_entity_host_provenance( + ac, 1, host_cell_id, host_cell_type, -1, source_level_set, + host_vertices.empty() ? std::span(cell_verts) + : host_vertices); } } } @@ -299,6 +388,22 @@ AdaptCell make_adapt_cell(const MeshView& mesh, I cell_id) for (int v = 0; v < nv; ++v) ac.entity_to_vertex[tdim].indices[static_cast(v)] = std::int32_t(v); + + ac.cell_source_cell_id.assign(1, std::int32_t(0)); + ac.cell_refinement_generation.assign(1, std::int32_t(0)); + ac.cell_refinement_reason.assign(1, CellRefinementReason::none); + ac.cell_host_parent_cell_id.assign(1, ac.parent_cell_id); + clear_entity_host_provenance(ac, tdim); + append_entity_host_provenance( + ac, + tdim, + 0, + ctype, + -1, + -1, + std::span( + ac.entity_to_vertex[tdim].indices.data(), + ac.entity_to_vertex[tdim].indices.size())); // Create edges and faces (if tdim=3) in the entity pools build_edges(ac); @@ -403,6 +508,13 @@ void rebuild_zero_entity_inventory(AdaptCell& ac) ac.zero_entity_is_owned.clear(); ac.zero_entity_parent_dim.clear(); ac.zero_entity_parent_id.clear(); + ac.zero_entity_host_cell_id.clear(); + ac.zero_entity_host_cell_type.clear(); + ac.zero_entity_host_face_id.clear(); + ac.zero_entity_source_level_set.clear(); + ac.zero_entity_host_cell_vertices.offsets.clear(); + ac.zero_entity_host_cell_vertices.indices.clear(); + ac.zero_entity_host_cell_vertices.offsets.push_back(std::int32_t(0)); const int n_vertices = ac.n_vertices(); for (int v = 0; v < n_vertices; ++v) @@ -421,6 +533,12 @@ void rebuild_zero_entity_inventory(AdaptCell& ac) ac.zero_entity_parent_id.push_back( ac.vertex_parent_id.empty() ? std::int32_t(-1) : ac.vertex_parent_id[static_cast(v)]); + ac.zero_entity_host_cell_id.push_back(std::int32_t(-1)); + ac.zero_entity_host_cell_type.push_back(cell::type::point); + ac.zero_entity_host_face_id.push_back(std::int32_t(-1)); + ac.zero_entity_source_level_set.push_back(std::int32_t(-1)); + ac.zero_entity_host_cell_vertices.offsets.push_back( + static_cast(ac.zero_entity_host_cell_vertices.indices.size())); } for (int dim = 1; dim < ac.tdim; ++dim) @@ -449,6 +567,30 @@ void rebuild_zero_entity_inventory(AdaptCell& ac) const auto host = infer_zero_entity_parent_host(ac, verts); ac.zero_entity_parent_dim.push_back(host.first); ac.zero_entity_parent_id.push_back(host.second); + if (e < static_cast(ac.entity_host_cell_id[dim].size())) + { + ac.zero_entity_host_cell_id.push_back( + ac.entity_host_cell_id[dim][static_cast(e)]); + ac.zero_entity_host_cell_type.push_back( + ac.entity_host_cell_type[dim][static_cast(e)]); + ac.zero_entity_host_face_id.push_back( + ac.entity_host_face_id[dim][static_cast(e)]); + ac.zero_entity_source_level_set.push_back( + ac.entity_source_level_set[dim][static_cast(e)]); + const auto host_vertices = + ac.entity_host_cell_vertices[dim][static_cast(e)]; + for (const auto hv : host_vertices) + ac.zero_entity_host_cell_vertices.indices.push_back(hv); + } + else + { + ac.zero_entity_host_cell_id.push_back(std::int32_t(-1)); + ac.zero_entity_host_cell_type.push_back(cell::type::point); + ac.zero_entity_host_face_id.push_back(std::int32_t(-1)); + ac.zero_entity_source_level_set.push_back(std::int32_t(-1)); + } + ac.zero_entity_host_cell_vertices.offsets.push_back( + static_cast(ac.zero_entity_host_cell_vertices.indices.size())); } } diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index 7f8ba04..c9a4289 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -538,6 +538,8 @@ void append_ready_cut_part_cells( std::vector& new_types, EntityAdjacency& new_cells, std::vector& old_cell_ids_for_new_cells, + std::vector& source_cell_ids_for_new_cells, + std::vector& refinement_reasons_for_new_cells, std::vector& explicit_current_ls_tags, std::map& token_to_vertex, T zero_tol, @@ -688,6 +690,8 @@ void append_ready_cut_part_cells( part._types[static_cast(pc)], std::span(mapped)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(old_cell_id); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::cut_level_set); explicit_current_ls_tags.push_back(side_tag); } } @@ -1456,6 +1460,19 @@ void mark_graph_refinement_request(ReadyCellGraphDiagnostics& diagnostics, } } +template +void mark_graph_refinement_point(ReadyCellGraphDiagnostics& diagnostics, + std::span point) +{ + if (!diagnostics.first_requested_refinement_point.empty() + || point.empty()) + { + return; + } + diagnostics.first_requested_refinement_point.assign( + point.begin(), point.end()); +} + template void observe_graph_direction(ReadyCellGraphDiagnostics& diagnostics, const graph_criteria::DirectionReport& report) @@ -2341,6 +2358,230 @@ bool build_face_host_frame(const std::vector& face_vertices, return h > tol; } +template +std::vector zero_entity_host_cell_vertex_ids(const AdaptCell& ac, + int local_zero_entity_id) +{ + if (local_zero_entity_id < 0 + || local_zero_entity_id >= ac.zero_entity_host_cell_vertices.size()) + { + return {}; + } + const auto vertices = ac.zero_entity_host_cell_vertices[ + static_cast(local_zero_entity_id)]; + std::vector out; + out.reserve(vertices.size()); + for (const auto v : vertices) + out.push_back(static_cast(v)); + return out; +} + +template +bool point_in_host_triangle(const AdaptCell& ac, + std::span triangle_vertices, + std::span point, + T tol) +{ + if (ac.tdim != 3 || triangle_vertices.size() != 3 || point.size() != 3) + return false; + + const auto a = point_span(ac.vertex_coords, triangle_vertices[0], 3); + const auto b = point_span(ac.vertex_coords, triangle_vertices[1], 3); + const auto c = point_span(ac.vertex_coords, triangle_vertices[2], 3); + std::array v0 = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; + std::array v1 = {c[0] - a[0], c[1] - a[1], c[2] - a[2]}; + std::array v2 = {point[0] - a[0], point[1] - a[1], point[2] - a[2]}; + const auto normal = geom::cross( + std::span(v0.data(), 3), + std::span(v1.data(), 3)); + const T area2 = geom::norm( + std::span(normal.data(), normal.size())); + if (area2 <= tol) + return false; + + const T plane = + std::fabs(geom::dot( + std::span(v2.data(), 3), + std::span(normal.data(), normal.size()))) / area2; + if (plane > tol) + return false; + + const T d00 = geom::dot( + std::span(v0.data(), 3), + std::span(v0.data(), 3)); + const T d01 = geom::dot( + std::span(v0.data(), 3), + std::span(v1.data(), 3)); + const T d11 = geom::dot( + std::span(v1.data(), 3), + std::span(v1.data(), 3)); + const T d20 = geom::dot( + std::span(v2.data(), 3), + std::span(v0.data(), 3)); + const T d21 = geom::dot( + std::span(v2.data(), 3), + std::span(v1.data(), 3)); + const T denom = d00 * d11 - d01 * d01; + if (std::fabs(denom) <= tol * tol) + return false; + + const T v = (d11 * d20 - d01 * d21) / denom; + const T w = (d00 * d21 - d01 * d20) / denom; + const T u = T(1) - v - w; + return u >= -tol && v >= -tol && w >= -tol + && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; +} + +template +std::vector host_boundary_face_normal_for_zero_face_edge( + const AdaptCell& ac, + int zero_face_id, + std::span edge_a, + std::span edge_b, + T tol, + int* host_face_id = nullptr) +{ + if (zero_face_id < 0 || zero_face_id >= ac.n_zero_entities() + || ac.tdim != 3 || edge_a.size() != 3 || edge_b.size() != 3) + { + return {}; + } + if (zero_face_id >= static_cast(ac.zero_entity_host_cell_type.size())) + return {}; + + const auto host_vertices = + zero_entity_host_cell_vertex_ids(ac, zero_face_id); + if (host_vertices.empty()) + return {}; + + const cell::type host_type = + ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; + for (int f = 0; f < cell::num_faces(host_type); ++f) + { + const auto local_face = cell::face_vertices(host_type, f); + if (local_face.size() != 3) + continue; + std::array face_vertices = { + host_vertices[static_cast(local_face[0])], + host_vertices[static_cast(local_face[1])], + host_vertices[static_cast(local_face[2])] + }; + if (!point_in_host_triangle( + ac, + std::span(face_vertices.data(), face_vertices.size()), + edge_a, + tol) + || !point_in_host_triangle( + ac, + std::span(face_vertices.data(), face_vertices.size()), + edge_b, + tol)) + { + continue; + } + + const auto normal = geom::face_normal( + point_span(ac.vertex_coords, face_vertices[0], 3), + point_span(ac.vertex_coords, face_vertices[1], 3), + point_span(ac.vertex_coords, face_vertices[2], 3), + true, + tol); + if (!normal.degenerate()) + { + if (host_face_id != nullptr) + *host_face_id = f; + return normal.value; + } + } + return {}; +} + +template +int find_zero_edge_entity_by_vertices(const AdaptCell& ac, + int vertex_a, + int vertex_b, + std::uint64_t zero_mask) +{ + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (ac.zero_entity_dim[static_cast(z)] != 1) + continue; + if ((ac.zero_entity_zero_mask[static_cast(z)] & zero_mask) + != zero_mask) + { + continue; + } + + const int edge_id = ac.zero_entity_id[static_cast(z)]; + const auto edge_vertices = + ac.entity_to_vertex[1][static_cast(edge_id)]; + if (edge_vertices.size() != 2) + continue; + const int a = static_cast(edge_vertices[0]); + const int b = static_cast(edge_vertices[1]); + if ((a == vertex_a && b == vertex_b) + || (a == vertex_b && b == vertex_a)) + { + return z; + } + } + return -1; +} + +template +void override_zero_edge_host_from_zero_face(AdaptCell& ac, + int zero_edge_id, + int zero_face_id, + int host_face_id) +{ + if (zero_edge_id < 0 || zero_face_id < 0 + || zero_edge_id >= ac.n_zero_entities() + || zero_face_id >= ac.n_zero_entities()) + { + return; + } + if (zero_edge_id >= static_cast(ac.zero_entity_host_cell_id.size()) + || zero_face_id >= static_cast(ac.zero_entity_host_cell_id.size())) + { + return; + } + + const auto face_host_vertices = + zero_entity_host_cell_vertex_ids(ac, zero_face_id); + if (face_host_vertices.empty()) + return; + + ac.zero_entity_host_cell_id[static_cast(zero_edge_id)] = + ac.zero_entity_host_cell_id[static_cast(zero_face_id)]; + ac.zero_entity_host_cell_type[static_cast(zero_edge_id)] = + ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; + ac.zero_entity_host_face_id[static_cast(zero_edge_id)] = + static_cast(host_face_id); + ac.zero_entity_source_level_set[static_cast(zero_edge_id)] = + ac.zero_entity_source_level_set[static_cast(zero_face_id)]; + + EntityAdjacency updated; + updated.offsets.push_back(std::int32_t(0)); + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (z == zero_edge_id) + { + for (const int v : face_host_vertices) + updated.indices.push_back(static_cast(v)); + } + else if (z < ac.zero_entity_host_cell_vertices.size()) + { + const auto old = ac.zero_entity_host_cell_vertices[ + static_cast(z)]; + for (const auto v : old) + updated.indices.push_back(v); + } + updated.offsets.push_back( + static_cast(updated.indices.size())); + } + ac.zero_entity_host_cell_vertices = std::move(updated); +} + template bool check_zero_edge_graph(const AdaptCell& adapt_cell, int local_zero_entity_id, @@ -2462,6 +2703,384 @@ bool check_zero_edge_graph(const AdaptCell& adapt_cell, return true; } +template +std::vector graph_push_forward_vector(const std::vector& jacobian, + int gdim, + int tdim, + std::span vector_ref); + +template +bool graph_physical_level_set_gradient( + const LevelSetCell& ls_cell, + std::span point_ref, + std::vector& gradient_phys, + T tol) +{ + if (static_cast(point_ref.size()) != ls_cell.tdim) + return false; + + std::vector gradient_ref(static_cast(ls_cell.tdim), T(0)); + curving::reference_level_set_gradient( + ls_cell, + point_ref, + std::span(gradient_ref.data(), gradient_ref.size())); + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return false; + + if (ls_cell.gdim == ls_cell.tdim) + { + std::vector jt( + static_cast(ls_cell.tdim * ls_cell.tdim), T(0)); + for (int a = 0; a < ls_cell.tdim; ++a) + { + for (int r = 0; r < ls_cell.gdim; ++r) + { + jt[static_cast(a * ls_cell.tdim + r)] = + jacobian[static_cast(r * ls_cell.tdim + a)]; + } + } + std::vector rhs(gradient_ref.begin(), gradient_ref.end()); + return solve_graph_dense_small( + std::move(jt), std::move(rhs), ls_cell.tdim, gradient_phys, tol); + } + + gradient_phys.assign(static_cast(ls_cell.gdim), T(0)); + for (int r = 0; r < ls_cell.gdim; ++r) + { + for (int a = 0; a < ls_cell.tdim; ++a) + { + gradient_phys[static_cast(r)] += + jacobian[static_cast(r * ls_cell.tdim + a)] + * gradient_ref[static_cast(a)]; + } + } + return geom::norm( + std::span(gradient_phys.data(), gradient_phys.size())) + > tol; +} + +template +int best_orthogonal_surface_edge_refinement_id( + const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span ordered_vertices, + const ReadyCellGraphOptions& graph_options, + std::vector* selected_midpoint_ref = nullptr) +{ + const int zdim = + adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) + return -1; + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return -1; + + const cell::type zero_face_type = + adapt_cell.entity_types[2][static_cast(zid)]; + const auto zero_face_edges = cell::edges(zero_face_type); + const T tol = graph_options.criteria.tolerance; + + int selected_edge_id = -1; + T selected_abs_cosine = std::numeric_limits::infinity(); + T selected_length = T(0); + for (const auto& zero_face_edge : zero_face_edges) + { + const int ia = zero_face_edge[0]; + const int ib = zero_face_edge[1]; + if (ia < 0 || ib < 0 + || ia >= static_cast(ordered_vertices.size()) + || ib >= static_cast(ordered_vertices.size())) + { + continue; + } + + const int a = ordered_vertices[static_cast(ia)]; + const int b = ordered_vertices[static_cast(ib)]; + const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); + const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); + + std::vector midpoint(static_cast(ls_cell.tdim), T(0)); + std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); + for (int d = 0; d < ls_cell.tdim; ++d) + { + midpoint[static_cast(d)] = T(0.5) * ( + pa[static_cast(d)] + + pb[static_cast(d)]); + tangent_ref[static_cast(d)] = + pb[static_cast(d)] + - pa[static_cast(d)]; + } + + std::vector tangent_phys(static_cast(ls_cell.gdim), T(0)); + for (int r = 0; r < ls_cell.gdim; ++r) + { + for (int d = 0; d < ls_cell.tdim; ++d) + { + tangent_phys[static_cast(r)] += + jacobian[static_cast(r * ls_cell.tdim + d)] + * tangent_ref[static_cast(d)]; + } + } + + std::vector gradient_phys; + if (!graph_physical_level_set_gradient( + ls_cell, + std::span(midpoint.data(), midpoint.size()), + gradient_phys, + tol)) + { + continue; + } + + const T tangent_norm = geom::norm( + std::span(tangent_phys.data(), tangent_phys.size())); + const T gradient_norm = geom::norm( + std::span(gradient_phys.data(), gradient_phys.size())); + if (tangent_norm <= tol || gradient_norm <= tol) + continue; + + const T abs_cosine = std::fabs(geom::dot( + std::span(gradient_phys.data(), gradient_phys.size()), + std::span(tangent_phys.data(), tangent_phys.size())) + / (gradient_norm * tangent_norm)); + const int edge_id = + graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); + if (edge_id < 0) + continue; + + if (selected_edge_id < 0 + || abs_cosine < selected_abs_cosine + || (std::fabs(abs_cosine - selected_abs_cosine) <= tol + && tangent_norm > selected_length)) + { + selected_edge_id = edge_id; + selected_abs_cosine = abs_cosine; + selected_length = tangent_norm; + if (selected_midpoint_ref != nullptr) + *selected_midpoint_ref = midpoint; + } + } + + return selected_edge_id; +} + +template +int best_midpoint_residual_surface_edge_refinement_id( + const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span ordered_vertices, + const ReadyCellGraphOptions& graph_options, + std::vector* selected_midpoint_ref = nullptr) +{ + const int zdim = + adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) + return -1; + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return -1; + + const cell::type zero_face_type = + adapt_cell.entity_types[2][static_cast(zid)]; + const auto zero_face_edges = cell::edges(zero_face_type); + const T tol = graph_options.criteria.tolerance; + + int selected_edge_id = -1; + T selected_score = -std::numeric_limits::infinity(); + T selected_length = T(0); + for (const auto& zero_face_edge : zero_face_edges) + { + const int ia = zero_face_edge[0]; + const int ib = zero_face_edge[1]; + if (ia < 0 || ib < 0 + || ia >= static_cast(ordered_vertices.size()) + || ib >= static_cast(ordered_vertices.size())) + { + continue; + } + + const int a = ordered_vertices[static_cast(ia)]; + const int b = ordered_vertices[static_cast(ib)]; + const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); + const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); + + std::vector midpoint(static_cast(ls_cell.tdim), T(0)); + std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); + for (int d = 0; d < ls_cell.tdim; ++d) + { + midpoint[static_cast(d)] = T(0.5) * ( + pa[static_cast(d)] + + pb[static_cast(d)]); + tangent_ref[static_cast(d)] = + pb[static_cast(d)] + - pa[static_cast(d)]; + } + + const auto tangent_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(tangent_ref.data(), tangent_ref.size())); + const T tangent_norm = geom::norm( + std::span(tangent_phys.data(), tangent_phys.size())); + if (tangent_norm <= tol) + continue; + + std::vector gradient_phys; + T gradient_norm = T(1); + if (graph_physical_level_set_gradient( + ls_cell, + std::span(midpoint.data(), midpoint.size()), + gradient_phys, + tol)) + { + gradient_norm = std::max( + geom::norm( + std::span( + gradient_phys.data(), gradient_phys.size())), + tol); + } + + const T score = std::fabs(ls_cell.value( + std::span( + midpoint.data(), midpoint.size()))) + / gradient_norm; + const int edge_id = + graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); + if (edge_id < 0) + continue; + + if (selected_edge_id < 0 + || score > selected_score + || (std::fabs(score - selected_score) <= tol + && tangent_norm > selected_length)) + { + selected_edge_id = edge_id; + selected_score = score; + selected_length = tangent_norm; + if (selected_midpoint_ref != nullptr) + *selected_midpoint_ref = midpoint; + } + } + + return selected_edge_id; +} + +template +int best_normal_variation_surface_edge_refinement_id( + const AdaptCell& adapt_cell, + int local_zero_entity_id, + const LevelSetCell& ls_cell, + std::span ordered_vertices, + const ReadyCellGraphOptions& graph_options, + std::vector* selected_midpoint_ref = nullptr) +{ + const int zdim = + adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) + return -1; + + std::vector jacobian; + if (!graph_affine_jacobian(ls_cell, jacobian)) + return -1; + + const cell::type zero_face_type = + adapt_cell.entity_types[2][static_cast(zid)]; + const auto zero_face_edges = cell::edges(zero_face_type); + const T tol = graph_options.criteria.tolerance; + + int selected_edge_id = -1; + T selected_score = -std::numeric_limits::infinity(); + T selected_length = T(0); + for (const auto& zero_face_edge : zero_face_edges) + { + const int ia = zero_face_edge[0]; + const int ib = zero_face_edge[1]; + if (ia < 0 || ib < 0 + || ia >= static_cast(ordered_vertices.size()) + || ib >= static_cast(ordered_vertices.size())) + { + continue; + } + + const int a = ordered_vertices[static_cast(ia)]; + const int b = ordered_vertices[static_cast(ib)]; + const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); + const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); + + std::vector midpoint(static_cast(ls_cell.tdim), T(0)); + std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); + for (int d = 0; d < ls_cell.tdim; ++d) + { + midpoint[static_cast(d)] = T(0.5) * ( + pa[static_cast(d)] + + pb[static_cast(d)]); + tangent_ref[static_cast(d)] = + pb[static_cast(d)] + - pa[static_cast(d)]; + } + + const auto tangent_phys = graph_push_forward_vector( + jacobian, ls_cell.gdim, ls_cell.tdim, + std::span(tangent_ref.data(), tangent_ref.size())); + const T tangent_norm = geom::norm( + std::span(tangent_phys.data(), tangent_phys.size())); + if (tangent_norm <= tol) + continue; + + std::vector ga; + std::vector gb; + if (!graph_physical_level_set_gradient( + ls_cell, pa, ga, tol) + || !graph_physical_level_set_gradient( + ls_cell, pb, gb, tol)) + { + continue; + } + const T ga_norm = geom::norm( + std::span(ga.data(), ga.size())); + const T gb_norm = geom::norm( + std::span(gb.data(), gb.size())); + if (ga_norm <= tol || gb_norm <= tol) + continue; + + const T abs_cosine = std::fabs(geom::dot( + std::span(ga.data(), ga.size()), + std::span(gb.data(), gb.size())) + / (ga_norm * gb_norm)); + const T score = (T(1) - std::clamp(abs_cosine, T(0), T(1))) + * tangent_norm; + const int edge_id = + graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); + if (edge_id < 0) + continue; + + if (selected_edge_id < 0 + || score > selected_score + || (std::fabs(score - selected_score) <= tol + && tangent_norm > selected_length)) + { + selected_edge_id = edge_id; + selected_score = score; + selected_length = tangent_norm; + if (selected_midpoint_ref != nullptr) + *selected_midpoint_ref = midpoint; + } + } + + return selected_edge_id; +} + template std::vector graph_push_forward_vector(const std::vector& jacobian, int gdim, @@ -3181,13 +3800,41 @@ bool check_zero_face_graph(const AdaptCell& adapt_cell, } const int nverts = static_cast(ordered_vertices.size()); + const int zdim = + adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; + const int zid = + adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; + if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + const cell::type zero_face_type = + adapt_cell.entity_types[2][static_cast(zid)]; + const auto zero_face_edges = cell::edges(zero_face_type); + const geom::ParentEntity cell_interior{ls_cell.tdim, -1}; int longest_face_edge_id = -1; T longest_face_edge_length = T(0); - for (int i = 0; i < nverts; ++i) + int face_interior_node_index = 1; + for (const auto& zero_face_edge : zero_face_edges) { - const int a = ordered_vertices[static_cast(i)]; - const int b = ordered_vertices[ - static_cast((i + 1) % nverts)]; + const int ia = zero_face_edge[0]; + const int ib = zero_face_edge[1]; + if (ia < 0 || ib < 0 || ia >= nverts || ib >= nverts) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_input); + return false; + } + + const int a = ordered_vertices[static_cast(ia)]; + const int b = ordered_vertices[static_cast(ib)]; const auto pa = point_span(vertex_coords, a, ls_cell.tdim); const auto pb = point_span(vertex_coords, b, ls_cell.tdim); const auto edge_delta = geom::subtract(pb, pa); @@ -3201,13 +3848,91 @@ bool check_zero_face_graph(const AdaptCell& adapt_cell, longest_face_edge_id = adapt_edge_id; } - if (!check_zero_edge_graph( + const std::uint64_t face_zero_mask = + adapt_cell.zero_entity_zero_mask[ + static_cast(local_zero_entity_id)]; + const int zero_edge_id = + find_zero_edge_entity_by_vertices( + adapt_cell, a, b, face_zero_mask); + if (zero_edge_id < 0) + { + mark_graph_failure( + diagnostics, + source_cell_id, + graph_criteria::FailureReason::invalid_host_frame); + return false; + } + + int host_face_id = -1; + auto boundary_host_normal = + host_boundary_face_normal_for_zero_face_edge( adapt_cell, local_zero_entity_id, + pa, + pb, + graph_options.criteria.tolerance, + &host_face_id); + if (boundary_host_normal.empty() || host_face_id < 0) + { + // This is an interior zero edge, for example the artificial + // diagonal introduced by triangulating a zero quadrilateral. It + // still curves as a zero edge, but its admissible host is the + // uncut subcell volume and graph checks use face-interior rules. + AdaptCell edge_context = adapt_cell; + override_zero_edge_host_from_zero_face( + edge_context, + zero_edge_id, + local_zero_entity_id, + -1); + + const int order = std::max(graph_options.geometry_order, 1); + for (int k = 1; k < order; ++k) + { + const T s = T(k) / T(order); + std::vector seed(static_cast(ls_cell.tdim), T(0)); + for (int d = 0; d < ls_cell.tdim; ++d) + { + seed[static_cast(d)] = + (T(1) - s) * pa[static_cast(d)] + + s * pb[static_cast(d)]; + } + + std::vector corrected; + if (!project_graph_point( + edge_context, + zero_edge_id, + ls_cell, + cell_interior, + host, + std::span(seed.data(), seed.size()), + GraphNodeKind::face_interior, + face_interior_node_index++, + adapt_edge_id, + graph_options, + diagnostics, + source_cell_id, + corrected)) + { + return false; + } + } + continue; + } + + AdaptCell edge_context = adapt_cell; + override_zero_edge_host_from_zero_face( + edge_context, + zero_edge_id, + local_zero_entity_id, + host_face_id); + if (!check_zero_edge_graph( + edge_context, + zero_edge_id, ls_cell, pa, pb, - std::span(host.normal.data(), host.normal.size()), + std::span( + boundary_host_normal.data(), boundary_host_normal.size()), true, adapt_edge_id, graph_options, @@ -3229,7 +3954,50 @@ bool check_zero_face_graph(const AdaptCell& adapt_cell, value /= static_cast(nverts); std::vector corrected_face_center; - const geom::ParentEntity cell_interior{ls_cell.tdim, -1}; + int face_center_refinement_edge_id = longest_face_edge_id; + std::vector face_center_refinement_point; + if (graph_options.refinement_mode + == GraphRefinementMode::green_orthogonal_surface_edge) + { + const int orthogonal_edge_id = + best_orthogonal_surface_edge_refinement_id( + adapt_cell, + local_zero_entity_id, + ls_cell, + ordered_vertices, + graph_options, + &face_center_refinement_point); + if (orthogonal_edge_id >= 0) + face_center_refinement_edge_id = orthogonal_edge_id; + } + else if (graph_options.refinement_mode + == GraphRefinementMode::green_midpoint_residual) + { + const int residual_edge_id = + best_midpoint_residual_surface_edge_refinement_id( + adapt_cell, + local_zero_entity_id, + ls_cell, + ordered_vertices, + graph_options, + &face_center_refinement_point); + if (residual_edge_id >= 0) + face_center_refinement_edge_id = residual_edge_id; + } + else if (graph_options.refinement_mode + == GraphRefinementMode::green_normal_variation) + { + const int normal_variation_edge_id = + best_normal_variation_surface_edge_refinement_id( + adapt_cell, + local_zero_entity_id, + ls_cell, + ordered_vertices, + graph_options, + &face_center_refinement_point); + if (normal_variation_edge_id >= 0) + face_center_refinement_edge_id = normal_variation_edge_id; + } if (!project_graph_point( adapt_cell, local_zero_entity_id, @@ -3239,12 +4007,17 @@ bool check_zero_face_graph(const AdaptCell& adapt_cell, std::span(face_center.data(), face_center.size()), GraphNodeKind::face_interior, 0, - longest_face_edge_id, + face_center_refinement_edge_id, graph_options, diagnostics, source_cell_id, corrected_face_center)) { + mark_graph_refinement_point( + diagnostics, + std::span( + face_center_refinement_point.data(), + face_center_refinement_point.size())); return false; } @@ -3371,6 +4144,10 @@ ZeroEntityGraphDiagnostics make_zero_entity_graph_record( int level_set_id, int dimension, std::uint64_t zero_mask, + int host_cell_id, + cell::type host_cell_type, + int host_face_id, + int source_level_set, const ReadyCellGraphDiagnostics& entity_diag) { ZeroEntityGraphDiagnostics record; @@ -3378,6 +4155,10 @@ ZeroEntityGraphDiagnostics make_zero_entity_graph_record( record.level_set_id = level_set_id; record.dimension = dimension; record.zero_mask = zero_mask; + record.host_cell_id = host_cell_id; + record.host_cell_type = host_cell_type; + record.host_face_id = host_face_id; + record.source_level_set = source_level_set; record.accepted = entity_diag.accepted; record.checked_edges = entity_diag.checked_edges; record.checked_faces = entity_diag.checked_faces; @@ -3413,6 +4194,8 @@ ZeroEntityGraphDiagnostics make_zero_entity_graph_record( entity_diag.first_requested_refinement_entity_dim; record.requested_refinement_entity_id = entity_diag.first_requested_refinement_entity_id; + record.requested_refinement_point = + entity_diag.first_requested_refinement_point; record.nodes = entity_diag.nodes; return record; } @@ -3462,7 +4245,14 @@ void accumulate_zero_entity_graph_record(ReadyCellGraphDiagnostics& diagnosti diagnostics, record.requested_refinement_entity_dim, record.requested_refinement_entity_id); - mark_graph_failure(diagnostics, source_cell_id, record.failure_reason); + mark_graph_refinement_point( + diagnostics, + std::span( + record.requested_refinement_point.data(), + record.requested_refinement_point.size())); + const int failed_cell_id = + source_cell_id >= 0 ? source_cell_id : record.host_cell_id; + mark_graph_failure(diagnostics, failed_cell_id, record.failure_reason); } } @@ -3561,6 +4351,18 @@ void populate_committed_zero_entity_graph_diagnostics( level_set_id, zdim, adapt_cell.zero_entity_zero_mask[static_cast(z)], + z < static_cast(adapt_cell.zero_entity_host_cell_id.size()) + ? adapt_cell.zero_entity_host_cell_id[static_cast(z)] + : -1, + z < static_cast(adapt_cell.zero_entity_host_cell_type.size()) + ? adapt_cell.zero_entity_host_cell_type[static_cast(z)] + : cell::type::point, + z < static_cast(adapt_cell.zero_entity_host_face_id.size()) + ? adapt_cell.zero_entity_host_face_id[static_cast(z)] + : -1, + z < static_cast(adapt_cell.zero_entity_source_level_set.size()) + ? adapt_cell.zero_entity_source_level_set[static_cast(z)] + : -1, entity_diag); accumulate_zero_entity_graph_record(diagnostics, record, -1); diagnostics.zero_entities.push_back(std::move(record)); @@ -3699,6 +4501,8 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, new_cells.offsets.push_back(0); std::vector new_types; std::vector old_cell_ids_for_new_cells; + std::vector source_cell_ids_for_new_cells; + std::vector refinement_reasons_for_new_cells; std::vector explicit_current_ls_tags; for (int c = 0; c < n_cells; ++c) @@ -3711,6 +4515,8 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); explicit_current_ls_tags.push_back(CellCertTag::not_classified); continue; } @@ -3877,6 +4683,9 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, new_types, new_cells, cell::type::triangle, std::span(mapped)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back( + CellRefinementReason::cut_level_set); explicit_current_ls_tags.push_back(side_tag); }; @@ -3905,12 +4714,16 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 3), - new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + new_types, new_cells, old_cell_ids_for_new_cells, + source_cell_ids_for_new_cells, refinement_reasons_for_new_cells, + explicit_current_ls_tags, token_to_vertex, zero_tol, CellCertTag::negative); append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, positive_part, leaf_cell_type, old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 3), - new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + new_types, new_cells, old_cell_ids_for_new_cells, + source_cell_ids_for_new_cells, refinement_reasons_for_new_cells, + explicit_current_ls_tags, token_to_vertex, zero_tol, CellCertTag::positive); } else @@ -3929,19 +4742,37 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, negative_part, leaf_cell_type, old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 6), - new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + new_types, new_cells, old_cell_ids_for_new_cells, + source_cell_ids_for_new_cells, refinement_reasons_for_new_cells, + explicit_current_ls_tags, token_to_vertex, zero_tol, CellCertTag::negative); append_ready_cut_part_cells( adapt_cell, ls_cell, level_set_id, c, positive_part, leaf_cell_type, old_cell_vertices, std::span(old_edge_ids_by_local_edge.data(), 6), - new_types, new_cells, old_cell_ids_for_new_cells, explicit_current_ls_tags, + new_types, new_cells, old_cell_ids_for_new_cells, + source_cell_ids_for_new_cells, refinement_reasons_for_new_cells, + explicit_current_ls_tags, token_to_vertex, zero_tol, CellCertTag::positive); } } apply_topology_update_preserve_certification( adapt_cell, std::move(new_types), std::move(new_cells), - std::span(old_cell_ids_for_new_cells)); + std::span(old_cell_ids_for_new_cells), + std::span(source_cell_ids_for_new_cells), + std::span(refinement_reasons_for_new_cells)); + + for (int c = 0; c < adapt_cell.n_entities(tdim); ++c) + { + if (c < static_cast(refinement_reasons_for_new_cells.size()) + && refinement_reasons_for_new_cells[static_cast(c)] + == CellRefinementReason::cut_level_set + && c < static_cast(adapt_cell.entity_source_level_set[tdim].size())) + { + adapt_cell.entity_source_level_set[tdim][static_cast(c)] = + static_cast(level_set_id); + } + } const int new_num_cells = adapt_cell.n_entities(tdim); for (int c = 0; c < new_num_cells; ++c) @@ -4195,6 +5026,255 @@ bool refine_green_on_requested_graph_edge(AdaptCell& adapt_cell, return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); } +template +bool refine_green_on_requested_graph_edge_midpoint(AdaptCell& adapt_cell, + int level_set_id, + int edge_id) +{ + if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) + return false; + + const int n_edges = adapt_cell.n_entities(1); + if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) + adapt_cell.resize_edge_root_tags(level_set_id + 1); + if (adapt_cell.edge_green_split_has_value.size() + < static_cast((level_set_id + 1) * n_edges)) + { + adapt_cell.resize_green_split_data(level_set_id + 1); + } + + const auto idx = + static_cast(level_set_id * n_edges + edge_id); + adapt_cell.set_edge_root_tag( + level_set_id, edge_id, EdgeRootTag::multiple_roots); + adapt_cell.edge_green_split_param[idx] = T(0.5); + adapt_cell.edge_green_split_has_value[idx] = 1; + return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); +} + +template +T graph_tetra_signed_volume6(const AdaptCell& adapt_cell, + std::span tet) +{ + if (adapt_cell.tdim != 3 || tet.size() != 4) + return T(0); + const auto p0 = point_span(adapt_cell.vertex_coords, tet[0], 3); + const auto p1 = point_span(adapt_cell.vertex_coords, tet[1], 3); + const auto p2 = point_span(adapt_cell.vertex_coords, tet[2], 3); + const auto p3 = point_span(adapt_cell.vertex_coords, tet[3], 3); + std::array a = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + std::array b = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + std::array c = {p3[0] - p0[0], p3[1] - p0[1], p3[2] - p0[2]}; + const auto axb = geom::cross( + std::span(a.data(), a.size()), + std::span(b.data(), b.size())); + return geom::dot( + std::span(axb.data(), axb.size()), + std::span(c.data(), c.size())); +} + +template +bool graph_point_barycentric_in_tetra( + const AdaptCell& adapt_cell, + std::span tet_vertices, + std::span point, + std::array& bary, + T tol) +{ + if (adapt_cell.tdim != 3 || tet_vertices.size() != 4 || point.size() != 3) + return false; + + const auto p0 = point_span(adapt_cell.vertex_coords, tet_vertices[0], 3); + std::vector A(9, T(0)); + std::vector rhs(3, T(0)); + for (int col = 0; col < 3; ++col) + { + const auto pc = point_span( + adapt_cell.vertex_coords, tet_vertices[static_cast(col + 1)], 3); + for (int row = 0; row < 3; ++row) + { + A[static_cast(row * 3 + col)] = + pc[static_cast(row)] - p0[static_cast(row)]; + } + } + for (int row = 0; row < 3; ++row) + rhs[static_cast(row)] = + point[static_cast(row)] - p0[static_cast(row)]; + + std::vector x; + if (!solve_graph_dense_small(std::move(A), std::move(rhs), 3, x, tol)) + return false; + + bary[1] = x[0]; + bary[2] = x[1]; + bary[3] = x[2]; + bary[0] = T(1) - bary[1] - bary[2] - bary[3]; + + T sum = T(0); + for (const T value : bary) + { + if (value < -tol || value > T(1) + tol) + return false; + sum += value; + } + return std::fabs(sum - T(1)) <= T(16) * tol; +} + +template +bool refine_green_on_requested_graph_point(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int failed_cell_id, + std::span point, + T zero_tol) +{ + if (adapt_cell.tdim != 3 || point.size() != 3) + return false; + + const T tol = std::max(zero_tol, T(1024) * std::numeric_limits::epsilon()); + struct SplitCell + { + int cell_id = -1; + std::array bary = {}; + }; + std::vector split_cells; + + auto consider_cell = [&](int c) + { + if (c < 0 || c >= adapt_cell.n_entities(3)) + return; + if (adapt_cell.entity_types[3][static_cast(c)] + != cell::type::tetrahedron) + { + return; + } + const auto verts = + adapt_cell.entity_to_vertex[3][static_cast(c)]; + std::array bary = {}; + if (graph_point_barycentric_in_tetra( + adapt_cell, verts, point, bary, tol)) + { + split_cells.push_back({c, bary}); + } + }; + + consider_cell(failed_cell_id); + if (split_cells.empty()) + { + for (int c = 0; c < adapt_cell.n_entities(3); ++c) + consider_cell(c); + } + if (split_cells.empty()) + return false; + + for (int v = 0; v < adapt_cell.n_vertices(); ++v) + { + const auto existing = point_span(adapt_cell.vertex_coords, v, 3); + const auto delta = geom::subtract(existing, point); + if (geom::norm(std::span(delta.data(), delta.size())) <= tol) + return false; + } + + std::vector parent_param(point.begin(), point.end()); + const int new_v = append_vertex_with_parent_info( + adapt_cell, + point, + adapt_cell.tdim, + adapt_cell.parent_cell_id, + std::span(parent_param.data(), parent_param.size()), + -1); + set_vertex_sign_for_level_set( + adapt_cell, + new_v, + level_set_id, + ls_cell.value(point), + zero_tol); + + std::set split_cell_ids; + for (const auto& split : split_cells) + split_cell_ids.insert(split.cell_id); + + EntityAdjacency new_cells; + new_cells.offsets.push_back(0); + std::vector new_types; + std::vector old_cell_ids_for_new_cells; + std::vector source_cell_ids_for_new_cells; + std::vector refinement_reasons_for_new_cells; + + const std::array, 4> opposite_faces = {{ + {{1, 2, 3}}, + {{0, 2, 3}}, + {{0, 1, 3}}, + {{0, 1, 2}}, + }}; + + for (int c = 0; c < adapt_cell.n_entities(3); ++c) + { + const auto verts = + adapt_cell.entity_to_vertex[3][static_cast(c)]; + if (split_cell_ids.find(c) == split_cell_ids.end()) + { + std::vector copy(verts.begin(), verts.end()); + append_top_cell_local( + new_types, new_cells, adapt_cell.entity_types[3][static_cast(c)], + std::span(copy.data(), copy.size())); + old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); + continue; + } + + std::array old_tet = { + static_cast(verts[0]), + static_cast(verts[1]), + static_cast(verts[2]), + static_cast(verts[3])}; + const T old_volume = graph_tetra_signed_volume6( + adapt_cell, + std::span(old_tet.data(), old_tet.size())); + + for (const auto& face : opposite_faces) + { + std::array child = { + new_v, + old_tet[static_cast(face[0])], + old_tet[static_cast(face[1])], + old_tet[static_cast(face[2])]}; + T child_volume = graph_tetra_signed_volume6( + adapt_cell, + std::span(child.data(), child.size())); + if (std::fabs(child_volume) <= tol * std::max(T(1), std::fabs(old_volume))) + continue; + if (old_volume * child_volume < T(0)) + { + std::swap(child[1], child[2]); + child_volume = -child_volume; + } + (void)child_volume; + append_top_cell_local( + new_types, new_cells, cell::type::tetrahedron, + std::span(child.data(), child.size())); + old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back( + CellRefinementReason::graph_green_edge); + } + } + + apply_topology_update_preserve_certification( + adapt_cell, + std::move(new_types), + std::move(new_cells), + std::span( + old_cell_ids_for_new_cells.data(), old_cell_ids_for_new_cells.size()), + std::span( + source_cell_ids_for_new_cells.data(), source_cell_ids_for_new_cells.size()), + std::span( + refinement_reasons_for_new_cells.data(), + refinement_reasons_for_new_cells.size())); + return true; +} + template bool refine_red_on_graph_failed_cell(AdaptCell& adapt_cell, int level_set_id, @@ -4337,6 +5417,46 @@ ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( refined = refine_red_on_graph_failed_cell( adapt_cell, level_set_id, diagnostics.first_failed_cell); } + else if (graph_options.refinement_mode + == GraphRefinementMode::green_orthogonal_surface_edge + && diagnostics.first_requested_refinement_entity_dim == 1) + { + if (!diagnostics.first_requested_refinement_point.empty()) + { + refined = refine_green_on_requested_graph_point( + adapt_cell, + ls_cell, + level_set_id, + diagnostics.first_failed_cell, + std::span( + diagnostics.first_requested_refinement_point.data(), + diagnostics.first_requested_refinement_point.size()), + zero_tol); + } + if (!refined) + { + refined = refine_green_on_requested_graph_edge_midpoint( + adapt_cell, + level_set_id, + diagnostics.first_requested_refinement_entity_id); + } + } + else if ((graph_options.refinement_mode + == GraphRefinementMode::green_midpoint_residual + || graph_options.refinement_mode + == GraphRefinementMode::green_normal_variation) + && !diagnostics.first_requested_refinement_point.empty()) + { + refined = refine_green_on_requested_graph_point( + adapt_cell, + ls_cell, + level_set_id, + diagnostics.first_failed_cell, + std::span( + diagnostics.first_requested_refinement_point.data(), + diagnostics.first_requested_refinement_point.size()), + zero_tol); + } else if (diagnostics.first_requested_refinement_entity_dim == 1) { refined = refine_green_on_requested_graph_edge( @@ -4349,6 +5469,12 @@ ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( edge_max_depth); } + if (!refined && diagnostics.first_failed_cell >= 0) + { + refined = refine_red_on_graph_failed_cell( + adapt_cell, level_set_id, diagnostics.first_failed_cell); + } + if (!refined) { refined = refine_ready_cell_on_largest_midpoint_value( diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index 43a7219..cde3a82 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -28,7 +28,10 @@ enum class GraphProjectionMode : std::uint8_t enum class GraphRefinementMode : std::uint8_t { green_edge = 0, - red_failed_cell = 1 + red_failed_cell = 1, + green_orthogonal_surface_edge = 2, + green_midpoint_residual = 3, + green_normal_variation = 4 }; template @@ -40,7 +43,7 @@ struct ReadyCellGraphOptions GraphProjectionMode projection_mode = GraphProjectionMode::level_set_gradient; GraphRefinementMode refinement_mode = - GraphRefinementMode::green_edge; + GraphRefinementMode::green_midpoint_residual; graph_criteria::Options criteria = {}; T min_level_set_gradient_host_alignment = T(0.9); }; @@ -92,6 +95,10 @@ struct ZeroEntityGraphDiagnostics int level_set_id = -1; int dimension = -1; std::uint64_t zero_mask = 0; + int host_cell_id = -1; + cell::type host_cell_type = cell::type::point; + int host_face_id = -1; + int source_level_set = -1; bool accepted = true; int checked_edges = 0; int checked_faces = 0; @@ -119,6 +126,7 @@ struct ZeroEntityGraphDiagnostics int requested_refinement_entity_dim = -1; int requested_refinement_entity_id = -1; + std::vector requested_refinement_point; std::vector> nodes; }; @@ -154,6 +162,7 @@ struct ReadyCellGraphDiagnostics T first_failed_projection_root_t = std::numeric_limits::quiet_NaN(); int first_requested_refinement_entity_dim = -1; int first_requested_refinement_entity_id = -1; + std::vector first_requested_refinement_point; /// Diagnostics attached to the committed AdaptCell zero entities. These /// are populated after the linear cut is committed and before any lazy diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp index cb1ef96..f97c901 100644 --- a/cpp/src/curving.cpp +++ b/cpp/src/curving.cpp @@ -107,6 +107,31 @@ std::vector local_zero_entity_vertex_ids(const AdaptCell& ac, return out; } +template +std::vector zero_entity_host_cell_vertex_ids(const AdaptCell& ac, + int local_zero_entity_id) +{ + if (local_zero_entity_id < 0 + || local_zero_entity_id >= ac.zero_entity_host_cell_vertices.size()) + { + return {}; + } + auto verts = ac.zero_entity_host_cell_vertices[ + static_cast(local_zero_entity_id)]; + std::vector out; + out.reserve(verts.size()); + for (const auto v : verts) + out.push_back(static_cast(v)); + return out; +} + +template +bool zero_entity_has_explicit_host_cell(const AdaptCell& ac, + int local_zero_entity_id) +{ + return !zero_entity_host_cell_vertex_ids(ac, local_zero_entity_id).empty(); +} + template void append_unique_host_vertex(const AdaptCell& ac, int vertex_id, @@ -165,7 +190,7 @@ LocalHostDomain local_host_domain_for_zero_entity( int local_zero_entity_id) { LocalHostDomain host; - if (ac.tdim != 3 || local_zero_entity_id < 0 + if ((ac.tdim != 2 && ac.tdim != 3) || local_zero_entity_id < 0 || local_zero_entity_id >= ac.n_zero_entities()) { return host; @@ -178,12 +203,79 @@ LocalHostDomain local_host_domain_for_zero_entity( if (zverts.empty()) return host; - const std::uint64_t zero_mask = - ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; - const int n_cells = ac.n_entities(ac.tdim); + const auto explicit_host_vertices = + zero_entity_host_cell_vertex_ids(ac, local_zero_entity_id); + + if (ac.tdim == 2 && zdim == 1 && !explicit_host_vertices.empty()) + { + host.dimension = 2; + host.ordered_boundary = true; + for (const int vertex_id : explicit_host_vertices) + { + const auto p = vertex_ref_point(ac, vertex_id); + host.vertices.insert(host.vertices.end(), p.begin(), p.end()); + } + return host; + } + + if (zdim == 1 + && local_zero_entity_id + < static_cast(ac.zero_entity_host_face_id.size()) + && local_zero_entity_id + < static_cast(ac.zero_entity_host_cell_type.size()) + && !explicit_host_vertices.empty()) + { + const int host_face_id = + ac.zero_entity_host_face_id[static_cast(local_zero_entity_id)]; + const cell::type host_type = + ac.zero_entity_host_cell_type[static_cast(local_zero_entity_id)]; + if (host_face_id >= 0 + && host_face_id < cell::num_faces(host_type)) + { + auto face_vertices = cell::face_vertices(host_type, host_face_id); + host.dimension = 2; + host.ordered_boundary = true; + for (const auto local_v : face_vertices) + { + if (local_v < 0 + || local_v >= static_cast(explicit_host_vertices.size())) + { + host.vertices.clear(); + host.dimension = -1; + return host; + } + const int vertex_id = + explicit_host_vertices[static_cast(local_v)]; + const auto p = vertex_ref_point(ac, vertex_id); + host.vertices.insert(host.vertices.end(), p.begin(), p.end()); + } + return host; + } + } + + if (zdim == 1 && !explicit_host_vertices.empty()) + { + std::vector ids; + for (const int v : explicit_host_vertices) + append_unique_host_vertex(ac, v, ids, host.vertices); + if (static_cast(ids.size()) >= 4) + host.dimension = 3; + return host; + } if (zdim == 2) { + if (!explicit_host_vertices.empty()) + { + std::vector ids; + for (const int v : explicit_host_vertices) + append_unique_host_vertex(ac, v, ids, host.vertices); + if (static_cast(ids.size()) >= 4) + host.dimension = 3; + return host; + } + + const int n_cells = ac.n_entities(ac.tdim); std::vector ids; for (int c = 0; c < n_cells; ++c) { @@ -320,6 +412,65 @@ bool clip_line_interval_in_ordered_face(const LocalHostDomain& host, return lo <= hi; } +template +bool clip_line_interval_in_ordered_polygon_2d(const LocalHostDomain& host, + int tdim, + std::span x0, + std::span direction, + T tol, + T& lo, + T& hi) +{ + if (tdim != 2 || !host.ordered_boundary) + return false; + const int nverts = + static_cast(host.vertices.size() / static_cast(tdim)); + if (nverts < 3) + return false; + + std::array centroid = {T(0), T(0)}; + for (int i = 0; i < nverts; ++i) + { + centroid[0] += host.vertices[static_cast(2 * i)]; + centroid[1] += host.vertices[static_cast(2 * i + 1)]; + } + centroid[0] /= static_cast(nverts); + centroid[1] /= static_cast(nverts); + + for (int i = 0; i < nverts; ++i) + { + const auto a = std::span( + host.vertices.data() + static_cast(2 * i), 2); + const auto b = std::span( + host.vertices.data() + + static_cast(2 * ((i + 1) % nverts)), 2); + std::array edge = {b[0] - a[0], b[1] - a[1]}; + std::array inward = {-edge[1], edge[0]}; + std::array ca = {centroid[0] - a[0], centroid[1] - a[1]}; + if (local_dot( + std::span(inward.data(), 2), + std::span(ca.data(), 2)) < T(0)) + { + for (T& v : inward) + v = -v; + } + const T offset = -local_dot( + std::span(inward.data(), 2), a); + if (!clip_interval_by_halfspace( + x0, + direction, + std::span(inward.data(), 2), + offset, + tol, + lo, + hi)) + { + return false; + } + } + return lo <= hi; +} + template bool clip_line_interval_in_convex_hull(const LocalHostDomain& host, int tdim, @@ -332,8 +483,15 @@ bool clip_line_interval_in_convex_hull(const LocalHostDomain& host, const int nverts = static_cast(host.vertices.size() / static_cast(tdim)); if (host.dimension == 2) - return clip_line_interval_in_ordered_face( - host, tdim, x0, direction, tol, lo, hi); + { + if (tdim == 3) + return clip_line_interval_in_ordered_face( + host, tdim, x0, direction, tol, lo, hi); + if (tdim == 2) + return clip_line_interval_in_ordered_polygon_2d( + host, tdim, x0, direction, tol, lo, hi); + return false; + } if (host.dimension != 3 || tdim != 3 || nverts < 4) return false; @@ -448,6 +606,38 @@ std::vector project_to_local_host_space(const AdaptCell& ac, return out; } +template +std::vector local_host_face_normal_reference(const AdaptCell& ac, + int local_zero_entity_id, + T tol) +{ + const auto host = local_host_domain_for_zero_entity( + ac, local_zero_entity_id); + if (ac.tdim != 3 || !host.valid(ac.tdim) || host.dimension != 2) + return {}; + + const int nverts = static_cast( + host.vertices.size() / static_cast(ac.tdim)); + if (nverts < 3) + return {}; + + const auto p0 = std::span(host.vertices.data(), 3); + const auto p1 = std::span(host.vertices.data() + 3, 3); + const auto p2 = std::span(host.vertices.data() + 6, 3); + std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + auto normal = local_cross( + std::span(e0.data(), 3), + std::span(e1.data(), 3)); + const T nn = local_norm(std::span(normal.data(), 3)); + if (nn <= tol) + return {}; + std::vector out(3, T(0)); + for (int d = 0; d < 3; ++d) + out[static_cast(d)] = normal[static_cast(d)] / nn; + return out; +} + template struct BoundaryEdgeState { @@ -459,6 +649,7 @@ struct BoundaryEdgeState struct BoundaryEdgeRef { int local_zero_entity_id = -1; + int host_face_id = -1; bool use_curved_state = true; }; @@ -1123,6 +1314,8 @@ bool host_parameter_interval(const AdaptCell& ac, { const auto local_host = local_host_domain_for_zero_entity(ac, local_zero_entity_id); + const bool has_explicit_host = + zero_entity_has_explicit_host_cell(ac, local_zero_entity_id); if (local_host.valid(ac.tdim)) { T local_lo = -std::numeric_limits::infinity(); @@ -1135,6 +1328,12 @@ bool host_parameter_interval(const AdaptCell& ac, hi = local_hi; return true; } + if (has_explicit_host) + return false; + } + else if (has_explicit_host) + { + return false; } const auto host = zero_entity_parent_entity(ac, local_zero_entity_id); @@ -1685,7 +1884,20 @@ geom::VectorQuantity straight_zero_entity_normal_direction( } else if (tdim == 3) { - if (host.dim == 2) + auto explicit_host_normal = + local_host_face_normal_reference( + ac, local_zero_entity_id, tol); + if (!explicit_host_normal.empty()) + { + raw = geom::in_face_segment_normal( + std::span(a.data(), a.size()), + std::span(b.data(), b.size()), + std::span( + explicit_host_normal.data(), explicit_host_normal.size()), + true, + tol); + } + else if (host.dim == 2) { const auto face_normal = geom::parent_face_normal( ac.parent_cell_type, host.id, true, tol); @@ -1947,6 +2159,48 @@ std::vector level_set_physical_point(const LevelSetCell& ls_cell, ref); } +template +std::vector local_host_face_normal_physical( + const LevelSetCell& ls_cell, + const AdaptCell& ac, + int local_zero_entity_id, + T tol) +{ + const auto host = local_host_domain_for_zero_entity( + ac, local_zero_entity_id); + if (ac.tdim != 3 || ls_cell.gdim != 3 + || !host.valid(ac.tdim) || host.dimension != 2) + { + return {}; + } + + const int nverts = static_cast( + host.vertices.size() / static_cast(ac.tdim)); + if (nverts < 3) + return {}; + + const auto p0_ref = std::span(host.vertices.data(), 3); + const auto p1_ref = std::span(host.vertices.data() + 3, 3); + const auto p2_ref = std::span(host.vertices.data() + 6, 3); + const auto p0 = level_set_physical_point(ls_cell, p0_ref); + const auto p1 = level_set_physical_point(ls_cell, p1_ref); + const auto p2 = level_set_physical_point(ls_cell, p2_ref); + + std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + auto normal = local_cross( + std::span(e0.data(), 3), + std::span(e1.data(), 3)); + const T nn = local_norm(std::span(normal.data(), 3)); + if (nn <= tol) + return {}; + + std::vector out(3, T(0)); + for (int d = 0; d < 3; ++d) + out[static_cast(d)] = normal[static_cast(d)] / nn; + return out; +} + template T curving_level_set_value(const LevelSetCell& ls_cell, std::span ref) @@ -1983,11 +2237,38 @@ std::vector physical_host_gradient_reference_direction( grad_ref, fallback_ref); + bool restricted_to_explicit_host_face = false; + const auto explicit_host_normal = + local_host_face_normal_physical( + ls_cell, + ac, + local_zero_entity_id, + std::numeric_limits::epsilon() * T(128)); + if (!explicit_host_normal.empty() + && direction_phys.size() == explicit_host_normal.size()) + { + const T nn = vec_dot( + std::span( + explicit_host_normal.data(), explicit_host_normal.size()), + std::span( + explicit_host_normal.data(), explicit_host_normal.size())); + if (nn > T(0)) + { + const T c = vec_dot( + std::span(direction_phys.data(), direction_phys.size()), + std::span( + explicit_host_normal.data(), explicit_host_normal.size())) / nn; + for (std::size_t d = 0; d < direction_phys.size(); ++d) + direction_phys[d] -= c * explicit_host_normal[d]; + restricted_to_explicit_host_face = true; + } + } + const int parent_dim = ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; const int parent_id = ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; - if (parent_dim == 2 && gdim == 3) + if (!restricted_to_explicit_host_face && parent_dim == 2 && gdim == 3) { auto face = cell::face_vertices(ac.parent_cell_type, parent_id); if (face.size() >= 3) @@ -2024,7 +2305,7 @@ std::vector physical_host_gradient_reference_direction( } } } - else if (parent_dim == 1) + else if (!restricted_to_explicit_host_face && parent_dim == 1) { auto edges = cell::edges(ac.parent_cell_type); if (parent_id >= 0 && parent_id < static_cast(edges.size())) @@ -2053,7 +2334,7 @@ std::vector physical_host_gradient_reference_direction( } } } - else if (parent_dim == 0) + else if (!restricted_to_explicit_host_face && parent_dim == 0) { return std::vector(static_cast(tdim), T(0)); } @@ -3807,7 +4088,7 @@ bool build_hierarchical_face_nodes( if (!append_quad_interior_node(u, v)) { - state.failure_reason = "projection failed inside parent host domain"; + state.failure_reason = "projection failed inside zero-entity host domain"; return false; } } @@ -3959,6 +4240,16 @@ void build_curved_state(CurvedZeroEntityState& state, seeds.data() + static_cast(i * ac.tdim), static_cast(ac.tdim)); + if (zdim == 1 && (i == 0 || i == nseeds - 1)) + { + state.ref_nodes.insert(state.ref_nodes.end(), seed.begin(), seed.end()); + append_node_stats( + state, + accepted_node_stats(CurvingFailureCode::exact_vertex), + ac.tdim); + continue; + } + ProjectionStats stats; const bool ok = project_seed_to_zero_entity( ac, @@ -3974,7 +4265,7 @@ void build_curved_state(CurvedZeroEntityState& state, if (!ok) { state.status = CurvingStatus::failed; - state.failure_reason = "projection failed inside parent host domain"; + state.failure_reason = "projection failed inside zero-entity host domain"; state.ref_nodes.clear(); return; } @@ -4167,6 +4458,12 @@ void rebuild_identity(CurvingData& curving, curving.identity_valid = true; } +template +int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, + int zero_face_id, + int zero_edge_id, + T tol); + template std::vector face_boundary_zero_edges(const AdaptCell& ac, int local_zero_entity_id) @@ -4193,7 +4490,6 @@ std::vector face_boundary_zero_edges(const AdaptCell& ac, face_verts[static_cast(edge[1])] }; int match = -1; - bool use_curved_state = true; for (int z = 0; z < ac.n_zero_entities(); ++z) { if (ac.zero_entity_dim[static_cast(z)] != 1) @@ -4207,17 +4503,185 @@ std::vector face_boundary_zero_edges(const AdaptCell& ac, std::span(edge_verts.data(), edge_verts.size()))) { match = z; - use_curved_state = true; break; } } if (match < 0) return {}; - edge_ids.push_back({match, use_curved_state}); + const int host_face_id = + host_face_for_zero_face_boundary_edge( + ac, + local_zero_entity_id, + match, + T(256) * std::numeric_limits::epsilon()); + // Triangulated zero quadrilaterals introduce an artificial diagonal. + // It is a zero subedge of the output triangle, but it is not on a + // host-cell boundary face. Keep it as a curved zero edge; a negative + // host_face_id makes its contextual host the uncut subcell volume. + edge_ids.push_back({match, host_face_id, true}); } return edge_ids; } +template +bool point_in_triangle_ref(const AdaptCell& ac, + std::span tri_vertices, + std::span point, + T tol) +{ + if (ac.tdim != 3 || tri_vertices.size() != 3 || point.size() != 3) + return false; + const auto a = vertex_ref_point(ac, tri_vertices[0]); + const auto b = vertex_ref_point(ac, tri_vertices[1]); + const auto c = vertex_ref_point(ac, tri_vertices[2]); + std::array v0 = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; + std::array v1 = {c[0] - a[0], c[1] - a[1], c[2] - a[2]}; + std::array v2 = {point[0] - a[0], point[1] - a[1], point[2] - a[2]}; + const auto n = local_cross( + std::span(v0.data(), 3), + std::span(v1.data(), 3)); + const T area2 = local_norm(std::span(n.data(), 3)); + if (area2 <= tol) + return false; + const T plane = + std::fabs(local_dot( + std::span(v2.data(), 3), + std::span(n.data(), 3))) / area2; + if (plane > tol) + return false; + + const T d00 = local_dot( + std::span(v0.data(), 3), + std::span(v0.data(), 3)); + const T d01 = local_dot( + std::span(v0.data(), 3), + std::span(v1.data(), 3)); + const T d11 = local_dot( + std::span(v1.data(), 3), + std::span(v1.data(), 3)); + const T d20 = local_dot( + std::span(v2.data(), 3), + std::span(v0.data(), 3)); + const T d21 = local_dot( + std::span(v2.data(), 3), + std::span(v1.data(), 3)); + const T denom = d00 * d11 - d01 * d01; + if (std::fabs(denom) <= tol * tol) + return false; + const T v = (d11 * d20 - d01 * d21) / denom; + const T w = (d00 * d21 - d01 * d20) / denom; + const T u = T(1) - v - w; + return u >= -tol && v >= -tol && w >= -tol + && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; +} + +template +int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, + int zero_face_id, + int zero_edge_id, + T tol) +{ + if (zero_face_id < 0 || zero_edge_id < 0 + || zero_face_id >= ac.n_zero_entities() + || zero_edge_id >= ac.n_zero_entities()) + { + return -1; + } + if (zero_face_id >= static_cast(ac.zero_entity_host_cell_type.size())) + return -1; + const cell::type host_type = + ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; + const auto host_cell_vertices = + zero_entity_host_cell_vertex_ids(ac, zero_face_id); + const auto edge_vertices = + local_zero_entity_vertex_ids(ac, zero_edge_id); + if (host_cell_vertices.empty() || edge_vertices.size() != 2 + || host_type == cell::type::point) + { + return -1; + } + + const auto p0 = vertex_ref_point(ac, edge_vertices[0]); + const auto p1 = vertex_ref_point(ac, edge_vertices[1]); + for (int f = 0; f < cell::num_faces(host_type); ++f) + { + auto local_face = cell::face_vertices(host_type, f); + if (local_face.size() != 3) + continue; + std::array face_vertices = { + host_cell_vertices[static_cast(local_face[0])], + host_cell_vertices[static_cast(local_face[1])], + host_cell_vertices[static_cast(local_face[2])] + }; + if (point_in_triangle_ref( + ac, + std::span(face_vertices.data(), face_vertices.size()), + p0, + tol) + && point_in_triangle_ref( + ac, + std::span(face_vertices.data(), face_vertices.size()), + p1, + tol)) + { + return f; + } + } + return -1; +} + +template +void override_zero_edge_host_from_face(AdaptCell& ac, + int zero_edge_id, + int zero_face_id, + int host_face_id) +{ + if (zero_edge_id < 0 || zero_face_id < 0 + || zero_edge_id >= ac.n_zero_entities() + || zero_face_id >= ac.n_zero_entities()) + { + return; + } + if (zero_face_id >= static_cast(ac.zero_entity_host_cell_id.size()) + || zero_edge_id >= static_cast(ac.zero_entity_host_cell_id.size())) + { + return; + } + const auto face_host_vertices = + zero_entity_host_cell_vertex_ids(ac, zero_face_id); + if (face_host_vertices.empty()) + return; + + ac.zero_entity_host_cell_id[static_cast(zero_edge_id)] = + ac.zero_entity_host_cell_id[static_cast(zero_face_id)]; + ac.zero_entity_host_cell_type[static_cast(zero_edge_id)] = + ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; + ac.zero_entity_host_face_id[static_cast(zero_edge_id)] = + static_cast(host_face_id); + ac.zero_entity_source_level_set[static_cast(zero_edge_id)] = + ac.zero_entity_source_level_set[static_cast(zero_face_id)]; + + EntityAdjacency updated; + updated.offsets.push_back(std::int32_t(0)); + for (int z = 0; z < ac.n_zero_entities(); ++z) + { + if (z == zero_edge_id) + { + for (const int v : face_host_vertices) + updated.indices.push_back(static_cast(v)); + } + else if (z < ac.zero_entity_host_cell_vertices.size()) + { + const auto old = ac.zero_entity_host_cell_vertices[static_cast(z)]; + for (const auto v : old) + updated.indices.push_back(v); + } + updated.offsets.push_back( + static_cast(updated.indices.size())); + } + ac.zero_entity_host_cell_vertices = std::move(updated); +} + template const CurvedZeroEntityState& ensure_curved( CurvingData& curving, @@ -4262,6 +4726,7 @@ const CurvedZeroEntityState& ensure_curved( if (!valid && !failed) { std::vector> boundary_edges; + std::vector> contextual_boundary_edge_states; if (ac.zero_entity_dim[static_cast(local_zero_entity_id)] == 2 && !zero_entity_below_curving_threshold(ac, local_zero_entity_id, options)) { @@ -4300,20 +4765,31 @@ const CurvedZeroEntityState& ensure_curved( } boundary_edges.reserve(boundary_edge_refs.size()); + contextual_boundary_edge_states.reserve(boundary_edge_refs.size()); for (const auto& edge_ref : boundary_edge_refs) { const CurvedZeroEntityState* edge_state = nullptr; if (edge_ref.use_curved_state) { - edge_state = &ensure_curved( - curving, - parent_cell_ids, - adapt_cells, + AdaptCell edge_context_ac = ac; + override_zero_edge_host_from_face( + edge_context_ac, + edge_ref.local_zero_entity_id, + local_zero_entity_id, + edge_ref.host_face_id); + contextual_boundary_edge_states.emplace_back(); + auto& mutable_edge_state = + contextual_boundary_edge_states.back(); + build_curved_state( + mutable_edge_state, + edge_context_ac, level_set_cells, ls_offsets, cut_cell_id, edge_ref.local_zero_entity_id, + std::span>(), options); + edge_state = &mutable_edge_state; if (edge_state->status != CurvingStatus::curved) { state.status = CurvingStatus::failed; diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp index 52aceef..0777fea 100644 --- a/cpp/src/refine_cell.cpp +++ b/cpp/src/refine_cell.cpp @@ -117,6 +117,12 @@ void clear_topology_caches(AdaptCell& adapt_cell) adapt_cell.zero_entity_is_owned.clear(); adapt_cell.zero_entity_parent_dim.clear(); adapt_cell.zero_entity_parent_id.clear(); + adapt_cell.zero_entity_host_cell_id.clear(); + adapt_cell.zero_entity_host_cell_type.clear(); + adapt_cell.zero_entity_host_face_id.clear(); + adapt_cell.zero_entity_source_level_set.clear(); + adapt_cell.zero_entity_host_cell_vertices.offsets.clear(); + adapt_cell.zero_entity_host_cell_vertices.indices.clear(); ++adapt_cell.zero_entity_version; } @@ -423,6 +429,133 @@ void rebuild_leaf_cell_certification( } } +template +void rebuild_leaf_cell_provenance( + AdaptCell& adapt_cell, + std::span old_cell_types, + const EntityAdjacency& old_cells, + std::span old_cell_source_cell_id, + std::span old_cell_refinement_generation, + std::span old_cell_refinement_reason, + std::span old_cell_host_parent_cell_id, + const EntityAdjacency& old_host_cell_vertices, + std::span old_host_cell_types, + std::span old_source_level_sets, + std::span old_cell_ids_for_new_cells, + std::span source_cell_ids_for_new_cells, + std::span refinement_reasons_for_new_cells) +{ + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); + + adapt_cell.cell_source_cell_id.assign( + static_cast(n_cells), std::int32_t(-1)); + adapt_cell.cell_refinement_generation.assign( + static_cast(n_cells), std::int32_t(0)); + adapt_cell.cell_refinement_reason.assign( + static_cast(n_cells), CellRefinementReason::none); + adapt_cell.cell_host_parent_cell_id.assign( + static_cast(n_cells), adapt_cell.parent_cell_id); + + adapt_cell.entity_host_cell_id[tdim].clear(); + adapt_cell.entity_host_cell_type[tdim].clear(); + adapt_cell.entity_host_face_id[tdim].clear(); + adapt_cell.entity_source_level_set[tdim].clear(); + adapt_cell.entity_host_cell_vertices[tdim].offsets.clear(); + adapt_cell.entity_host_cell_vertices[tdim].indices.clear(); + adapt_cell.entity_host_cell_vertices[tdim].offsets.push_back(std::int32_t(0)); + + auto append_host_vertices = [&](std::span vertices) + { + for (const auto v : vertices) + adapt_cell.entity_host_cell_vertices[tdim].indices.push_back(v); + adapt_cell.entity_host_cell_vertices[tdim].offsets.push_back( + static_cast( + adapt_cell.entity_host_cell_vertices[tdim].indices.size())); + }; + + const int old_num_cells = static_cast(old_cell_types.size()); + for (int c = 0; c < n_cells; ++c) + { + const int old_c = + (c < static_cast(old_cell_ids_for_new_cells.size())) + ? old_cell_ids_for_new_cells[static_cast(c)] + : -1; + const int source_c = + (c < static_cast(source_cell_ids_for_new_cells.size())) + ? source_cell_ids_for_new_cells[static_cast(c)] + : old_c; + const CellRefinementReason reason = + (c < static_cast(refinement_reasons_for_new_cells.size())) + ? refinement_reasons_for_new_cells[static_cast(c)] + : CellRefinementReason::none; + const CellRefinementReason stored_reason = + (reason == CellRefinementReason::none + && old_c >= 0 + && old_c < static_cast(old_cell_refinement_reason.size())) + ? old_cell_refinement_reason[static_cast(old_c)] + : reason; + const int meta_c = + (source_c >= 0 && source_c < old_num_cells) + ? source_c + : ((old_c >= 0 && old_c < old_num_cells) ? old_c : -1); + + adapt_cell.cell_source_cell_id[static_cast(c)] = + static_cast(source_c); + if (meta_c >= 0 + && meta_c < static_cast(old_cell_refinement_generation.size())) + { + const bool unchanged_copy = + old_c == source_c && old_c >= 0 + && reason == CellRefinementReason::none; + adapt_cell.cell_refinement_generation[static_cast(c)] = + old_cell_refinement_generation[static_cast(meta_c)] + + (unchanged_copy ? 0 : 1); + } + adapt_cell.cell_refinement_reason[static_cast(c)] = stored_reason; + if (meta_c >= 0 + && meta_c < static_cast(old_cell_host_parent_cell_id.size())) + { + adapt_cell.cell_host_parent_cell_id[static_cast(c)] = + old_cell_host_parent_cell_id[static_cast(meta_c)]; + } + + adapt_cell.entity_host_cell_id[tdim].push_back( + static_cast(source_c)); + const cell::type host_type = + (meta_c >= 0 && meta_c < static_cast(old_host_cell_types.size())) + ? old_host_cell_types[static_cast(meta_c)] + : ((meta_c >= 0 && meta_c < static_cast(old_cell_types.size())) + ? old_cell_types[static_cast(meta_c)] + : adapt_cell.entity_types[tdim][static_cast(c)]); + adapt_cell.entity_host_cell_type[tdim].push_back(host_type); + adapt_cell.entity_host_face_id[tdim].push_back(std::int32_t(-1)); + const int source_level_set = + (meta_c >= 0 && meta_c < static_cast(old_source_level_sets.size())) + ? old_source_level_sets[static_cast(meta_c)] + : -1; + adapt_cell.entity_source_level_set[tdim].push_back( + reason == CellRefinementReason::cut_level_set + ? source_level_set + : std::int32_t(-1)); + + std::span host_vertices; + if (reason == CellRefinementReason::cut_level_set + && meta_c >= 0 && meta_c < old_cells.size()) + { + host_vertices = old_cells[static_cast(meta_c)]; + } + else if (old_c >= 0 && old_c < old_host_cell_vertices.size() + && reason == CellRefinementReason::none) + { + host_vertices = old_host_cell_vertices[static_cast(old_c)]; + } + if (host_vertices.empty()) + host_vertices = adapt_cell.entity_to_vertex[tdim][static_cast(c)]; + append_host_vertices(host_vertices); + } +} + void append_top_cell(std::vector& types, EntityAdjacency& adj, cell::type ctype, @@ -441,17 +574,49 @@ void apply_topology_update_preserve_certification( AdaptCell& adapt_cell, std::vector&& new_types, EntityAdjacency&& new_cells, - std::span old_cell_ids_for_new_cells) + std::span old_cell_ids_for_new_cells, + std::span source_cell_ids_for_new_cells, + std::span refinement_reasons_for_new_cells) { const int tdim = adapt_cell.tdim; const CapturedEdgeState old_edge_state = capture_edge_state(adapt_cell); const int old_num_level_sets = adapt_cell.cell_cert_tag_num_level_sets; const int old_num_cells = adapt_cell.n_entities(tdim); const std::vector old_cell_tags = adapt_cell.cell_cert_tag; + const std::vector old_cell_types = adapt_cell.entity_types[tdim]; + const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; + const std::vector old_cell_source_cell_id = + adapt_cell.cell_source_cell_id; + const std::vector old_cell_refinement_generation = + adapt_cell.cell_refinement_generation; + const std::vector old_cell_refinement_reason = + adapt_cell.cell_refinement_reason; + const std::vector old_cell_host_parent_cell_id = + adapt_cell.cell_host_parent_cell_id; + const EntityAdjacency old_host_cell_vertices = + adapt_cell.entity_host_cell_vertices[tdim]; + const std::vector old_host_cell_types = + adapt_cell.entity_host_cell_type[tdim]; + const std::vector old_source_level_sets = + adapt_cell.entity_source_level_set[tdim]; adapt_cell.entity_types[tdim] = std::move(new_types); adapt_cell.entity_to_vertex[tdim] = std::move(new_cells); + rebuild_leaf_cell_provenance( + adapt_cell, + std::span(old_cell_types), + old_cells, + std::span(old_cell_source_cell_id), + std::span(old_cell_refinement_generation), + std::span(old_cell_refinement_reason), + std::span(old_cell_host_parent_cell_id), + old_host_cell_vertices, + std::span(old_host_cell_types), + std::span(old_source_level_sets), + old_cell_ids_for_new_cells, + source_cell_ids_for_new_cells, + refinement_reasons_for_new_cells); rebuild_leaf_edges_preserve_certification(adapt_cell, old_edge_state); rebuild_leaf_cell_certification(adapt_cell, std::span(old_cell_tags), @@ -559,6 +724,8 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, new_cells.offsets.push_back(0); std::vector new_types; std::vector old_cell_ids_for_new_cells; + std::vector source_cell_ids_for_new_cells; + std::vector refinement_reasons_for_new_cells; for (int c = 0; c < static_cast(old_types.size()); ++c) { @@ -577,8 +744,12 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, const std::array t1 = {new_v, b, cvert}; append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); split = true; } else if ((b == v0 && cvert == v1) || (b == v1 && cvert == v0)) @@ -587,8 +758,12 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, const std::array t1 = {new_v, cvert, a}; append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); split = true; } else if ((cvert == v0 && a == v1) || (cvert == v1 && a == v0)) @@ -597,8 +772,12 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, const std::array t1 = {new_v, a, b}; append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t0)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); append_top_cell(new_types, new_cells, cell::type::triangle, std::span(t1)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); split = true; } @@ -607,6 +786,8 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, std::vector copy(verts.begin(), verts.end()); append_top_cell(new_types, new_cells, cell::type::triangle, std::span(copy)); old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); } } else @@ -642,8 +823,12 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, t1[static_cast(pos[0])] = new_v; append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(t0)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(t1)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::green_edge); split = true; break; } @@ -653,13 +838,17 @@ bool refine_green_on_multiple_root_edges(AdaptCell& adapt_cell, std::vector copy(verts.begin(), verts.end()); append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(copy)); old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); } } } apply_topology_update_preserve_certification( adapt_cell, std::move(new_types), std::move(new_cells), - std::span(old_cell_ids_for_new_cells)); + std::span(old_cell_ids_for_new_cells), + std::span(source_cell_ids_for_new_cells), + std::span(refinement_reasons_for_new_cells)); return true; } @@ -705,6 +894,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, new_cells.offsets.push_back(0); std::vector new_types; std::vector old_cell_ids_for_new_cells; + std::vector source_cell_ids_for_new_cells; + std::vector refinement_reasons_for_new_cells; for (int c = 0; c < n_cells; ++c) { @@ -718,6 +909,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, std::vector copy(verts.begin(), verts.end()); append_top_cell(new_types, new_cells, ctype, std::span(copy)); old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); continue; } @@ -743,6 +936,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, { append_top_cell(new_types, new_cells, cell::type::interval, std::span(child)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::red_cell); } break; } @@ -778,6 +973,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, local_to_global[static_cast(child[2])]}; append_top_cell(new_types, new_cells, cell::type::triangle, std::span(g)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::red_cell); } break; } @@ -816,6 +1013,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, local_to_global[static_cast(child[3])]}; append_top_cell(new_types, new_cells, cell::type::quadrilateral, std::span(g)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::red_cell); } break; } @@ -853,6 +1052,8 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, local_to_global[static_cast(child[3])]}; append_top_cell(new_types, new_cells, cell::type::tetrahedron, std::span(g)); old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::red_cell); } break; } @@ -864,7 +1065,9 @@ bool refine_red_on_ambiguous_cells(AdaptCell& adapt_cell, apply_topology_update_preserve_certification( adapt_cell, std::move(new_types), std::move(new_cells), - std::span(old_cell_ids_for_new_cells)); + std::span(old_cell_ids_for_new_cells), + std::span(source_cell_ids_for_new_cells), + std::span(refinement_reasons_for_new_cells)); return true; } @@ -913,11 +1116,15 @@ template void apply_topology_update_preserve_certification( AdaptCell&, std::vector&&, EntityAdjacency&&, - std::span); + std::span, + std::span, + std::span); template void apply_topology_update_preserve_certification( AdaptCell&, std::vector&&, EntityAdjacency&&, - std::span); + std::span, + std::span, + std::span); } // namespace cutcells diff --git a/cpp/src/refine_cell.h b/cpp/src/refine_cell.h index 7bb0986..816b5cc 100644 --- a/cpp/src/refine_cell.h +++ b/cpp/src/refine_cell.h @@ -64,7 +64,9 @@ void apply_topology_update_preserve_certification( AdaptCell& adapt_cell, std::vector&& new_types, EntityAdjacency&& new_cells, - std::span old_cell_ids_for_new_cells); + std::span old_cell_ids_for_new_cells, + std::span source_cell_ids_for_new_cells = {}, + std::span refinement_reasons_for_new_cells = {}); // ===================================================================== // Invalidation helpers diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index c5f265a..d79d5be 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -91,6 +91,25 @@ cutcells::GraphRefinementMode graph_refinement_mode_from_string( { return cutcells::GraphRefinementMode::red_failed_cell; } + if (name == "green_orthogonal_surface_edge" + || name == "orthogonal_surface_edge" + || name == "surface_orthogonal_edge" + || name == "orthogonal_edge") + { + return cutcells::GraphRefinementMode::green_orthogonal_surface_edge; + } + if (name == "green_midpoint_residual" + || name == "midpoint_residual" + || name == "residual") + { + return cutcells::GraphRefinementMode::green_midpoint_residual; + } + if (name == "green_normal_variation" + || name == "normal_variation" + || name == "curvature") + { + return cutcells::GraphRefinementMode::green_normal_variation; + } throw std::invalid_argument("unknown graph refinement mode"); } @@ -2495,6 +2514,10 @@ void declare_ho_cut(nb::module_& m, const std::string& type) std::vector local_zero_entity_id; std::vector level_set_id; std::vector zero_entity_dim; + std::vector host_cell_id; + std::vector host_cell_type; + std::vector host_face_id; + std::vector source_level_set; std::vector graph_accepted; std::vector graph_failed_checks; std::vector graph_checked_edges; @@ -2524,6 +2547,10 @@ void declare_ho_cut(nb::module_& m, const std::string& type) local_zero_entity_id.reserve(infos.size()); level_set_id.reserve(infos.size()); zero_entity_dim.reserve(infos.size()); + host_cell_id.reserve(infos.size()); + host_cell_type.reserve(infos.size()); + host_face_id.reserve(infos.size()); + source_level_set.reserve(infos.size()); graph_accepted.reserve(infos.size()); graph_failed_checks.reserve(infos.size()); graph_checked_edges.reserve(infos.size()); @@ -2597,6 +2624,10 @@ void declare_ho_cut(nb::module_& m, const std::string& type) if (record == nullptr) { level_set_id.push_back(-1); + host_cell_id.push_back(-1); + host_cell_type.push_back(-1); + host_face_id.push_back(-1); + source_level_set.push_back(-1); graph_accepted.push_back(-1); graph_failed_checks.push_back(-1); graph_checked_edges.push_back(-1); @@ -2626,6 +2657,11 @@ void declare_ho_cut(nb::module_& m, const std::string& type) } level_set_id.push_back(record->level_set_id); + host_cell_id.push_back(record->host_cell_id); + host_cell_type.push_back( + static_cast(record->host_cell_type)); + host_face_id.push_back(record->host_face_id); + source_level_set.push_back(record->source_level_set); graph_accepted.push_back(record->accepted ? 1 : 0); graph_failed_checks.push_back(record->failed_checks); graph_checked_edges.push_back(record->checked_edges); @@ -2678,6 +2714,14 @@ void declare_ho_cut(nb::module_& m, const std::string& type) as_nbarray(std::move(local_zero_entity_id)); out["level_set_id"] = as_nbarray(std::move(level_set_id)); out["zero_entity_dim"] = as_nbarray(std::move(zero_entity_dim)); + out["zero_entity_host_cell_id"] = + as_nbarray(std::move(host_cell_id)); + out["zero_entity_host_cell_type"] = + as_nbarray(std::move(host_cell_type)); + out["zero_entity_host_face_id"] = + as_nbarray(std::move(host_face_id)); + out["zero_entity_source_level_set"] = + as_nbarray(std::move(source_level_set)); out["graph_accepted"] = as_nbarray(std::move(graph_accepted)); out["graph_failed_checks"] = as_nbarray(std::move(graph_failed_checks)); @@ -2956,7 +3000,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, nb::arg("graph_max_refinements") = 5, nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("graph_refinement_mode") = "green_midpoint_residual", nb::arg("min_level_set_gradient_host_alignment") = T(0.9), nb::arg("graph_enabled") = true, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" @@ -2987,7 +3031,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, nb::arg("graph_max_refinements") = 5, nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("graph_refinement_mode") = "green_midpoint_residual", nb::arg("min_level_set_gradient_host_alignment") = T(0.9), nb::arg("graph_enabled") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" @@ -3018,7 +3062,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, nb::arg("graph_max_refinements") = 5, nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("graph_refinement_mode") = "green_midpoint_residual", nb::arg("min_level_set_gradient_host_alignment") = T(0.9), nb::arg("graph_enabled") = true, "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" @@ -3049,7 +3093,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, nb::arg("graph_max_refinements") = 5, nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_edge", + nb::arg("graph_refinement_mode") = "green_midpoint_residual", nb::arg("min_level_set_gradient_host_alignment") = T(0.9), nb::arg("graph_enabled") = true, "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" @@ -3253,6 +3297,26 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal) + .def_prop_ro( + "face_connectivity", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[2].indices.data(), + {self.entity_to_vertex[2].indices.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "face_offsets", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.entity_to_vertex[2].offsets.data(), + {self.entity_to_vertex[2].offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) .def_prop_ro( "cell_types", [](const AdaptCellT& self) @@ -3275,6 +3339,48 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_source_cell_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.cell_source_cell_id.data(), + {self.cell_source_cell_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_refinement_generation", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.cell_refinement_generation.data(), + {self.cell_refinement_generation.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_refinement_reason", + [](const AdaptCellT& self) + { + const auto* data = reinterpret_cast( + self.cell_refinement_reason.data()); + return nb::ndarray( + data, + {self.cell_refinement_reason.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_host_parent_cell_id", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.cell_host_parent_cell_id.data(), + {self.cell_host_parent_cell_id.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) .def_prop_ro( "cell_offsets", [](const AdaptCellT& self) @@ -3589,7 +3695,7 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("level_set_cell"), nb::arg("level_set_id"), nb::arg("projection_direction") = "level_set_gradient", - nb::arg("refinement_mode") = "green_edge", + nb::arg("refinement_mode") = "green_midpoint_residual", nb::arg("graph_max_refinements") = 5, nb::arg("max_relative_correction_distance") = T(0.5), nb::arg("max_relative_tangential_shift") = T(0.25), @@ -3696,7 +3802,7 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("edge_max_depth") = 20, nb::arg("triangulate_cut_parts") = false, nb::arg("projection_direction") = "level_set_gradient", - nb::arg("refinement_mode") = "green_edge", + nb::arg("refinement_mode") = "green_midpoint_residual", nb::arg("graph_max_refinements") = 5, nb::arg("max_relative_correction_distance") = T(0.5), nb::arg("max_relative_tangential_shift") = T(0.25), diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index 544c831..ce95f8e 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -471,6 +471,48 @@ def test_cut_graph_check_accepts_red_failed_cell_refinement_mode(self): self.assertGreater(np.asarray(summary["accepted"]).size, 0) self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) + def test_cut_graph_check_accepts_orthogonal_surface_edge_refinement_mode(self): + grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) + mesh = cutcells.mesh_from_pyvista(grid) + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + result = cutcells.cut( + mesh, + ls, + graph_max_refinements=1, + graph_refinement_mode="green_orthogonal_surface_edge", + ) + summary = result.graph_check_summary() + + self.assertGreater(np.asarray(summary["accepted"]).size, 0) + self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) + + def test_cut_graph_check_accepts_surface_error_refinement_modes(self): + grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) + mesh = cutcells.mesh_from_pyvista(grid) + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, + degree=2, + name="phi", + ) + + for mode in ("green_midpoint_residual", "green_normal_variation"): + result = cutcells.cut( + mesh, + ls, + graph_max_refinements=1, + graph_refinement_mode=mode, + ) + summary = result.graph_check_summary() + self.assertGreater(np.asarray(summary["accepted"]).size, 0) + self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) + def test_scalar_projection_records_parent_clipped_tetra_bracket(self): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( @@ -541,6 +583,41 @@ def test_straight_normal_projection_is_gradient_oriented(self): for idx in projected: self.assertGreaterEqual(float(np.dot(2.0 * seeds[idx], directions[idx])), -1e-12) + def test_triangulated_zero_quad_faces_curve_across_artificial_diagonal(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.5, + degree=1, + name="phi", + ) + + result = cutcells.ho_cut( + mesh, + ls, + triangulate=True, + graph_max_refinements=0, + min_level_set_gradient_host_alignment=0.0, + ) + curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") + graph_nodes = result["phi = 0"].graph_check_node_data() + + dim = np.asarray(curved["dim"], dtype=np.int32) + status = np.asarray(curved["status"], dtype=np.uint8) + edge_states = np.flatnonzero(dim == 1) + face_states = np.flatnonzero(dim == 2) + + self.assertGreaterEqual(edge_states.size, 5) + self.assertTrue(np.all(status[edge_states] == 2)) # CurvingStatus::curved + self.assertGreaterEqual(face_states.size, 2) + self.assertTrue(np.all(status[face_states] == 2)) # CurvingStatus::curved + face_interior_nodes = ( + np.asarray(graph_nodes["node_kind"], dtype=np.int32) == 3 + ) + self.assertTrue(np.any( + np.asarray(graph_nodes["node_index"], dtype=np.int32)[face_interior_nodes] > 0 + )) + def test_curving_accepts_near_zero_multi_level_set_node_without_projection(self): mesh = _single_triangle_mesh() diff --git a/python/tests/test_geometric_quantity_header.py b/python/tests/test_geometric_quantity_header.py new file mode 100644 index 0000000..f5ec6eb --- /dev/null +++ b/python/tests/test_geometric_quantity_header.py @@ -0,0 +1,114 @@ +import os +import shlex +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + + +def test_geometric_quantity_header_primitives(tmp_path): + cxx = os.environ.get("CXX") + if cxx: + compiler = shlex.split(cxx) + else: + path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") + if path is None: + pytest.skip("C++ compiler not available") + compiler = [path] + + repo = Path(__file__).resolve().parents[2] + source = tmp_path / "geometric_quantity_check.cpp" + exe = tmp_path / "geometric_quantity_check" + source.write_text( + textwrap.dedent( + r""" + #include "geometric_quantity.h" + + #include + #include + #include + #include + + namespace geom = cutcells::geom; + namespace cell = cutcells::cell; + + bool close(double a, double b) + { + return std::fabs(a - b) < 1.0e-12; + } + + int main() + { + const auto tet = cell::type::tetrahedron; + + // 1. Projecting a 3D gradient into a face plane. + // Tetrahedron face 3 is the z = 0 face with normal (0, 0, 1). + std::array gradient = {1.0, 2.0, 3.0}; + const auto face_gradient = + geom::project_into_parent_face_tangent( + tet, 3, std::span(gradient)); + assert(!face_gradient.degenerate()); + assert(close(face_gradient.value[0], 1.0)); + assert(close(face_gradient.value[1], 2.0)); + assert(close(face_gradient.value[2], 0.0)); + + // 2. Computing an in-face normal to a segment. + std::array a = {0.0, 0.0, 0.0}; + std::array b = {1.0, 0.0, 0.0}; + std::array face_normal = {0.0, 0.0, 1.0}; + const auto in_face_normal = + geom::in_face_segment_normal( + std::span(a), + std::span(b), + std::span(face_normal)); + assert(!in_face_normal.degenerate()); + assert(close(in_face_normal.value[0], 0.0)); + assert(close(in_face_normal.value[1], 1.0)); + assert(close(in_face_normal.value[2], 0.0)); + + // 3. Projecting a direction onto a parent edge. + // Tetrahedron edge 5 is the x-axis edge from vertex 0 to 1. + std::array direction = {3.0, 4.0, 5.0}; + const auto edge_direction = + geom::project_onto_parent_edge( + tet, 5, std::span(direction)); + assert(!edge_direction.degenerate()); + assert(close(edge_direction.value[0], 3.0)); + assert(close(edge_direction.value[1], 0.0)); + assert(close(edge_direction.value[2], 0.0)); + + // 4. Detecting a degenerate projected direction. + std::array vertical = {0.0, 0.0, 7.0}; + const auto admissible = + geom::admissible_direction_in_parent_frame( + tet, + geom::ParentEntity{2, 3}, + std::span(vertical)); + assert(admissible.degenerate()); + assert(admissible.degeneracy == geom::Degeneracy::zero_projection); + + return 0; + } + """ + ) + ) + + compile_result = subprocess.run( + [ + *compiler, + "-std=c++20", + "-I", + str(repo / "cpp/src"), + str(source), + "-o", + str(exe), + ], + capture_output=True, + text=True, + ) + assert compile_result.returncode == 0, compile_result.stderr + + run_result = subprocess.run([str(exe)], capture_output=True, text=True) + assert run_result.returncode == 0, run_result.stderr diff --git a/python/tests/test_graph_criteria_header.py b/python/tests/test_graph_criteria_header.py new file mode 100644 index 0000000..54f5b06 --- /dev/null +++ b/python/tests/test_graph_criteria_header.py @@ -0,0 +1,217 @@ +import os +import shlex +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + + +def test_graph_criteria_header_reports(tmp_path): + cxx = os.environ.get("CXX") + if cxx: + compiler = shlex.split(cxx) + else: + path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") + if path is None: + pytest.skip("C++ compiler not available") + compiler = [path] + + repo = Path(__file__).resolve().parents[2] + source = tmp_path / "graph_criteria_check.cpp" + exe = tmp_path / "graph_criteria_check" + source.write_text( + textwrap.dedent( + r""" + #include "graph_criteria.h" + + #include + #include + #include + #include + + namespace cell = cutcells::cell; + namespace geom = cutcells::geom; + namespace graph = cutcells::graph_criteria; + + template + std::span sp(const std::array& a) + { + return std::span(a.data(), a.size()); + } + + int main() + { + const auto tet = cell::type::tetrahedron; + const geom::ParentEntity z_face{2, 3}; + + graph::Options opts; + opts.min_restricted_gradient_strength = 1.0e-12; + opts.min_transversality = 1.0e-6; + opts.max_relative_correction_distance = 1.0; + opts.max_relative_tangential_shift = 0.25; + + graph::HostFrame edge_host; + edge_host.dimension = graph::HostDimension::edge; + edge_host.normal = {1.0, 0.0, 0.0}; + edge_host.tangent = {0.0, 1.0, 0.0}; + edge_host.h = 1.0; + + // 1. Candidate direction tangent to the true zero set. + { + const std::array xh = {0.2, 0.2, 0.0}; + const std::array grad = {0.0, 1.0, 0.0}; + const std::array d = {1.0, 0.0, 0.0}; + const std::array xc = {0.25, 0.2, 0.0}; + const auto report = graph::evaluate_direction( + tet, z_face, edge_host, sp(xh), sp(grad), sp(d), sp(xc), + graph::DirectionKind::projected_level_set_gradient, opts); + assert(!report.accepted); + assert(report.failure_reason == graph::FailureReason::tangent_to_zero_set); + } + + // 2. Candidate direction too tangential to the straight host normal. + { + graph::Options drift_opts = opts; + drift_opts.min_host_normal_alignment = 0.0; + drift_opts.max_drift_amplification = 4.0; + const std::array xh = {0.2, 0.2, 0.0}; + const std::array grad = {1.0, 0.01, 0.0}; + const std::array d = {1.0, 0.01, 0.0}; + const std::array xc = {0.25, 0.2005, 0.0}; + graph::HostFrame host = edge_host; + host.normal = {0.0, 1.0, 0.0}; + host.tangent = {1.0, 0.0, 0.0}; + const auto report = graph::evaluate_direction( + tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), + graph::DirectionKind::projected_level_set_gradient, drift_opts); + assert(!report.accepted); + assert(report.failure_reason + == graph::FailureReason::excessive_drift_amplification); + } + + // 3. Root search segment leaves the admissible parent face. + { + const std::array xh = {0.9, 0.05, 0.0}; + const std::array grad = {1.0, 0.0, 0.0}; + const std::array d = {1.0, 0.0, 0.0}; + const std::array xc = {1.1, 0.05, 0.0}; + const auto report = graph::evaluate_direction( + tet, z_face, edge_host, sp(xh), sp(grad), sp(d), sp(xc), + graph::DirectionKind::projected_straight_host_normal, opts); + assert(!report.accepted); + assert(report.failure_reason + == graph::FailureReason::root_segment_leaves_parent_entity); + } + + // 4. Parent-face edge using the in-face segment normal is valid. + { + const std::array a = {0.25, 0.25, 0.0}; + const std::array b = {0.75, 0.25, 0.0}; + const auto face_normal = + geom::parent_face_normal(tet, 3, true); + const auto in_face_normal = + geom::in_face_segment_normal( + sp(a), sp(b), + std::span( + face_normal.value.data(), face_normal.value.size())); + const auto tangent = + geom::segment_tangent(sp(a), sp(b), true); + graph::HostFrame host; + host.dimension = graph::HostDimension::edge; + host.normal = in_face_normal.value; + host.tangent = tangent.value; + host.h = 0.5; + const std::array xh = {0.4, 0.25, 0.0}; + const std::array grad = {0.0, 1.0, 0.0}; + const std::array d = {0.0, 1.0, 0.0}; + const std::array xc = {0.4, 0.30, 0.0}; + const auto report = graph::evaluate_direction( + tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), + graph::DirectionKind::projected_straight_host_normal, opts); + assert(report.accepted); + assert(report.failure_reason == graph::FailureReason::none); + } + + // 5. Excessive tangential shift is rejected even when the root lies on the ray. + { + graph::Options shift_opts = opts; + shift_opts.max_drift_amplification = 10.0; + shift_opts.max_relative_tangential_shift = 0.1; + graph::HostFrame host = edge_host; + host.normal = {0.0, 1.0, 0.0}; + host.tangent = {1.0, 0.0, 0.0}; + const std::array xh = {0.1, 0.1, 0.0}; + const std::array grad = {0.8, 1.0, 0.0}; + const std::array d = {0.8, 1.0, 0.0}; + const std::array xc = {0.26, 0.3, 0.0}; + const auto report = graph::evaluate_direction( + tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), + graph::DirectionKind::projected_level_set_gradient, shift_opts); + assert(!report.accepted); + assert(report.failure_reason + == graph::FailureReason::excessive_tangential_shift); + } + + // Preferred direction rule: accept the straight-host normal first. + { + graph::HostFrame host = edge_host; + host.normal = {0.0, 1.0, 0.0}; + host.tangent = {1.0, 0.0, 0.0}; + const std::array xh = {0.2, 0.2, 0.0}; + const std::array grad = {0.0, 1.0, 0.0}; + const std::array grad_d = {0.2, 1.0, 0.0}; + const std::array grad_root = {0.21, 0.25, 0.0}; + const std::array normal_d = {0.0, 1.0, 0.0}; + const std::array normal_root = {0.2, 0.25, 0.0}; + const auto selected = graph::select_preferred_direction( + tet, z_face, host, sp(xh), sp(grad), sp(grad_d), sp(grad_root), + sp(normal_d), sp(normal_root), opts); + assert(selected.accepted); + assert(selected.selected_kind + == graph::DirectionKind::projected_straight_host_normal); + } + + // Ordering and face quality helpers remain pure accept/reject checks. + { + const std::array host_pts = {0.0, 0.0, 0.5, 0.0, 1.0, 0.0}; + const std::array ok_pts = {0.0, 0.0, 0.5, 0.0, 1.0, 0.0}; + const std::array folded_pts = {0.0, 0.0, 0.6, 0.0, 0.55, 0.0}; + const std::array tangent = {1.0, 0.0}; + const auto ok = graph::evaluate_projected_edge_ordering( + sp(host_pts), sp(ok_pts), 2, sp(tangent), 1.0, opts); + const auto folded = graph::evaluate_projected_edge_ordering( + sp(host_pts), sp(folded_pts), 2, sp(tangent), 1.0, opts); + assert(ok.accepted); + assert(!folded.accepted); + assert(folded.failure_reason == graph::FailureReason::edge_ordering_fold); + + assert(graph::failure_reason_name( + graph::FailureReason::surface_jacobian_not_positive) + == "surface_jacobian_not_positive"); + } + + return 0; + } + """ + ) + ) + + compile_result = subprocess.run( + [ + *compiler, + "-std=c++20", + "-I", + str(repo / "cpp/src"), + str(source), + "-o", + str(exe), + ], + capture_output=True, + text=True, + ) + assert compile_result.returncode == 0, compile_result.stderr + + run_result = subprocess.run([str(exe)], capture_output=True, text=True) + assert run_result.returncode == 0, run_result.stderr diff --git a/python/tests/test_tangent_shift_header.py b/python/tests/test_tangent_shift_header.py new file mode 100644 index 0000000..211bd95 --- /dev/null +++ b/python/tests/test_tangent_shift_header.py @@ -0,0 +1,207 @@ +import os +import shlex +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + + +def test_tangent_shift_header_reports(tmp_path): + cxx = os.environ.get("CXX") + if cxx: + compiler = shlex.split(cxx) + else: + path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") + if path is None: + pytest.skip("C++ compiler not available") + compiler = [path] + + repo = Path(__file__).resolve().parents[2] + source = tmp_path / "tangent_shift_check.cpp" + exe = tmp_path / "tangent_shift_check" + source.write_text( + textwrap.dedent( + r""" + #include "tangent_shift.h" + + #include + #include + #include + #include + #include + + namespace cell = cutcells::cell; + namespace geom = cutcells::geom; + namespace ts = cutcells::tangent_shift; + + bool close(double a, double b, double tol = 1.0e-8) + { + return std::fabs(a - b) <= tol; + } + + std::span sp(const std::vector& v) + { + return std::span(v.data(), v.size()); + } + + std::array circle_point(double fraction) + { + const double x = 0.2 + 0.6 * fraction; + const double dx = x - 0.5; + const double y = -0.5 + std::sqrt(0.36 - dx * dx); + return {x, y, 0.0}; + } + + std::vector make_points( + std::initializer_list> points) + { + std::vector out; + for (const auto& point : points) + out.insert(out.end(), point.begin(), point.end()); + return out; + } + + int main() + { + const auto tet = cell::type::tetrahedron; + const geom::ParentEntity z_face{2, 3}; + const auto a = circle_point(0.0); + const auto b = circle_point(1.0); + const auto midpoint = circle_point(0.5); + const auto drifted = circle_point(0.70); + + const std::vector provisional = make_points({a, b}); + const std::vector desired_mid = {0.0, 0.5, 1.0}; + + ts::Options opts; + opts.max_relative_arclength_drift = 0.02; + opts.max_absolute_arclength_drift = 1.0e-12; + opts.max_relative_final_arclength_error = 0.03; + opts.max_absolute_final_arclength_error = 1.0e-10; + opts.min_relative_spacing = 1.0e-8; + opts.phi_tolerance = 1.0e-10; + opts.max_correction_iterations = 24; + + auto circle_phi = [](std::span x) -> double + { + const double dx = x[0] - 0.5; + const double dy = x[1] + 0.5; + return dx * dx + dy * dy - 0.36; + }; + auto circle_grad = [](std::span x, + std::span g) + { + g[0] = 2.0 * (x[0] - 0.5); + g[1] = 2.0 * (x[1] + 0.5); + g[2] = 0.0; + }; + + // 1. Projected midpoint already has acceptable arclength drift. + { + const std::vector edge = make_points({a, midpoint, b}); + const auto report = + ts::correct_projected_edge_node( + tet, z_face, sp(provisional), sp(edge), 3, + sp(desired_mid), 1, circle_phi, circle_grad, opts); + assert(report.accepted); + assert(!report.corrected); + assert(report.failure_reason == ts::FailureReason::none); + assert(report.metrics.initial_arclength_error < 1.0e-12); + } + + // 2. Excessive midpoint drift is shifted and re-corrected to phi = 0. + { + const std::vector edge = make_points({a, drifted, b}); + const auto report = + ts::correct_projected_edge_node( + tet, z_face, sp(provisional), sp(edge), 3, + sp(desired_mid), 1, circle_phi, circle_grad, opts); + assert(report.accepted); + assert(report.corrected); + assert(close(report.corrected_node[0], midpoint[0])); + assert(close(report.corrected_node[1], midpoint[1])); + assert(report.metrics.phi_residual <= opts.phi_tolerance); + } + + // 3. If the correction would leave the admissible parent face, reject. + { + auto outside_phi = [](std::span x) -> double + { + return x[0] + x[1] - 1.2; + }; + auto outside_grad = [](std::span, + std::span g) + { + g[0] = 1.0; + g[1] = 1.0; + g[2] = 0.0; + }; + const std::vector edge = make_points({a, drifted, b}); + auto fail_opts = opts; + fail_opts.max_correction_iterations = 8; + const auto report = + ts::correct_projected_edge_node( + tet, z_face, sp(provisional), sp(edge), 3, + sp(desired_mid), 1, outside_phi, outside_grad, fail_opts); + assert(!report.accepted); + assert(report.request_refinement); + assert(report.failure_reason + == ts::FailureReason::correction_left_parent_entity); + } + + // 4. A correction that crosses the next edge node destroys ordering. + { + const auto before = circle_point(0.35); + const auto next = circle_point(0.45); + const std::vector edge = make_points({a, before, next, b}); + const std::vector desired = {0.0, 0.5, 0.75, 1.0}; + const auto report = + ts::correct_projected_edge_node( + tet, z_face, sp(provisional), sp(edge), 3, + sp(desired), 1, circle_phi, circle_grad, opts); + assert(!report.accepted); + assert(report.request_refinement); + assert(report.failure_reason + == ts::FailureReason::ordering_destroyed); + } + + // 5. If re-correction cannot satisfy phi = 0, reject. + { + const std::vector edge = make_points({a, drifted, b}); + auto no_iter = opts; + no_iter.max_correction_iterations = 0; + const auto report = + ts::correct_projected_edge_node( + tet, z_face, sp(provisional), sp(edge), 3, + sp(desired_mid), 1, circle_phi, circle_grad, no_iter); + assert(!report.accepted); + assert(report.request_refinement); + assert(report.failure_reason + == ts::FailureReason::phi_residual_too_large); + } + + return 0; + } + """ + ) + ) + + compile_result = subprocess.run( + [ + *compiler, + "-std=c++20", + "-I", + str(repo / "cpp/src"), + str(source), + "-o", + str(exe), + ], + capture_output=True, + text=True, + ) + assert compile_result.returncode == 0, compile_result.stderr + + run_result = subprocess.run([str(exe)], capture_output=True, text=True) + assert run_result.returncode == 0, run_result.stderr From e6e939765b0daad2aa48950a8ab73869ba371cb6 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Tue, 5 May 2026 19:58:54 +0200 Subject: [PATCH 20/23] mapping looking ok for quads and tris. Triangulation leads to problems. --- cpp/src/adapt_cell.cpp | 46 +- cpp/src/adapt_cell.h | 17 +- cpp/src/cell_certification.cpp | 52 +- cpp/src/curving.cpp | 47 +- cpp/src/curving.h | 2 +- cpp/src/edge_root.h | 2 +- cpp/src/ho_mesh_part_output.cpp | 643 +++++++++++++----- cpp/src/quadrature.cpp | 22 +- cpp/src/refine_cell.cpp | 19 +- python/cutcells/wrapper.cpp | 40 ++ python/tests/test_certification_refinement.py | 33 +- python/tests/test_runtime_quadrature.py | 16 + 12 files changed, 711 insertions(+), 228 deletions(-) diff --git a/cpp/src/adapt_cell.cpp b/cpp/src/adapt_cell.cpp index 1f2cba6..5be42df 100644 --- a/cpp/src/adapt_cell.cpp +++ b/cpp/src/adapt_cell.cpp @@ -213,6 +213,8 @@ void build_faces(AdaptCell& ac) std::map, int> face_map; const int n_cells = ac.n_entities(ac.tdim); + std::vector> face_to_cells; + std::vector> cell_to_faces(static_cast(n_cells)); for (int c = 0; c < n_cells; ++c) { const cell::type ctype = ac.entity_types[ac.tdim][static_cast(c)]; @@ -235,17 +237,24 @@ void build_faces(AdaptCell& ac) } std::sort(sorted_fv.begin(), sorted_fv.end()); - if (face_map.find(sorted_fv) == face_map.end()) + auto face_it = face_map.find(sorted_fv); + int face_id = -1; + if (face_it == face_map.end()) { - face_map[sorted_fv] = ac.n_entities(2); + face_id = ac.n_entities(2); + face_map[sorted_fv] = face_id; ac.entity_types[2].push_back(cell::face_type(ctype, fi)); for (int j = 0; j < fsize; ++j) ac.entity_to_vertex[2].indices.push_back(global_fv[static_cast(j)]); ac.entity_to_vertex[2].offsets.push_back( static_cast(ac.entity_to_vertex[2].indices.size())); + face_to_cells.emplace_back(); const int host_cell_id = - (c < static_cast(ac.cell_source_cell_id.size())) + (ac.tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_host_cell_id[ac.tdim].size())) + ? ac.entity_host_cell_id[ac.tdim][static_cast(c)] + : (c < static_cast(ac.cell_source_cell_id.size())) ? ac.cell_source_cell_id[static_cast(c)] : c; const cell::type host_cell_type = @@ -265,8 +274,34 @@ void build_faces(AdaptCell& ac) host_vertices.empty() ? std::span(cell_verts) : host_vertices); } + else + face_id = face_it->second; + + face_to_cells[static_cast(face_id)].push_back( + static_cast(c)); + cell_to_faces[static_cast(c)].push_back( + static_cast(face_id)); } } + + auto fill_connectivity = [](EntityAdjacency& adjacency, + const std::vector>& rows) + { + adjacency.indices.clear(); + adjacency.offsets.clear(); + adjacency.offsets.push_back(std::int32_t(0)); + for (const auto& row : rows) + { + for (const auto value : row) + adjacency.indices.push_back(value); + adjacency.offsets.push_back( + static_cast(adjacency.indices.size())); + } + }; + fill_connectivity(ac.connectivity[2][ac.tdim], face_to_cells); + ac.has_connectivity[2][ac.tdim] = 1; + fill_connectivity(ac.connectivity[ac.tdim][2], cell_to_faces); + ac.has_connectivity[ac.tdim][2] = 1; } // --------------------------------------------------------------------------- @@ -311,7 +346,10 @@ void build_edges(AdaptCell& ac) static_cast(ac.entity_to_vertex[1].indices.size())); const int host_cell_id = - (c < static_cast(ac.cell_source_cell_id.size())) + (tdim < AdaptCell::max_dim + && c < static_cast(ac.entity_host_cell_id[tdim].size())) + ? ac.entity_host_cell_id[tdim][static_cast(c)] + : (c < static_cast(ac.cell_source_cell_id.size())) ? ac.cell_source_cell_id[static_cast(c)] : c; const cell::type host_cell_type = diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h index 6d5347f..a0f7760 100644 --- a/cpp/src/adapt_cell.h +++ b/cpp/src/adapt_cell.h @@ -198,10 +198,12 @@ struct AdaptCell /// This is CSR-style: offsets + flat indices. std::array entity_to_vertex; - /// Structured host provenance for entities. For zero faces this stores - /// the uncut adaptcell leaf cell that produced the extracted surface - /// element. For zero edges on a zero face boundary, host_face_id is filled - /// by the face context when available. + /// Structured embedding-host provenance for entities. For zero entities + /// created by a cut, this is the uncut AdaptCell leaf that embeds the + /// interface and bounds curving/projection. It is deliberately not the + /// accepted cut output cell that happens to contain the entity. + /// For zero edges on a zero face boundary, host_face_id is filled by the + /// face context when available. std::array, max_dim> entity_host_cell_id; std::array, max_dim> entity_host_cell_type; std::array, max_dim> entity_host_face_id; @@ -242,10 +244,9 @@ struct AdaptCell std::vector zero_entity_parent_dim; ///< -1 if not on a shared parent entity std::vector zero_entity_parent_id; ///< -1 if not on a shared parent entity - /// Provenance back to the uncut adaptcell leaf cell used to generate the - /// zero entity. These arrays mirror entity_host_* for the zero-entity - /// inventory and survive inventory rebuilds as long as the underlying - /// entity provenance is available. + /// Embedding-host provenance for the zero entity. These arrays mirror + /// entity_host_* for the zero-entity inventory and survive inventory + /// rebuilds as long as the underlying entity provenance is available. std::vector zero_entity_host_cell_id; std::vector zero_entity_host_cell_type; std::vector zero_entity_host_face_id; diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index c9a4289..695f5c4 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -2432,6 +2432,27 @@ bool point_in_host_triangle(const AdaptCell& ac, && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; } +template +bool point_in_host_face(const AdaptCell& ac, + std::span face_vertices, + std::span point, + T tol) +{ + if (face_vertices.size() == 3) + return point_in_host_triangle(ac, face_vertices, point, tol); + if (face_vertices.size() != 4) + return false; + + const std::array tri0 = { + face_vertices[0], face_vertices[1], face_vertices[3]}; + const std::array tri1 = { + face_vertices[0], face_vertices[3], face_vertices[2]}; + return point_in_host_triangle( + ac, std::span(tri0.data(), tri0.size()), point, tol) + || point_in_host_triangle( + ac, std::span(tri1.data(), tri1.size()), point, tol); +} + template std::vector host_boundary_face_normal_for_zero_face_edge( const AdaptCell& ac, @@ -2459,23 +2480,17 @@ std::vector host_boundary_face_normal_for_zero_face_edge( for (int f = 0; f < cell::num_faces(host_type); ++f) { const auto local_face = cell::face_vertices(host_type, f); - if (local_face.size() != 3) + if (local_face.size() != 3 && local_face.size() != 4) continue; - std::array face_vertices = { - host_vertices[static_cast(local_face[0])], - host_vertices[static_cast(local_face[1])], - host_vertices[static_cast(local_face[2])] - }; - if (!point_in_host_triangle( - ac, - std::span(face_vertices.data(), face_vertices.size()), - edge_a, - tol) - || !point_in_host_triangle( - ac, - std::span(face_vertices.data(), face_vertices.size()), - edge_b, - tol)) + std::array face_vertices = {-1, -1, -1, -1}; + for (std::size_t i = 0; i < local_face.size(); ++i) + face_vertices[i] = + host_vertices[static_cast(local_face[i])]; + + const std::span face_span( + face_vertices.data(), local_face.size()); + if (!point_in_host_face(ac, face_span, edge_a, tol) + || !point_in_host_face(ac, face_span, edge_b, tol)) { continue; } @@ -2483,7 +2498,10 @@ std::vector host_boundary_face_normal_for_zero_face_edge( const auto normal = geom::face_normal( point_span(ac.vertex_coords, face_vertices[0], 3), point_span(ac.vertex_coords, face_vertices[1], 3), - point_span(ac.vertex_coords, face_vertices[2], 3), + point_span( + ac.vertex_coords, + face_vertices[local_face.size() == 4 ? 3 : 2], + 3), true, tol); if (!normal.degenerate()) diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp index f97c901..1eb52a7 100644 --- a/cpp/src/curving.cpp +++ b/cpp/src/curving.cpp @@ -4575,6 +4575,27 @@ bool point_in_triangle_ref(const AdaptCell& ac, && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; } +template +bool point_in_face_ref(const AdaptCell& ac, + std::span face_vertices, + std::span point, + T tol) +{ + if (face_vertices.size() == 3) + return point_in_triangle_ref(ac, face_vertices, point, tol); + if (face_vertices.size() != 4) + return false; + + const std::array tri0 = { + face_vertices[0], face_vertices[1], face_vertices[3]}; + const std::array tri1 = { + face_vertices[0], face_vertices[3], face_vertices[2]}; + return point_in_triangle_ref( + ac, std::span(tri0.data(), tri0.size()), point, tol) + || point_in_triangle_ref( + ac, std::span(tri1.data(), tri1.size()), point, tol); +} + template int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, int zero_face_id, @@ -4606,23 +4627,17 @@ int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, for (int f = 0; f < cell::num_faces(host_type); ++f) { auto local_face = cell::face_vertices(host_type, f); - if (local_face.size() != 3) + if (local_face.size() != 3 && local_face.size() != 4) continue; - std::array face_vertices = { - host_cell_vertices[static_cast(local_face[0])], - host_cell_vertices[static_cast(local_face[1])], - host_cell_vertices[static_cast(local_face[2])] - }; - if (point_in_triangle_ref( - ac, - std::span(face_vertices.data(), face_vertices.size()), - p0, - tol) - && point_in_triangle_ref( - ac, - std::span(face_vertices.data(), face_vertices.size()), - p1, - tol)) + std::array face_vertices = {-1, -1, -1, -1}; + for (std::size_t i = 0; i < local_face.size(); ++i) + face_vertices[i] = + host_cell_vertices[static_cast(local_face[i])]; + + const std::span face_span( + face_vertices.data(), local_face.size()); + if (point_in_face_ref(ac, face_span, p0, tol) + && point_in_face_ref(ac, face_span, p1, tol)) { return f; } diff --git a/cpp/src/curving.h b/cpp/src/curving.h index 75b768f..ddae978 100644 --- a/cpp/src/curving.h +++ b/cpp/src/curving.h @@ -82,7 +82,7 @@ struct CurvingOptions CurvingDirectionMode::level_set_gradient; int max_iter = 32; T xtol = T(1e-12); - T ftol = std::sqrt(std::numeric_limits::epsilon()); + T ftol = T(64) * std::numeric_limits::epsilon(); T domain_tol = T(1e-10); T active_face_tol = T(1e-9); // Length-scale threshold in parent reference coordinates. Zero edges diff --git a/cpp/src/edge_root.h b/cpp/src/edge_root.h index f344194..5b98baa 100644 --- a/cpp/src/edge_root.h +++ b/cpp/src/edge_root.h @@ -22,7 +22,7 @@ namespace cutcells::cell::edge_root template inline T default_value_tolerance() { - return std::sqrt(std::numeric_limits::epsilon()); + return T(64) * std::numeric_limits::epsilon(); } enum class method diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index 9f89f1a..813cded 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -457,6 +457,46 @@ std::vector cell_vertex_shape_weights(cell::type cell_type, throw std::runtime_error("cell_vertex_shape_weights: unsupported cell type"); } +template +std::vector cell_vertex_shape_gradients(cell::type cell_type, + std::span xi) +{ + if (cell_type == cell::type::interval) + return {T(-1), T(1)}; + if (cell_type == cell::type::triangle) + return {T(-1), T(-1), + T(1), T(0), + T(0), T(1)}; + if (cell_type == cell::type::quadrilateral) + { + const T u = xi[0]; + const T v = xi[1]; + return {-(T(1) - v), -(T(1) - u), + (T(1) - v), -u, + -v, T(1) - u, + v, u}; + } + if (cell_type == cell::type::tetrahedron) + return {T(-1), T(-1), T(-1), + T(1), T(0), T(0), + T(0), T(1), T(0), + T(0), T(0), T(1)}; + if (cell_type == cell::type::prism) + { + const T u = xi[0]; + const T v = xi[1]; + const T z = xi[2]; + const T w0 = T(1) - u - v; + return {-(T(1) - z), -(T(1) - z), -w0, + (T(1) - z), T(0), -u, + T(0), (T(1) - z), -v, + -z, -z, w0, + z, T(0), u, + T(0), z, v}; + } + throw std::runtime_error("cell_vertex_shape_gradients: unsupported cell type"); +} + template std::vector affine_ref_from_bary(const LocalCurvedSimplexMap& map, std::span bary) @@ -490,6 +530,30 @@ std::vector straight_ref_from_cell_point(const LocalCurvedSimplexMap& map, return x; } +template +std::vector straight_ref_jacobian_from_cell_point( + const LocalCurvedSimplexMap& map, + std::span xi) +{ + const auto gradients = cell_vertex_shape_gradients(map.simplex_type, xi); + const int nv = cell::get_num_vertices(map.simplex_type); + std::vector J(static_cast(map.dim * map.parent_tdim), T(0)); + for (int c = 0; c < map.dim; ++c) + { + for (int v = 0; v < nv; ++v) + { + const T dNv = gradients[static_cast(v * map.dim + c)]; + for (int d = 0; d < map.parent_tdim; ++d) + { + J[static_cast(c * map.parent_tdim + d)] += + dNv * map.ref_vertex_coords[ + static_cast(v * map.parent_tdim + d)]; + } + } + } + return J; +} + template std::vector push_parent_ref_to_physical(const LocalCurvedSimplexMap& map, std::span ref_point) @@ -502,6 +566,89 @@ std::vector push_parent_ref_to_physical(const LocalCurvedSimplexMap& map, return phys; } +template +std::vector parent_ref_to_physical_jacobian( + const LocalCurvedSimplexMap& map) +{ + const auto cols = cell::jacobian_col_indices(map.parent_cell_type); + const T* x0 = map.parent_physical_coords.data(); + std::vector J( + static_cast(map.parent_tdim * map.gdim), T(0)); + for (int c = 0; c < map.parent_tdim; ++c) + { + const T* xc = + map.parent_physical_coords.data() + + static_cast(cols[c] * map.gdim); + for (int r = 0; r < map.gdim; ++r) + J[static_cast(c * map.gdim + r)] = xc[r] - x0[r]; + } + return J; +} + +template +std::vector compose_ref_to_physical_jacobian( + const LocalCurvedSimplexMap& map, + std::span ref_jacobian) +{ + const auto parent_J = parent_ref_to_physical_jacobian(map); + std::vector J(static_cast(map.dim * map.gdim), T(0)); + for (int c = 0; c < map.dim; ++c) + { + for (int p = 0; p < map.parent_tdim; ++p) + { + const T dref = + ref_jacobian[static_cast(c * map.parent_tdim + p)]; + for (int r = 0; r < map.gdim; ++r) + { + J[static_cast(c * map.gdim + r)] += + dref * parent_J[static_cast(p * map.gdim + r)]; + } + } + } + return J; +} + +template +T measure_from_jacobian(int dim, int gdim, std::span J) +{ + if (dim == 1) + { + T n2 = T(0); + for (int r = 0; r < gdim; ++r) + n2 += J[static_cast(r)] + * J[static_cast(r)]; + return std::sqrt(n2); + } + + if (dim == gdim) + { + if (dim == 2) + return J[0] * J[3] - J[2] * J[1]; + if (dim == 3) + { + return J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + } + } + + T G[9] = {}; + for (int i = 0; i < dim; ++i) + { + for (int j = 0; j < dim; ++j) + { + for (int r = 0; r < gdim; ++r) + { + G[i * dim + j] += J[static_cast(i * gdim + r)] + * J[static_cast(j * gdim + r)]; + } + } + } + if (dim == 2) + return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); + return T(0); +} + inline bool same_unordered(std::span a, std::span b) { if (a.size() != b.size()) @@ -547,6 +694,28 @@ T lagrange_basis_1d(int i, std::span params, T x) return value; } +template +T lagrange_basis_1d_derivative(int i, std::span params, T x) +{ + const T xi = params[static_cast(i)]; + T derivative = T(0); + for (int m = 0; m < static_cast(params.size()); ++m) + { + if (m == i) + continue; + T term = T(1) / (xi - params[static_cast(m)]); + for (int j = 0; j < static_cast(params.size()); ++j) + { + if (j == i || j == m) + continue; + term *= (x - params[static_cast(j)]) + / (xi - params[static_cast(j)]); + } + derivative += term; + } + return derivative; +} + template T warp_factor(int order, T r) { @@ -687,6 +856,31 @@ std::vector triangle_monomials(int order, std::span bary) return values; } +template +std::pair, std::vector> triangle_monomial_gradients( + int order, + std::span bary) +{ + const T u = bary[1]; + const T v = bary[2]; + std::vector du; + std::vector dv; + du.reserve(static_cast((order + 1) * (order + 2) / 2)); + dv.reserve(static_cast((order + 1) * (order + 2) / 2)); + for (int total = 0; total <= order; ++total) + { + for (int j = 0; j <= total; ++j) + { + const int i = total - j; + du.push_back(i == 0 ? T(0) + : T(i) * std::pow(u, i - 1) * std::pow(v, j)); + dv.push_back(j == 0 ? T(0) + : T(j) * std::pow(u, i) * std::pow(v, j - 1)); + } + } + return {std::move(du), std::move(dv)}; +} + template bool solve_dense(std::vector A, std::vector b, int n, std::vector& x) { @@ -737,9 +931,18 @@ bool solve_dense(std::vector A, std::vector b, int n, std::vector& x) } template -std::vector triangle_lagrange_basis(int order, - curving::NodeFamily node_family, - std::span bary) +struct TriangleLagrangeBasisData +{ + std::vector values; + std::vector du; + std::vector dv; +}; + +template +TriangleLagrangeBasisData triangle_lagrange_basis_data( + int order, + curving::NodeFamily node_family, + std::span bary) { const auto nodes = triangle_interpolation_barycentric_nodes(order, node_family); @@ -756,10 +959,24 @@ std::vector triangle_lagrange_basis(int order, } const auto rhs = triangle_monomials(order, bary); - std::vector basis; - if (!solve_dense(std::move(matrix), rhs, n, basis)) + const auto [rhs_du, rhs_dv] = triangle_monomial_gradients(order, bary); + + TriangleLagrangeBasisData out; + if (!solve_dense(matrix, rhs, n, out.values) + || !solve_dense(matrix, rhs_du, n, out.du) + || !solve_dense(std::move(matrix), rhs_dv, n, out.dv)) + { throw std::runtime_error("triangle_lagrange_basis: singular interpolation matrix"); - return basis; + } + return out; +} + +template +std::vector triangle_lagrange_basis(int order, + curving::NodeFamily node_family, + std::span bary) +{ + return triangle_lagrange_basis_data(order, node_family, bary).values; } template @@ -800,17 +1017,59 @@ std::vector eval_curved_edge_ref(const CurvedBoundaryEdge& edge, } template -std::vector eval_curved_face_ref(const CurvedBoundaryFace& face, - int order, - curving::NodeFamily node_family, - std::span coordinates) +std::vector eval_curved_edge_ref_derivative(const CurvedBoundaryEdge& edge, + int order, + curving::NodeFamily family, + T t_local) +{ + const bool same_orientation = + edge.zero_vertices[0] == edge.local_global_vertices[0] + && edge.zero_vertices[1] == edge.local_global_vertices[1]; + const bool reverse_orientation = + edge.zero_vertices[0] == edge.local_global_vertices[1] + && edge.zero_vertices[1] == edge.local_global_vertices[0]; + if (!same_orientation && !reverse_orientation) + throw std::runtime_error("eval_curved_edge_ref_derivative: edge orientation mismatch"); + const T sign = same_orientation ? T(1) : T(-1); + const T t = same_orientation ? t_local : T(1) - t_local; + const auto params = interpolation_parameters(order, family); + const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); + std::vector dx(static_cast(tdim), T(0)); + for (int i = 0; i <= order; ++i) + { + const T dLi = + sign * lagrange_basis_1d_derivative( + i, std::span(params.data(), params.size()), t); + for (int d = 0; d < tdim; ++d) + dx[static_cast(d)] += dLi * edge.state->ref_nodes[ + static_cast(i * tdim + d)]; + } + return dx; +} + +template +struct CurvedFaceEval +{ + std::vector point; + // Derivatives are stored column-major: du column, then dv column. + std::vector jacobian; +}; + +template +CurvedFaceEval eval_curved_face_ref_with_jacobian( + const CurvedBoundaryFace& face, + int order, + curving::NodeFamily node_family, + std::span coordinates) { const int nodes_per_face = (face.zero_face_type == cell::type::quadrilateral) ? (order + 1) * (order + 1) : (order + 1) * (order + 2) / 2; const int tdim = static_cast(face.state->ref_nodes.size()) / nodes_per_face; - std::vector x(static_cast(tdim), T(0)); + CurvedFaceEval out; + out.point.assign(static_cast(tdim), T(0)); + out.jacobian.assign(static_cast(2 * tdim), T(0)); if (face.zero_face_type == cell::type::quadrilateral) { @@ -822,29 +1081,55 @@ std::vector eval_curved_face_ref(const CurvedBoundaryFace& face, { const T Lj = lagrange_basis_1d( j, std::span(params.data(), params.size()), v); + const T dLj = lagrange_basis_1d_derivative( + j, std::span(params.data(), params.size()), v); for (int i = 0; i <= order; ++i) { const T Li = lagrange_basis_1d( i, std::span(params.data(), params.size()), u); + const T dLi = lagrange_basis_1d_derivative( + i, std::span(params.data(), params.size()), u); const T L = Li * Lj; for (int d = 0; d < tdim; ++d) - x[static_cast(d)] += L * face.state->ref_nodes[ + { + const T node_value = face.state->ref_nodes[ static_cast(node * tdim + d)]; + out.point[static_cast(d)] += L * node_value; + out.jacobian[static_cast(d)] += dLi * Lj * node_value; + out.jacobian[static_cast(tdim + d)] += Li * dLj * node_value; + } ++node; } } - return x; + return out; } - const auto basis = triangle_lagrange_basis(order, node_family, coordinates); - for (int node = 0; node < static_cast(basis.size()); ++node) + const auto basis = triangle_lagrange_basis_data(order, node_family, coordinates); + for (int node = 0; node < static_cast(basis.values.size()); ++node) { - const T L = basis[static_cast(node)]; + const T L = basis.values[static_cast(node)]; + const T dLdu = basis.du[static_cast(node)]; + const T dLdv = basis.dv[static_cast(node)]; for (int d = 0; d < tdim; ++d) - x[static_cast(d)] += L * face.state->ref_nodes[ + { + const T node_value = face.state->ref_nodes[ static_cast(node * tdim + d)]; + out.point[static_cast(d)] += L * node_value; + out.jacobian[static_cast(d)] += dLdu * node_value; + out.jacobian[static_cast(tdim + d)] += dLdv * node_value; + } } - return x; + return out; +} + +template +std::vector eval_curved_face_ref(const CurvedBoundaryFace& face, + int order, + curving::NodeFamily node_family, + std::span coordinates) +{ + return eval_curved_face_ref_with_jacobian( + face, order, node_family, coordinates).point; } template @@ -868,41 +1153,83 @@ bool edge_is_in_any_curved_face(const CurvedBoundaryEdge& edge, } template -std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, - std::span xi) +struct CurvedMapRefEval +{ + std::vector point; + // Derivatives d(point) / d(xi_c), column-major in local coordinates. + std::vector jacobian; +}; + +template +CurvedMapRefEval curved_map_ref_point_with_jacobian( + const LocalCurvedSimplexMap& map, + std::span xi) { const auto vertex_weights = cell_vertex_shape_weights(map.simplex_type, xi); - std::vector ref = straight_ref_from_cell_point(map, xi); + const auto vertex_gradients = cell_vertex_shape_gradients(map.simplex_type, xi); + CurvedMapRefEval out; + out.point = straight_ref_from_cell_point(map, xi); + out.jacobian = straight_ref_jacobian_from_cell_point(map, xi); constexpr T eps = T(64) * std::numeric_limits::epsilon(); if (map.dim == 1) { for (const auto& edge : map.curved_edges) { - const auto curved = eval_curved_edge_ref( + out.point = eval_curved_edge_ref( + edge, map.geometry_order, map.node_family, xi[0]); + const auto dcurved = eval_curved_edge_ref_derivative( edge, map.geometry_order, map.node_family, xi[0]); for (int d = 0; d < map.parent_tdim; ++d) - ref[static_cast(d)] = curved[static_cast(d)]; + out.jacobian[static_cast(d)] = + dcurved[static_cast(d)]; } - return ref; + return out; } for (const auto& face : map.curved_faces) { T s = T(0); std::array local_w = {}; + std::array local_dw = {}; + std::array ds = {}; for (int i = 0; i < face.num_local_vertices; ++i) { + const int lv = face.local_vertices[static_cast(i)]; local_w[static_cast(i)] = - vertex_weights[static_cast( - face.local_vertices[static_cast(i)])]; + vertex_weights[static_cast(lv)]; s += local_w[static_cast(i)]; + for (int c = 0; c < map.dim; ++c) + { + const T value = + vertex_gradients[static_cast(lv * map.dim + c)]; + local_dw[static_cast(i * map.dim + c)] = value; + ds[static_cast(c)] += value; + } } if (s <= eps) continue; + std::array normalized_w = {}; + std::array normalized_dw = {}; + for (int i = 0; i < face.num_local_vertices; ++i) + { + normalized_w[static_cast(i)] = + local_w[static_cast(i)] / s; + for (int c = 0; c < map.dim; ++c) + { + normalized_dw[static_cast(i * map.dim + c)] = + (local_dw[static_cast(i * map.dim + c)] * s + - local_w[static_cast(i)] + * ds[static_cast(c)]) + / (s * s); + } + } + std::array zero_w = {}; + std::array zero_dw = {}; std::array zero_uv = {}; + std::array zero_duv = {}; for (int zi = 0; zi < face.num_zero_vertices; ++zi) { for (int li = 0; li < face.num_local_vertices; ++li) @@ -911,17 +1238,29 @@ std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, == map.vertices[static_cast( face.local_vertices[static_cast(li)])]) { - const T w = local_w[static_cast(li)] / s; if (face.zero_face_type == cell::type::quadrilateral) { const T u = (zi == 1 || zi == 3) ? T(1) : T(0); const T v = (zi == 2 || zi == 3) ? T(1) : T(0); - zero_uv[0] += w * u; - zero_uv[1] += w * v; + zero_uv[0] += normalized_w[static_cast(li)] * u; + zero_uv[1] += normalized_w[static_cast(li)] * v; + for (int c = 0; c < map.dim; ++c) + { + zero_duv[static_cast(c)] += + normalized_dw[static_cast(li * map.dim + c)] * u; + zero_duv[static_cast(map.dim + c)] += + normalized_dw[static_cast(li * map.dim + c)] * v; + } } else { - zero_w[static_cast(zi)] = w; + zero_w[static_cast(zi)] = + normalized_w[static_cast(li)]; + for (int c = 0; c < map.dim; ++c) + { + zero_dw[static_cast(zi * map.dim + c)] = + normalized_dw[static_cast(li * map.dim + c)]; + } } } } @@ -931,20 +1270,61 @@ std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, (face.zero_face_type == cell::type::quadrilateral) ? std::span(zero_uv.data(), zero_uv.size()) : std::span(zero_w.data(), zero_w.size()); - const auto curved = eval_curved_face_ref( + const auto curved = eval_curved_face_ref_with_jacobian( face, map.geometry_order, map.node_family, face_coordinates); std::vector straight(static_cast(map.parent_tdim), T(0)); + std::vector straight_jacobian( + static_cast(map.dim * map.parent_tdim), T(0)); for (int li = 0; li < face.num_local_vertices; ++li) { const int lv = face.local_vertices[static_cast(li)]; - const T w = local_w[static_cast(li)] / s; + const T w = normalized_w[static_cast(li)]; for (int d = 0; d < map.parent_tdim; ++d) + { straight[static_cast(d)] += w * map.ref_vertex_coords[ static_cast(lv * map.parent_tdim + d)]; + for (int c = 0; c < map.dim; ++c) + { + straight_jacobian[static_cast( + c * map.parent_tdim + d)] += + normalized_dw[static_cast(li * map.dim + c)] + * map.ref_vertex_coords[ + static_cast(lv * map.parent_tdim + d)]; + } + } } for (int d = 0; d < map.parent_tdim; ++d) - ref[static_cast(d)] += s * (curved[static_cast(d)] - - straight[static_cast(d)]); + { + const T diff = + curved.point[static_cast(d)] + - straight[static_cast(d)]; + out.point[static_cast(d)] += s * diff; + for (int c = 0; c < map.dim; ++c) + { + T dcurved = T(0); + if (face.zero_face_type == cell::type::quadrilateral) + { + dcurved = + curved.jacobian[static_cast(d)] + * zero_duv[static_cast(c)] + + curved.jacobian[static_cast(map.parent_tdim + d)] + * zero_duv[static_cast(map.dim + c)]; + } + else + { + dcurved = + curved.jacobian[static_cast(d)] + * zero_dw[static_cast(map.dim + c)] + + curved.jacobian[static_cast(map.parent_tdim + d)] + * zero_dw[static_cast(2 * map.dim + c)]; + } + const std::size_t jd = + static_cast(c * map.parent_tdim + d); + out.jacobian[jd] += + ds[static_cast(c)] * diff + + s * (dcurved - straight_jacobian[jd]); + } + } } for (const auto& edge : map.curved_edges) @@ -962,7 +1342,10 @@ std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, const T t = b / s; const auto curved = eval_curved_edge_ref( edge, map.geometry_order, map.node_family, t); + const auto curved_dt = eval_curved_edge_ref_derivative( + edge, map.geometry_order, map.node_family, t); std::vector straight(static_cast(map.parent_tdim), T(0)); + std::vector straight_dt(static_cast(map.parent_tdim), T(0)); for (int d = 0; d < map.parent_tdim; ++d) { const T x0 = map.ref_vertex_coords[ @@ -970,12 +1353,37 @@ std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, const T x1 = map.ref_vertex_coords[ static_cast(edge.local_vertices[1] * map.parent_tdim + d)]; straight[static_cast(d)] = (T(1) - t) * x0 + t * x1; - ref[static_cast(d)] += s * (curved[static_cast(d)] - - straight[static_cast(d)]); + straight_dt[static_cast(d)] = x1 - x0; + const T diff = + curved[static_cast(d)] + - straight[static_cast(d)]; + out.point[static_cast(d)] += s * diff; + for (int c = 0; c < map.dim; ++c) + { + const T da = + vertex_gradients[static_cast( + edge.local_vertices[0] * map.dim + c)]; + const T db = + vertex_gradients[static_cast( + edge.local_vertices[1] * map.dim + c)]; + const T ds_edge = da + db; + const T dt = (db * s - b * ds_edge) / (s * s); + out.jacobian[static_cast(c * map.parent_tdim + d)] += + ds_edge * diff + + s * (curved_dt[static_cast(d)] + - straight_dt[static_cast(d)]) * dt; + } } } - return ref; + return out; +} + +template +std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, + std::span xi) +{ + return curved_map_ref_point_with_jacobian(map, xi).point; } template @@ -1158,136 +1566,57 @@ std::vector> subdivide_child_simplex(cell::type simplex_type, } template -T finite_difference_measure(const LocalCurvedSimplexMap& map, - std::span xi) +T curved_map_measure(const LocalCurvedSimplexMap& map, + std::span xi) { - const int d = map.dim; - constexpr T h = T(1e-6); - std::vector J(static_cast(map.gdim * d), T(0)); - for (int c = 0; c < d; ++c) - { - std::vector xp(xi.begin(), xi.end()); - std::vector xm(xi.begin(), xi.end()); - xp[static_cast(c)] += h; - xm[static_cast(c)] -= h; - const auto pp = curved_map_physical_point(map, std::span(xp.data(), xp.size())); - const auto pm = curved_map_physical_point(map, std::span(xm.data(), xm.size())); - for (int r = 0; r < map.gdim; ++r) - J[static_cast(c * map.gdim + r)] = - (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); - } - - if (d == 1) - { - T n2 = T(0); - for (int r = 0; r < map.gdim; ++r) - n2 += J[static_cast(r)] * J[static_cast(r)]; - return std::sqrt(n2); - } - if (d == map.gdim) - { - if (d == 2) - return J[0] * J[3] - J[2] * J[1]; - return J[0] * (J[4] * J[8] - J[7] * J[5]) - - J[3] * (J[1] * J[8] - J[7] * J[2]) - + J[6] * (J[1] * J[5] - J[4] * J[2]); - } - - T G[9] = {}; - for (int i = 0; i < d; ++i) - for (int j = 0; j < d; ++j) - for (int r = 0; r < map.gdim; ++r) - G[i * d + j] += J[static_cast(i * map.gdim + r)] - * J[static_cast(j * map.gdim + r)]; - if (d == 2) - return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); - return T(0); + const auto ref_eval = curved_map_ref_point_with_jacobian(map, xi); + const auto physical_J = compose_ref_to_physical_jacobian( + map, + std::span(ref_eval.jacobian.data(), ref_eval.jacobian.size())); + return measure_from_jacobian( + map.dim, + map.gdim, + std::span(physical_J.data(), physical_J.size())); } template -T finite_difference_straight_measure(const LocalCurvedSimplexMap& map, - std::span xi) +T straight_map_measure(const LocalCurvedSimplexMap& map, + std::span xi) { - const int d = map.dim; - constexpr T h = T(1e-6); - std::vector J(static_cast(map.gdim * d), T(0)); - auto straight_physical = [&](std::span xref) - { - const auto ref = straight_ref_from_cell_point(map, xref); - return push_parent_ref_to_physical( - map, std::span(ref.data(), ref.size())); - }; - - for (int c = 0; c < d; ++c) - { - std::vector xp(xi.begin(), xi.end()); - std::vector xm(xi.begin(), xi.end()); - xp[static_cast(c)] += h; - xm[static_cast(c)] -= h; - const auto pp = straight_physical(std::span(xp.data(), xp.size())); - const auto pm = straight_physical(std::span(xm.data(), xm.size())); - for (int r = 0; r < map.gdim; ++r) - J[static_cast(c * map.gdim + r)] = - (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); - } - - if (d == 1) - { - T n2 = T(0); - for (int r = 0; r < map.gdim; ++r) - n2 += J[static_cast(r)] * J[static_cast(r)]; - return std::sqrt(n2); - } - if (d == map.gdim) - { - if (d == 2) - return std::abs(J[0] * J[3] - J[2] * J[1]); - return std::abs(J[0] * (J[4] * J[8] - J[7] * J[5]) - - J[3] * (J[1] * J[8] - J[7] * J[2]) - + J[6] * (J[1] * J[5] - J[4] * J[2])); - } - - T G[9] = {}; - for (int i = 0; i < d; ++i) - for (int j = 0; j < d; ++j) - for (int r = 0; r < map.gdim; ++r) - G[i * d + j] += J[static_cast(i * map.gdim + r)] - * J[static_cast(j * map.gdim + r)]; - if (d == 2) - return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); - return T(0); + const auto ref_J = straight_ref_jacobian_from_cell_point(map, xi); + const auto physical_J = compose_ref_to_physical_jacobian( + map, + std::span(ref_J.data(), ref_J.size())); + return std::abs(measure_from_jacobian( + map.dim, + map.gdim, + std::span(physical_J.data(), physical_J.size()))); } template -T finite_difference_child_measure(cell::type cell_type, - std::span child_vertices, - std::span xi) +T child_map_measure(cell::type cell_type, + std::span child_vertices, + std::span xi) { const int d = cell::get_tdim(cell_type); - constexpr T h = T(1e-6); + const int nv = cell::get_num_vertices(cell_type); + const auto gradients = cell_vertex_shape_gradients(cell_type, xi); std::vector J(static_cast(d * d), T(0)); for (int c = 0; c < d; ++c) { - std::vector xp(xi.begin(), xi.end()); - std::vector xm(xi.begin(), xi.end()); - xp[static_cast(c)] += h; - xm[static_cast(c)] -= h; - const auto pp = map_child_xi_to_parent_xi( - cell_type, child_vertices, std::span(xp.data(), xp.size())); - const auto pm = map_child_xi_to_parent_xi( - cell_type, child_vertices, std::span(xm.data(), xm.size())); - for (int r = 0; r < d; ++r) - J[static_cast(c * d + r)] = - (pp[static_cast(r)] - pm[static_cast(r)]) / (T(2) * h); + for (int v = 0; v < nv; ++v) + { + const T dNv = gradients[static_cast(v * d + c)]; + for (int r = 0; r < d; ++r) + { + J[static_cast(c * d + r)] += + dNv * child_vertices[static_cast(v * d + r)]; + } + } } - if (d == 1) - return std::abs(J[0]); - if (d == 2) - return std::abs(J[0] * J[3] - J[2] * J[1]); - return std::abs(J[0] * (J[4] * J[8] - J[7] * J[5]) - - J[3] * (J[1] * J[8] - J[7] * J[2]) - + J[6] * (J[1] * J[5] - J[4] * J[2])); + return std::abs(measure_from_jacobian( + d, d, std::span(J.data(), J.size()))); } template @@ -1324,7 +1653,7 @@ bool curved_map_valid(const LocalCurvedSimplexMap& map) constexpr T tol = T(1e-12); for (const auto& sample : samples) { - const T measure = finite_difference_measure( + const T measure = curved_map_measure( map, std::span(sample.data(), sample.size())); if (!(std::abs(measure) > tol)) return false; @@ -1374,7 +1703,7 @@ bool curved_child_map_valid(const LocalCurvedSimplexMap& map, map.simplex_type, child_vertices, std::span(sample.data(), sample.size())); - const T measure = finite_difference_measure( + const T measure = curved_map_measure( map, std::span(xi_parent.data(), xi_parent.size())); if (!(std::abs(measure) > tol)) return false; @@ -2674,17 +3003,17 @@ void append_curved_child_simplex_quadrature(quadrature::QuadratureRules& rule } rules._points.insert(rules._points.end(), ref.begin(), ref.end()); - const T child_measure = finite_difference_child_measure( + const T child_measure = child_map_measure( map.simplex_type, child_vertices, xi_child); T measure = T(0); if (valid) { - measure = std::abs(finite_difference_measure( + measure = std::abs(curved_map_measure( map, std::span(xi.data(), xi.size()))) * child_measure; } else { - measure = finite_difference_straight_measure( + measure = straight_map_measure( map, std::span(xi.data(), xi.size())) * child_measure; } rules._weights.push_back(ref_rule._weights[static_cast(q)] * measure); diff --git a/cpp/src/quadrature.cpp b/cpp/src/quadrature.cpp index a6cea0d..3f78186 100644 --- a/cpp/src/quadrature.cpp +++ b/cpp/src/quadrature.cpp @@ -603,6 +603,10 @@ std::vector physical_points( const int coff = offset[ci]; const type ctype = map_vtk_type_to_cell_type( static_cast(vtk_type[ci])); + if (get_tdim(ctype) != tdim) + throw std::invalid_argument( + "physical_points: quadrature point dimension does not match " + "the supplied VTK parent cell type"); const int nv = get_num_vertices(ctype); // Gather physical vertices for this cell @@ -615,22 +619,8 @@ std::vector physical_points( tl_phys_verts[j * 3 + 2] = points[vid * 3 + 2]; } - // Push ref → phys for all nq quadrature points of this rule. - // For tdim == gdim == 3 use the existing square-Jacobian routine; - // for tdim < 3 (embedded cells) apply the affine map manually: - // x_phys = v0 + sum_k (v_{col_k} - v0) * X_ref[k] - if (tdim == 3) - { - push_forward_affine( - ctype, - tl_phys_verts, - 3, - std::span(rules._points.data() + q_begin * tdim, - static_cast(nq) * tdim), - std::span(out.data() + q_begin * 3, - static_cast(nq) * 3)); - } - else + // Push parent reference points to physical coordinates with the same + // affine column convention used by the cut-cell maps. { const auto cols = jacobian_col_indices(ctype); const T* v0 = tl_phys_verts.data(); diff --git a/cpp/src/refine_cell.cpp b/cpp/src/refine_cell.cpp index 0777fea..81b4926 100644 --- a/cpp/src/refine_cell.cpp +++ b/cpp/src/refine_cell.cpp @@ -522,12 +522,18 @@ void rebuild_leaf_cell_provenance( adapt_cell.entity_host_cell_id[tdim].push_back( static_cast(source_c)); - const cell::type host_type = - (meta_c >= 0 && meta_c < static_cast(old_host_cell_types.size())) - ? old_host_cell_types[static_cast(meta_c)] - : ((meta_c >= 0 && meta_c < static_cast(old_cell_types.size())) - ? old_cell_types[static_cast(meta_c)] - : adapt_cell.entity_types[tdim][static_cast(c)]); + cell::type host_type = + adapt_cell.entity_types[tdim][static_cast(c)]; + if (meta_c >= 0 + && meta_c < static_cast(old_host_cell_types.size())) + { + host_type = old_host_cell_types[static_cast(meta_c)]; + } + else if (meta_c >= 0 + && meta_c < static_cast(old_cell_types.size())) + { + host_type = old_cell_types[static_cast(meta_c)]; + } adapt_cell.entity_host_cell_type[tdim].push_back(host_type); adapt_cell.entity_host_face_id[tdim].push_back(std::int32_t(-1)); const int source_level_set = @@ -631,7 +637,6 @@ void apply_topology_update_preserve_certification( rebuild_zero_entity_inventory(adapt_cell); } - // ===================================================================== // Invalidation helpers // ===================================================================== diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index d79d5be..6e53f41 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -3317,6 +3317,46 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::cast(self, nb::rv_policy::reference)); }, nb::rv_policy::reference_internal) + .def_prop_ro( + "face_to_cell_connectivity", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.connectivity[2][self.tdim].indices.data(), + {self.connectivity[2][self.tdim].indices.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "face_to_cell_offsets", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.connectivity[2][self.tdim].offsets.data(), + {self.connectivity[2][self.tdim].offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_to_face_connectivity", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.connectivity[self.tdim][2].indices.data(), + {self.connectivity[self.tdim][2].indices.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "cell_to_face_offsets", + [](const AdaptCellT& self) + { + return nb::ndarray( + self.connectivity[self.tdim][2].offsets.data(), + {self.connectivity[self.tdim][2].offsets.size()}, + nb::cast(self, nb::rv_policy::reference)); + }, + nb::rv_policy::reference_internal) .def_prop_ro( "cell_types", [](const AdaptCellT& self) diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index ce95f8e..da02724 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -618,11 +618,42 @@ def test_triangulated_zero_quad_faces_curve_across_artificial_diagonal(self): np.asarray(graph_nodes["node_index"], dtype=np.int32)[face_interior_nodes] > 0 )) + def test_zero_face_final_incidence_uses_face_to_cell_connectivity(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.5, + degree=1, + name="phi", + ) + + result = cutcells.ho_cut( + mesh, + ls, + graph_max_refinements=0, + min_level_set_gradient_host_alignment=0.0, + ) + adapt = result.adapt_cell(0) + + zero_dims = np.asarray(adapt.zero_entity_dim, dtype=np.int32) + zero_ids = np.asarray(adapt.zero_entity_id, dtype=np.int32) + zero_face_ids = zero_ids[zero_dims == 2] + self.assertEqual(zero_face_ids.size, 1) + + face_id = int(zero_face_ids[0]) + face_to_cell = np.asarray(adapt.face_to_cell_connectivity, dtype=np.int32) + face_offsets = np.asarray(adapt.face_to_cell_offsets, dtype=np.int32) + incident_cells = face_to_cell[face_offsets[face_id]:face_offsets[face_id + 1]] + self.assertEqual(incident_cells.size, 2) + + cell_types = np.asarray(adapt.cell_types, dtype=np.int32) + self.assertEqual(cell_types[incident_cells].tolist(), [6, 6]) + def test_curving_accepts_near_zero_multi_level_set_node_without_projection(self): mesh = _single_triangle_mesh() def phi(X): - return X[0] + X[1] - 0.25 + 5.12e-11 * X[0] * X[1] + return X[0] + X[1] - 0.25 + 5.12e-13 * X[0] * X[1] ls0 = cutcells.create_level_set(mesh, phi, degree=2, name="phi") ls1 = cutcells.create_level_set(mesh, phi, degree=2, name="psi") diff --git a/python/tests/test_runtime_quadrature.py b/python/tests/test_runtime_quadrature.py index 54d9607..f5f3f31 100644 --- a/python/tests/test_runtime_quadrature.py +++ b/python/tests/test_runtime_quadrature.py @@ -135,6 +135,22 @@ def test_cut_tet_physical_points(): assert phys.reshape(-1, 3).shape[1] == 3 +def test_physical_points_rejects_non_vtk_parent_type_ids(): + """physical_points expects VTK parent ids, not internal CutCells type ids.""" + pts_arr = np.array(TET_PTS, dtype=np.float64).flatten() + ls = np.full(4, -1.0, dtype=np.float64) + conn = np.array([0, 1, 2, 3], dtype=np.int32) + offs = np.array([0, 4], dtype=np.int32) + vtk_tetra = np.array([10], dtype=np.int32) + internal_tetra = np.array([3], dtype=np.int32) + rules = cutcells.runtime_quadrature( + ls, pts_arr, conn, offs, vtk_tetra, "phi<0", False, 2 + ) + + with pytest.raises(ValueError, match="dimension does not match"): + cutcells.physical_points(rules, pts_arr, conn, offs, internal_tetra) + + # --------------------------------------------------------------------------- # Fully outside → empty result # --------------------------------------------------------------------------- From 2707290c182aaa9e421d6993e1cceb1f988e03f2 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 16 May 2026 00:49:17 +0200 Subject: [PATCH 21/23] clean up curving to merge without curving for now for new release --- cpp/src/CMakeLists.txt | 4 - cpp/src/adapt_cell.h | 6 +- cpp/src/cell_certification.cpp | 4234 +------------- cpp/src/cell_certification.h | 197 +- cpp/src/curving.cpp | 4952 ----------------- cpp/src/curving.h | 234 - cpp/src/edge_certification.cpp | 172 +- cpp/src/edge_certification.h | 25 +- cpp/src/geometric_quantity.h | 947 ---- cpp/src/graph_criteria.h | 640 --- cpp/src/ho_cut_mesh.cpp | 113 +- cpp/src/ho_cut_mesh.h | 15 +- cpp/src/ho_mesh_part_output.cpp | 3685 ++---------- cpp/src/ho_mesh_part_output.h | 39 +- cpp/src/tangent_shift.h | 873 --- cpp/src/write_vtk.cpp | 11 +- cpp/src/write_vtk.h | 12 +- python/cutcells/__init__.py | 17 +- python/cutcells/wrapper.cpp | 1258 +---- python/demo/demo_level_set_ho_vtk.py | 95 - ..._meshview_ho_cut.py => demo_mesh_parts.py} | 0 python/demo/demo_meshview.py | 271 - .../demo/demo_meshview_ho_cut_triangulated.py | 526 -- python/demo/demo_sphere_p2_cut_vtu.py | 254 - python/demo/demo_two_level_sets.py | 139 + python/tests/test_certification_refinement.py | 407 +- .../tests/test_geometric_quantity_header.py | 114 - python/tests/test_graph_criteria_header.py | 217 - python/tests/test_tangent_shift_header.py | 207 - .../test_tetra_triangulation_analysis.py | 13 +- 30 files changed, 1029 insertions(+), 18648 deletions(-) delete mode 100644 cpp/src/curving.cpp delete mode 100644 cpp/src/curving.h delete mode 100644 cpp/src/geometric_quantity.h delete mode 100644 cpp/src/graph_criteria.h delete mode 100644 cpp/src/tangent_shift.h delete mode 100644 python/demo/demo_level_set_ho_vtk.py rename python/demo/{demo_meshview_ho_cut.py => demo_mesh_parts.py} (100%) delete mode 100644 python/demo/demo_meshview.py delete mode 100644 python/demo/demo_meshview_ho_cut_triangulated.py delete mode 100644 python/demo/demo_sphere_p2_cut_vtu.py create mode 100644 python/demo/demo_two_level_sets.py delete mode 100644 python/tests/test_geometric_quantity_header.py delete mode 100644 python/tests/test_graph_criteria_header.py delete mode 100644 python/tests/test_tangent_shift_header.py diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 292b8c7..3b6f073 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -35,12 +35,9 @@ set(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.h ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.h - ${CMAKE_CURRENT_SOURCE_DIR}/curving.h ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.h ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.h ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.h - ${CMAKE_CURRENT_SOURCE_DIR}/geometric_quantity.h - ${CMAKE_CURRENT_SOURCE_DIR}/tangent_shift.h ${CMAKE_CURRENT_SOURCE_DIR}/refine_cell.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature.h ${CMAKE_CURRENT_SOURCE_DIR}/quadrature_tables.h @@ -69,7 +66,6 @@ target_sources(cutcells PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/adapt_cell.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ho_cut_mesh.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ho_mesh_part_output.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/curving.cpp ${CMAKE_CURRENT_SOURCE_DIR}/selection_expr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/edge_certification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cell_certification.cpp diff --git a/cpp/src/adapt_cell.h b/cpp/src/adapt_cell.h index a0f7760..d376fd4 100644 --- a/cpp/src/adapt_cell.h +++ b/cpp/src/adapt_cell.h @@ -60,8 +60,8 @@ enum class CellRefinementReason : std::uint8_t none = 0, green_edge = 1, red_cell = 2, - graph_green_edge = 3, - graph_red_cell = 4, + targeted_green_edge = 3, + targeted_red_cell = 4, cut_level_set = 5 }; @@ -200,7 +200,7 @@ struct AdaptCell /// Structured embedding-host provenance for entities. For zero entities /// created by a cut, this is the uncut AdaptCell leaf that embeds the - /// interface and bounds curving/projection. It is deliberately not the + /// interface. It is deliberately not the /// accepted cut output cell that happens to contain the entity. /// For zero edges on a zero face boundary, host_face_id is filled by the /// face context when available. diff --git a/cpp/src/cell_certification.cpp b/cpp/src/cell_certification.cpp index 695f5c4..cf8a496 100644 --- a/cpp/src/cell_certification.cpp +++ b/cpp/src/cell_certification.cpp @@ -7,12 +7,10 @@ #include "bernstein.h" #include "cell_flags.h" #include "cell_topology.h" -#include "curving.h" #include "cut_cell.h" #include "cut_tetrahedron.h" #include "cut_triangle.h" #include "edge_certification.h" -#include "geometric_quantity.h" #include "mapping.h" #include "reference_cell.h" #include "refine_cell.h" @@ -330,6 +328,41 @@ void gather_adapt_edge_bernstein(const AdaptCell& adapt_cell, xi_a, xi_b, edge_coeffs); } +template +bool linear_one_root_parameter_from_endpoint_values(T phi0, + T phi1, + T zero_tol, + T& root_t) +{ + const bool left_zero = std::fabs(phi0) <= zero_tol; + const bool right_zero = std::fabs(phi1) <= zero_tol; + + root_t = T(0); + if (left_zero && right_zero) + return false; + if (left_zero) + return true; + if (right_zero) + { + root_t = T(1); + return true; + } + + const bool brackets_zero = (phi0 < T(0) && phi1 > T(0)) + || (phi0 > T(0) && phi1 < T(0)); + if (!brackets_zero) + return false; + + const T denom = phi0 - phi1; + const T denom_tol = + std::max(zero_tol, T(64) * std::numeric_limits::epsilon()); + if (std::fabs(denom) <= denom_tol) + return false; + + root_t = std::clamp(phi0 / denom, T(0), T(1)); + return true; +} + /// Returns true if the Bernstein expansion has a sign-definite partial /// derivative in at least one reference direction on the subcell. /// A monotone function with no boundary root cannot have an interior zero. @@ -450,15 +483,29 @@ int ensure_one_root_vertex_on_edge(AdaptCell& adapt_cell, if (adapt_cell.get_edge_root_tag(level_set_id, edge_id) != EdgeRootTag::one_root) return -1; - std::vector edge_coeffs; - gather_adapt_edge_bernstein(adapt_cell, ls_cell, edge_id, edge_coeffs); + (void)sign_tol; + (void)edge_max_depth; T root_t = T(0); - if (!locate_one_root_parameter(std::span(edge_coeffs), - zero_tol, sign_tol, edge_max_depth, root_t)) + bool has_linear_root = adapt_cell.edge_one_root_has_value[idx] != 0; + if (has_linear_root) + { + root_t = adapt_cell.edge_one_root_param[idx]; + } + else + { + std::vector edge_coeffs; + gather_adapt_edge_bernstein(adapt_cell, ls_cell, edge_id, edge_coeffs); + has_linear_root = + edge_coeffs.size() >= 2 + && linear_one_root_parameter_from_endpoint_values( + edge_coeffs.front(), edge_coeffs.back(), zero_tol, root_t); + } + + if (!has_linear_root) { throw std::runtime_error( - "ensure_one_root_vertex_on_edge: failed to localize unique edge root"); + "ensure_one_root_vertex_on_edge: failed to compute linear edge intersection"); } auto verts = adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; @@ -636,7 +683,7 @@ void append_ready_cut_part_cells( { // Straight cut vertices lie on the straight interface by // construction. They should participate in phi=0 - // extraction even when the curved level set evaluated at + // extraction even when the polynomial level set evaluated at // this affine point is not exactly zero. set_vertex_sign_for_level_set( adapt_cell, vertex_id, level_set_id, T(0), zero_tol); @@ -1353,3279 +1400,149 @@ void fill_all_vertex_signs_from_level_set(AdaptCell& adapt_cell, } } -template -std::span point_span(const std::vector& points, - int point_id, - int dim) -{ - return std::span( - points.data() - + static_cast(point_id) - * static_cast(dim), - static_cast(dim)); -} +// ===================================================================== +// process_ready_to_cut_cells +// ===================================================================== -template -geom::ParentEntity common_parent_entity_for_points( - cell::type parent_cell_type, - const std::vector& points, - int dim, - T tol) +template +void process_ready_to_cut_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + T zero_tol, + T sign_tol, + int edge_max_depth, + bool triangulate_cut_parts) { - const int npoints = static_cast( - points.size() / static_cast(dim)); - - if (npoints == 0) - return {-1, -1}; - - const auto edges = cell::edges(parent_cell_type); - for (int e = 0; e < static_cast(edges.size()); ++e) - { - bool all_on_edge = true; - for (int i = 0; i < npoints; ++i) - all_on_edge = all_on_edge - && geom::point_on_parent_edge( - parent_cell_type, e, point_span(points, i, dim), tol); - if (all_on_edge) - return {1, e}; - } - - if (cell::get_tdim(parent_cell_type) == 3) - { - for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) - { - bool all_on_face = true; - for (int i = 0; i < npoints; ++i) - all_on_face = all_on_face - && geom::point_on_parent_face( - parent_cell_type, f, point_span(points, i, dim), tol); - if (all_on_face) - return {2, f}; - } - } - - return {cell::get_tdim(parent_cell_type), -1}; -} + const int tdim = adapt_cell.tdim; + const int n_cells = adapt_cell.n_entities(tdim); -template -geom::ParentEntity common_parent_face_or_cell_for_points( - cell::type parent_cell_type, - const std::vector& points, - int dim, - T tol) -{ - const int npoints = static_cast( - points.size() / static_cast(dim)); - if (cell::get_tdim(parent_cell_type) == 3) + bool has_ready = false; + for (int c = 0; c < n_cells; ++c) { - for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) + if (adapt_cell.get_cell_cert_tag(level_set_id, c) == CellCertTag::ready_to_cut) { - bool all_on_face = true; - for (int i = 0; i < npoints; ++i) - all_on_face = all_on_face - && geom::point_on_parent_face( - parent_cell_type, f, point_span(points, i, dim), tol); - if (all_on_face) - return {2, f}; + has_ready = true; + break; } } - return {cell::get_tdim(parent_cell_type), -1}; -} - -template -void mark_graph_failure(ReadyCellGraphDiagnostics& diagnostics, - int cell_id, - graph_criteria::FailureReason reason) -{ - diagnostics.accepted = false; - ++diagnostics.failed_checks; - if (diagnostics.first_failed_cell < 0) - { - diagnostics.first_failed_cell = cell_id; - diagnostics.first_failure_reason = reason; - } -} - -template -void mark_graph_refinement_request(ReadyCellGraphDiagnostics& diagnostics, - int entity_dim, - int entity_id) -{ - if (entity_dim < 0 || entity_id < 0) - return; - if (diagnostics.first_requested_refinement_entity_dim < 0) - { - diagnostics.first_requested_refinement_entity_dim = entity_dim; - diagnostics.first_requested_refinement_entity_id = entity_id; - } -} - -template -void mark_graph_refinement_point(ReadyCellGraphDiagnostics& diagnostics, - std::span point) -{ - if (!diagnostics.first_requested_refinement_point.empty() - || point.empty()) - { + if (!has_ready) return; - } - diagnostics.first_requested_refinement_point.assign( - point.begin(), point.end()); -} - -template -void observe_graph_direction(ReadyCellGraphDiagnostics& diagnostics, - const graph_criteria::DirectionReport& report) -{ - diagnostics.min_true_transversality = - std::min(diagnostics.min_true_transversality, - report.metrics.true_transversality); - diagnostics.min_host_normal_alignment = - std::min(diagnostics.min_host_normal_alignment, - report.metrics.host_normal_alignment); - diagnostics.max_drift_amplification = - std::max(diagnostics.max_drift_amplification, - report.metrics.drift_amplification); - diagnostics.max_relative_correction_distance = - std::max(diagnostics.max_relative_correction_distance, - report.metrics.relative_correction_distance); - diagnostics.max_relative_tangential_shift = - std::max(diagnostics.max_relative_tangential_shift, - report.metrics.relative_tangential_shift); -} - -template -void observe_graph_node(ReadyCellGraphDiagnostics& diagnostics, - const GraphNodeDiagnostics& node) -{ - diagnostics.nodes.push_back(node); - if (std::isfinite(node.level_set_gradient_host_alignment)) - { - diagnostics.min_level_set_gradient_host_alignment = - std::min(diagnostics.min_level_set_gradient_host_alignment, - node.level_set_gradient_host_alignment); - } - if (!node.accepted) - { - mark_graph_refinement_request( - diagnostics, - node.requested_refinement_entity_dim, - node.requested_refinement_entity_id); - } -} -template -void observe_failed_graph_projection( - ReadyCellGraphDiagnostics& diagnostics, - const curving::ProjectionDiagnostic& projection) -{ - if (!diagnostics.first_failed_projection_seed.empty()) - return; - diagnostics.first_failed_projection_seed = projection.seed; - diagnostics.first_failed_projection_direction = projection.direction; - diagnostics.first_failed_projection_clip_lo = projection.clip_lo; - diagnostics.first_failed_projection_clip_hi = projection.clip_hi; - diagnostics.first_failed_projection_root_t = projection.root_t; -} + const std::vector old_types = adapt_cell.entity_types[tdim]; + const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; + const auto edge_lookup = build_leaf_edge_lookup(adapt_cell); -template -void observe_failed_face_orientation( - ReadyCellGraphDiagnostics& diagnostics, - const graph_criteria::FaceQualityReport& quality) -{ - if (diagnostics.first_failed_face_triangle_index < 0) - { - diagnostics.first_failed_face_triangle_index = - quality.failed_triangle_index; - diagnostics.first_failed_face_area_ratio = - quality.failed_surface_jacobian_ratio; - } -} + EntityAdjacency new_cells; + new_cells.offsets.push_back(0); + std::vector new_types; + std::vector old_cell_ids_for_new_cells; + std::vector source_cell_ids_for_new_cells; + std::vector refinement_reasons_for_new_cells; + std::vector explicit_current_ls_tags; -template -int find_adapt_edge_by_vertices(const AdaptCell& adapt_cell, int a, int b) -{ - if (a < 0 || b < 0) - return -1; - const int n_edges = adapt_cell.n_entities(1); - for (int e = 0; e < n_edges; ++e) + for (int c = 0; c < n_cells; ++c) { - auto ev = adapt_cell.entity_to_vertex[1][static_cast(e)]; - if (ev.size() != 2) - continue; - const int e0 = static_cast(ev[0]); - const int e1 = static_cast(ev[1]); - if ((e0 == a && e1 == b) || (e0 == b && e1 == a)) - return e; - } - return -1; -} - -template -int graph_refinement_edge_from_failed_face_segment( - const AdaptCell& adapt_cell, - int a, - int b) -{ - int edge_id = find_adapt_edge_by_vertices(adapt_cell, a, b); - if (edge_id >= 0) - return edge_id; + auto old_cell_vertices = old_cells[static_cast(c)]; + const cell::type leaf_cell_type = old_types[static_cast(c)]; - auto source_edge = [&](int v) -> int - { - if (v < 0 - || v >= static_cast(adapt_cell.vertex_source_edge_id.size())) + if (adapt_cell.get_cell_cert_tag(level_set_id, c) != CellCertTag::ready_to_cut) { - return -1; + std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); + append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); + explicit_current_ls_tags.push_back(CellCertTag::not_classified); + continue; } - const int source = adapt_cell.vertex_source_edge_id[static_cast(v)]; - return (source >= 0 && source < adapt_cell.n_entities(1)) ? source : -1; - }; - - edge_id = source_edge(a); - if (edge_id >= 0) - return edge_id; - return source_edge(b); -} - -template -T graph_alignment_to_host_normal(std::span direction, - std::span host_normal, - T tol) -{ - if (direction.size() != host_normal.size()) - return std::numeric_limits::quiet_NaN(); - const auto alignment = geom::alignment(direction, host_normal, tol); - if (alignment.degenerate) - return T(0); - return std::fabs(alignment.cosine); -} - -template -T graph_angle_to_tangent_degrees(T host_alignment) -{ - if (!std::isfinite(host_alignment)) - return std::numeric_limits::quiet_NaN(); - const T clipped = std::clamp(host_alignment, T(0), T(1)); - const T pi = std::acos(T(-1)); - return std::asin(clipped) * T(180) / pi; -} -template -bool solve_graph_dense_small(std::vector A, - std::vector b, - int n, - std::vector& x, - T tol) -{ - if (n <= 0 || static_cast(A.size()) != n * n - || static_cast(b.size()) != n) - { - return false; - } - - for (int k = 0; k < n; ++k) - { - int pivot = k; - T pivot_abs = std::fabs(A[static_cast(k * n + k)]); - for (int i = k + 1; i < n; ++i) + if (leaf_cell_type != cell::type::triangle + && leaf_cell_type != cell::type::tetrahedron) { - const T candidate = - std::fabs(A[static_cast(i * n + k)]); - if (candidate > pivot_abs) - { - pivot = i; - pivot_abs = candidate; - } + throw std::runtime_error( + "process_ready_to_cut_cells: ready_to_cut only implemented for triangle and tetrahedron leaves"); } - if (pivot_abs <= tol) - return false; - if (pivot != k) + std::vector vertex_coords( + static_cast(old_cell_vertices.size() * tdim), T(0)); + for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) { - for (int j = k; j < n; ++j) + const int gv = old_cell_vertices[j]; + for (int d = 0; d < tdim; ++d) { - std::swap(A[static_cast(k * n + j)], - A[static_cast(pivot * n + j)]); + vertex_coords[static_cast(j * tdim + d)] = + adapt_cell.vertex_coords[static_cast(gv * tdim + d)]; } - std::swap(b[static_cast(k)], - b[static_cast(pivot)]); } + const std::vector ls_values = + gather_leaf_cell_vertex_level_set_values(adapt_cell, ls_cell, c); - const T diag = A[static_cast(k * n + k)]; - for (int i = k + 1; i < n; ++i) + const std::uint64_t current_level_set_bit = std::uint64_t(1) << level_set_id; + std::vector current_level_set_zero(old_cell_vertices.size(), false); + bool has_strict_negative = false; + bool has_strict_positive = false; + bool has_zero = false; + for (std::size_t j = 0; j < ls_values.size(); ++j) { - const T factor = A[static_cast(i * n + k)] / diag; - A[static_cast(i * n + k)] = T(0); - for (int j = k + 1; j < n; ++j) - { - A[static_cast(i * n + j)] -= - factor * A[static_cast(k * n + j)]; - } - b[static_cast(i)] -= - factor * b[static_cast(k)]; + const T value = ls_values[j]; + const int gv = old_cell_vertices[j]; + current_level_set_zero[j] = + (adapt_cell.zero_mask_per_vertex[static_cast(gv)] + & current_level_set_bit) != 0; + has_strict_negative = has_strict_negative || value < -zero_tol; + has_strict_positive = has_strict_positive || value > zero_tol; + has_zero = has_zero || current_level_set_zero[j]; } - } - - x.assign(static_cast(n), T(0)); - for (int i = n - 1; i >= 0; --i) - { - T value = b[static_cast(i)]; - for (int j = i + 1; j < n; ++j) - value -= A[static_cast(i * n + j)] - * x[static_cast(j)]; - const T diag = A[static_cast(i * n + i)]; - if (std::fabs(diag) <= tol) - return false; - x[static_cast(i)] = value / diag; - } - return true; -} - -template -bool graph_affine_jacobian(const LevelSetCell& ls_cell, - std::vector& jacobian) -{ - const int tdim = ls_cell.tdim; - const int gdim = ls_cell.gdim; - if (tdim <= 0 || tdim > 3 || gdim <= 0 - || ls_cell.parent_vertex_coords.empty()) - { - return false; - } - - const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); - jacobian.assign(static_cast(gdim * tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - const int va = cols[static_cast(a)]; - if (va < 0) - return false; - for (int r = 0; r < gdim; ++r) + if (!(has_strict_negative && has_strict_positive)) { - jacobian[static_cast(r * tdim + a)] = - ls_cell.parent_vertex_coords[ - static_cast(va * gdim + r)] - - ls_cell.parent_vertex_coords[static_cast(r)]; + std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); + append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); + old_cell_ids_for_new_cells.push_back(c); + CellCertTag copied_tag = CellCertTag::zero; + if (has_strict_negative) + copied_tag = CellCertTag::negative; + else if (has_strict_positive) + copied_tag = CellCertTag::positive; + else if (!has_zero) + copied_tag = CellCertTag::not_classified; + explicit_current_ls_tags.push_back(copied_tag); + continue; } - } - return true; -} - -template -T graph_metric_alignment_to_host_normal( - const LevelSetCell& ls_cell, - std::span direction_ref, - const graph_criteria::HostFrame& host, - T tol) -{ - if (static_cast(direction_ref.size()) != ls_cell.tdim) - { - return std::numeric_limits::quiet_NaN(); - } - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return graph_alignment_to_host_normal( - direction_ref, - std::span(host.normal.data(), host.normal.size()), - tol); - - std::vector direction_phys(static_cast(ls_cell.gdim), T(0)); - for (int r = 0; r < ls_cell.gdim; ++r) - { - for (int a = 0; a < ls_cell.tdim; ++a) + std::array old_edge_ids_by_local_edge; + old_edge_ids_by_local_edge.fill(-1); + const auto ledges = cell::edges(leaf_cell_type); + for (std::size_t le = 0; le < ledges.size(); ++le) { - const T j_ra = jacobian[static_cast( - r * ls_cell.tdim + a)]; - direction_phys[static_cast(r)] += - j_ra * direction_ref[static_cast(a)]; + const int a = old_cell_vertices[static_cast(ledges[le][0])]; + const int b = old_cell_vertices[static_cast(ledges[le][1])]; + const std::pair key = {std::min(a, b), std::max(a, b)}; + auto edge_it = edge_lookup.find(key); + if (edge_it == edge_lookup.end()) + { + throw std::runtime_error( + "process_ready_to_cut_cells: missing leaf edge for ready-to-cut cell " + + std::to_string(c) + " type=" + + cell_type_to_str(leaf_cell_type) + " local_edge=" + + std::to_string(le) + " key=(" + + std::to_string(key.first) + "," + + std::to_string(key.second) + ")"); + } + old_edge_ids_by_local_edge[le] = edge_it->second; } - } - const T direction_norm = geom::norm( - std::span(direction_phys.data(), direction_phys.size())); - if (direction_norm <= tol) - return T(0); - - if (host.dimension == graph_criteria::HostDimension::edge) - { - if (static_cast(host.tangent.size()) != ls_cell.tdim) - return std::numeric_limits::quiet_NaN(); - - std::vector tangent_phys(static_cast(ls_cell.gdim), T(0)); - for (int r = 0; r < ls_cell.gdim; ++r) + std::map token_to_vertex; + for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) + token_to_vertex[100 + static_cast(j)] = old_cell_vertices[j]; + for (std::size_t le = 0; le < ledges.size(); ++le) { - for (int a = 0; a < ls_cell.tdim; ++a) + const int old_edge_id = old_edge_ids_by_local_edge[le]; + if (old_edge_id < 0) + continue; + if (adapt_cell.get_edge_root_tag(level_set_id, old_edge_id) + != EdgeRootTag::one_root) { - tangent_phys[static_cast(r)] += - jacobian[static_cast(r * ls_cell.tdim + a)] - * host.tangent[static_cast(a)]; - } - } - - const T tangent_norm = geom::norm( - std::span(tangent_phys.data(), tangent_phys.size())); - if (tangent_norm <= tol) - return std::numeric_limits::quiet_NaN(); - - const T cosine = geom::dot( - std::span(direction_phys.data(), direction_phys.size()), - std::span(tangent_phys.data(), tangent_phys.size())) - / (direction_norm * tangent_norm); - const T clipped = std::clamp(cosine, T(-1), T(1)); - return std::sqrt(std::max(T(0), T(1) - clipped * clipped)); - } - - if (static_cast(host.normal.size()) != ls_cell.tdim - || ls_cell.gdim != ls_cell.tdim) - { - return graph_alignment_to_host_normal( - direction_ref, - std::span(host.normal.data(), host.normal.size()), - tol); - } - - std::vector jt(static_cast(ls_cell.tdim * ls_cell.tdim), T(0)); - for (int a = 0; a < ls_cell.tdim; ++a) - { - for (int r = 0; r < ls_cell.gdim; ++r) - { - jt[static_cast(a * ls_cell.tdim + r)] = - jacobian[static_cast(r * ls_cell.tdim + a)]; - } - } - std::vector normal_phys; - std::vector rhs(host.normal.begin(), host.normal.end()); - if (!solve_graph_dense_small( - std::move(jt), std::move(rhs), ls_cell.tdim, normal_phys, tol)) - { - return graph_alignment_to_host_normal( - direction_ref, - std::span(host.normal.data(), host.normal.size()), - tol); - } - - return graph_alignment_to_host_normal( - std::span(direction_phys.data(), direction_phys.size()), - std::span(normal_phys.data(), normal_phys.size()), - tol); -} - -template -std::vector graph_pull_back_physical_direction( - std::span jacobian, - int gdim, - int tdim, - std::span direction_phys, - std::span fallback_ref, - T tol) -{ - std::vector gram(static_cast(tdim * tdim), T(0)); - std::vector rhs(static_cast(tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - for (int r = 0; r < gdim; ++r) - { - rhs[static_cast(a)] += - jacobian[static_cast(r * tdim + a)] - * direction_phys[static_cast(r)]; - } - for (int b = 0; b < tdim; ++b) - { - T value = T(0); - for (int r = 0; r < gdim; ++r) - { - value += jacobian[static_cast(r * tdim + a)] - * jacobian[static_cast(r * tdim + b)]; - } - gram[static_cast(a * tdim + b)] = value; - } - } - - std::vector out; - if (solve_graph_dense_small( - std::move(gram), std::move(rhs), tdim, out, tol)) - { - return out; - } - return std::vector(fallback_ref.begin(), fallback_ref.end()); -} - -template -std::vector graph_parent_entity_metric_gradient_direction( - const LevelSetCell& ls_cell, - geom::ParentEntity parent_entity, - std::span grad_ref, - T tol) -{ - const auto fallback = geom::restricted_level_set_gradient_in_parent_frame( - ls_cell.cell_type, parent_entity, grad_ref, tol); - std::vector fallback_ref = - fallback.value.empty() - ? std::vector(grad_ref.begin(), grad_ref.end()) - : fallback.value; - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return fallback_ref; - - const int tdim = ls_cell.tdim; - const int gdim = ls_cell.gdim; - - std::vector grad_phys; - if (gdim == tdim) - { - std::vector jt(static_cast(tdim * tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - for (int r = 0; r < gdim; ++r) - { - jt[static_cast(a * tdim + r)] = - jacobian[static_cast(r * tdim + a)]; - } - } - std::vector rhs(grad_ref.begin(), grad_ref.end()); - if (!solve_graph_dense_small( - std::move(jt), std::move(rhs), tdim, grad_phys, tol)) - { - return fallback_ref; - } - } - else - { - grad_phys.assign(static_cast(gdim), T(0)); - for (int r = 0; r < gdim; ++r) - { - for (int a = 0; a < tdim; ++a) - { - grad_phys[static_cast(r)] += - jacobian[static_cast(r * tdim + a)] - * fallback_ref[static_cast(a)]; - } - } - } - - if (parent_entity.dim == 2 && gdim == 3) - { - const auto face = cell::face_vertices(ls_cell.cell_type, parent_entity.id); - if (face.size() >= 3) - { - std::array e0 = {T(0), T(0), T(0)}; - std::array e1 = {T(0), T(0), T(0)}; - for (int d = 0; d < 3; ++d) - { - e0[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(face[1] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(face[0] * gdim + d)]; - e1[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(face[2] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(face[0] * gdim + d)]; - } - std::array normal = { - e0[1] * e1[2] - e0[2] * e1[1], - e0[2] * e1[0] - e0[0] * e1[2], - e0[0] * e1[1] - e0[1] * e1[0]}; - const T nn = normal[0] * normal[0] - + normal[1] * normal[1] - + normal[2] * normal[2]; - if (nn > tol * tol) - { - const T c = (grad_phys[0] * normal[0] - + grad_phys[1] * normal[1] - + grad_phys[2] * normal[2]) / nn; - for (int d = 0; d < 3; ++d) - grad_phys[static_cast(d)] -= c * normal[d]; - } - } - } - else if (parent_entity.dim == 1) - { - const auto edges = cell::edges(ls_cell.cell_type); - if (parent_entity.id >= 0 - && parent_entity.id < static_cast(edges.size())) - { - const auto edge = edges[static_cast(parent_entity.id)]; - std::vector tangent(static_cast(gdim), T(0)); - for (int d = 0; d < gdim; ++d) - { - tangent[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(edge[1] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(edge[0] * gdim + d)]; - } - const T tt = geom::dot( - std::span(tangent.data(), tangent.size()), - std::span(tangent.data(), tangent.size())); - if (tt > tol * tol) - { - const T c = geom::dot( - std::span(grad_phys.data(), grad_phys.size()), - std::span(tangent.data(), tangent.size())) / tt; - for (int d = 0; d < gdim; ++d) - grad_phys[static_cast(d)] = - c * tangent[static_cast(d)]; - } - } - } - else if (parent_entity.dim == 0) - { - return std::vector(static_cast(tdim), T(0)); - } - - auto pulled = graph_pull_back_physical_direction( - std::span(jacobian.data(), jacobian.size()), - gdim, - tdim, - std::span(grad_phys.data(), grad_phys.size()), - std::span(fallback_ref.data(), fallback_ref.size()), - tol); - const auto admissible = geom::admissible_direction_in_parent_frame( - ls_cell.cell_type, - parent_entity, - std::span(pulled.data(), pulled.size()), - tol); - return admissible.value.empty() ? pulled : admissible.value; -} - -template -curving::CurvingOptions graph_curving_options( - const ReadyCellGraphOptions& graph_options) -{ - curving::CurvingOptions options; - options.geometry_order = std::max(graph_options.geometry_order, 1); - options.direction_mode = - (graph_options.projection_mode - == GraphProjectionMode::straight_zero_entity_normal) - ? curving::CurvingDirectionMode::straight_zero_entity_normal - : curving::CurvingDirectionMode::level_set_gradient; - options.domain_tol = graph_options.criteria.tolerance; - options.active_face_tol = graph_options.criteria.tolerance; - return options; -} - -inline graph_criteria::FailureReason graph_failure_from_curving( - curving::CurvingFailureCode code) -{ - switch (code) - { - case curving::CurvingFailureCode::none: - case curving::CurvingFailureCode::exact_vertex: - case curving::CurvingFailureCode::boundary_from_edge: - case curving::CurvingFailureCode::small_entity_kept_straight: - return graph_criteria::FailureReason::none; - case curving::CurvingFailureCode::invalid_constraint_count: - case curving::CurvingFailureCode::missing_level_set_cell: - case curving::CurvingFailureCode::empty_zero_mask: - case curving::CurvingFailureCode::unsupported_entity: - case curving::CurvingFailureCode::missing_boundary_edge: - case curving::CurvingFailureCode::boundary_edge_failed: - return graph_criteria::FailureReason::invalid_input; - case curving::CurvingFailureCode::no_host_interval: - case curving::CurvingFailureCode::outside_host_domain: - return graph_criteria::FailureReason::root_segment_leaves_parent_entity; - case curving::CurvingFailureCode::singular_gradient_system: - return graph_criteria::FailureReason::degenerate_direction; - case curving::CurvingFailureCode::no_sign_changing_bracket: - case curving::CurvingFailureCode::brent_failed: - case curving::CurvingFailureCode::line_search_failed: - case curving::CurvingFailureCode::max_iterations: - case curving::CurvingFailureCode::projection_failed: - case curving::CurvingFailureCode::closest_face_retry_failed: - case curving::CurvingFailureCode::constrained_newton_failed: - return graph_criteria::FailureReason::root_not_on_search_line; - } - return graph_criteria::FailureReason::root_not_on_search_line; -} - -template -bool project_graph_point( - const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - geom::ParentEntity admissible_parent_entity, - const graph_criteria::HostFrame& host, - std::span host_point, - GraphNodeKind node_kind, - int node_index, - int requested_refinement_edge_id, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics, - int source_cell_id, - std::vector& corrected_point) -{ - GraphNodeDiagnostics node; - node.node_index = node_index; - node.node_kind = node_kind; - node.parent_entity_dim = admissible_parent_entity.dim; - node.parent_entity_id = admissible_parent_entity.id; - node.seed.assign(host_point.begin(), host_point.end()); - node.straight_helper_normal.assign(host.normal.begin(), host.normal.end()); - if (requested_refinement_edge_id >= 0) - { - node.requested_refinement_entity_dim = 1; - node.requested_refinement_entity_id = requested_refinement_edge_id; - } - node.selected_direction_kind = - (graph_options.projection_mode - == GraphProjectionMode::straight_zero_entity_normal) - ? graph_criteria::DirectionKind::projected_straight_host_normal - : graph_criteria::DirectionKind::projected_level_set_gradient; - - std::vector gradient(static_cast(ls_cell.tdim), T(0)); - curving::reference_level_set_gradient( - ls_cell, - host_point, - std::span(gradient.data(), gradient.size())); - auto graph_gradient_direction = - graph_parent_entity_metric_gradient_direction( - ls_cell, - admissible_parent_entity, - std::span(gradient.data(), gradient.size()), - graph_options.criteria.tolerance); - node.level_set_gradient_direction = graph_gradient_direction; - node.level_set_gradient_host_alignment = - graph_metric_alignment_to_host_normal( - ls_cell, - std::span( - graph_gradient_direction.data(), graph_gradient_direction.size()), - host, - graph_options.criteria.tolerance); - node.level_set_gradient_angle_to_tangent_deg = - graph_angle_to_tangent_degrees( - node.level_set_gradient_host_alignment); - if (std::isfinite(node.level_set_gradient_host_alignment) - && node.level_set_gradient_host_alignment - < graph_options.min_level_set_gradient_host_alignment) - { - node.accepted = false; - node.failure_reason = - graph_criteria::FailureReason::direction_too_tangential_to_host; - observe_graph_node(diagnostics, node); - mark_graph_failure( - diagnostics, source_cell_id, node.failure_reason); - return false; - } - - std::vector straight_normal_direction; - const auto admissible_normal = - geom::admissible_direction_in_parent_frame( - ls_cell.cell_type, - admissible_parent_entity, - std::span(host.normal.data(), host.normal.size()), - graph_options.criteria.tolerance); - if (!admissible_normal.degenerate()) - straight_normal_direction = admissible_normal.value; - - const T grad_norm = geom::norm( - std::span( - graph_gradient_direction.data(), graph_gradient_direction.size())); - const T normal_norm = geom::norm( - std::span( - straight_normal_direction.data(), straight_normal_direction.size())); - std::vector selected_policy_direction; - if (graph_options.projection_mode == GraphProjectionMode::level_set_gradient) - { - if (grad_norm > graph_options.criteria.tolerance) - { - selected_policy_direction = graph_gradient_direction; - node.selected_direction_kind = - graph_criteria::DirectionKind::projected_level_set_gradient; - } - else - { - selected_policy_direction = straight_normal_direction; - node.selected_direction_kind = - graph_criteria::DirectionKind::projected_straight_host_normal; - node.fallback_used = true; - } - } - else - { - if (normal_norm > graph_options.criteria.tolerance) - { - selected_policy_direction = straight_normal_direction; - node.selected_direction_kind = - graph_criteria::DirectionKind::projected_straight_host_normal; - } - else - { - selected_policy_direction = graph_gradient_direction; - node.selected_direction_kind = - graph_criteria::DirectionKind::projected_level_set_gradient; - node.fallback_used = true; - } - } - - const auto curving_options = graph_curving_options(graph_options); - auto projection = curving::project_seed_to_zero_entity_diagnostic( - adapt_cell, - local_zero_entity_id, - ls_cell, - host_point, - curving_options); - - if (!projection.accepted) - { - node.accepted = false; - node.failure_reason = graph_failure_from_curving(projection.failure_code); - node.selected_direction = selected_policy_direction; - node.corrected = projection.projected; - observe_graph_node(diagnostics, node); - observe_failed_graph_projection(diagnostics, projection); - mark_graph_failure(diagnostics, source_cell_id, node.failure_reason); - return false; - } - - corrected_point = std::move(projection.projected); - node.corrected = corrected_point; - if (corrected_point.size() != host_point.size()) - { - node.accepted = false; - node.failure_reason = graph_criteria::FailureReason::invalid_input; - observe_graph_node(diagnostics, node); - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - std::vector direction = std::move(selected_policy_direction); - if (geom::norm(std::span(direction.data(), direction.size())) - <= graph_options.criteria.tolerance) - { - direction.resize(host_point.size(), T(0)); - for (std::size_t d = 0; d < host_point.size(); ++d) - direction[d] = corrected_point[d] - host_point[d]; - } - - if (geom::norm(std::span(direction.data(), direction.size())) - <= graph_options.criteria.tolerance) - { - node.accepted = true; - node.failure_reason = graph_criteria::FailureReason::none; - node.selected_direction = direction; - observe_graph_node(diagnostics, node); - return true; - } - node.selected_direction = direction; - - auto report = graph_criteria::evaluate_direction( - ls_cell.cell_type, - admissible_parent_entity, - host, - host_point, - std::span( - graph_gradient_direction.data(), graph_gradient_direction.size()), - std::span(direction.data(), direction.size()), - std::span(corrected_point.data(), corrected_point.size()), - node.selected_direction_kind, - graph_options.criteria); - const T metric_selected_alignment = - graph_metric_alignment_to_host_normal( - ls_cell, - std::span(direction.data(), direction.size()), - host, - graph_options.criteria.tolerance); - if (std::isfinite(metric_selected_alignment)) - { - report.metrics.host_normal_alignment = metric_selected_alignment; - report.metrics.drift_amplification = - graph_criteria::drift_from_alignment( - metric_selected_alignment, - graph_options.criteria.tolerance); - if ((report.failure_reason - == graph_criteria::FailureReason::excessive_drift_amplification - || report.failure_reason - == graph_criteria::FailureReason::direction_too_tangential_to_host) - && report.metrics.drift_amplification - <= graph_options.criteria.max_drift_amplification - && report.metrics.host_normal_alignment - >= graph_options.criteria.min_host_normal_alignment) - { - report.accepted = true; - report.failure_reason = graph_criteria::FailureReason::none; - } - } - observe_graph_direction(diagnostics, report); - node.true_transversality = report.metrics.true_transversality; - node.selected_host_alignment = report.metrics.host_normal_alignment; - node.drift_amplification = report.metrics.drift_amplification; - node.relative_correction_distance = - report.metrics.relative_correction_distance; - node.relative_tangential_shift = - report.metrics.relative_tangential_shift; - if (!report.accepted) - { - if (report.failure_reason - == graph_criteria::FailureReason::root_not_on_search_line) - { - node.accepted = true; - node.failure_reason = graph_criteria::FailureReason::none; - observe_graph_node(diagnostics, node); - return true; - } - node.accepted = false; - node.failure_reason = report.failure_reason; - observe_graph_node(diagnostics, node); - observe_failed_graph_projection(diagnostics, projection); - mark_graph_failure( - diagnostics, source_cell_id, report.failure_reason); - return false; - } - node.accepted = true; - node.failure_reason = graph_criteria::FailureReason::none; - observe_graph_node(diagnostics, node); - return true; -} - -template -bool build_edge_host_frame(std::span a, - std::span b, - std::span zero_face_normal, - graph_criteria::HostFrame& host, - T tol) -{ - const auto tangent = geom::segment_tangent(a, b, true, tol); - if (tangent.degenerate()) - return false; - - host.dimension = graph_criteria::HostDimension::edge; - host.tangent = tangent.value; - host.h = tangent.norm; - - if (a.size() == 2) - { - const auto normal = geom::segment_normal(a, b, true, tol); - if (normal.degenerate()) - return false; - host.normal = normal.value; - return true; - } - - if (zero_face_normal.size() == 3) - { - const auto normal = geom::in_face_segment_normal( - a, b, zero_face_normal, true, tol); - if (normal.degenerate()) - return false; - host.normal = normal.value; - return true; - } - - return false; -} - -template -bool build_face_host_frame(const std::vector& face_vertices, - graph_criteria::HostFrame& host, - T tol) -{ - const int dim = 3; - const int nverts = static_cast( - face_vertices.size() / static_cast(dim)); - if (nverts < 3) - return false; - - const auto normal = geom::face_normal( - point_span(face_vertices, 0, dim), - point_span(face_vertices, 1, dim), - point_span(face_vertices, 2, dim), - true, - tol); - if (normal.degenerate()) - return false; - - T h = T(0); - for (int i = 0; i < nverts; ++i) - { - const auto a = point_span(face_vertices, i, dim); - const auto b = point_span(face_vertices, (i + 1) % nverts, dim); - h = std::max(h, geom::norm( - std::span(geom::subtract(b, a).data(), dim))); - } - - host.dimension = graph_criteria::HostDimension::face; - host.normal = normal.value; - host.tangent.clear(); - host.h = h; - return h > tol; -} - -template -std::vector zero_entity_host_cell_vertex_ids(const AdaptCell& ac, - int local_zero_entity_id) -{ - if (local_zero_entity_id < 0 - || local_zero_entity_id >= ac.zero_entity_host_cell_vertices.size()) - { - return {}; - } - const auto vertices = ac.zero_entity_host_cell_vertices[ - static_cast(local_zero_entity_id)]; - std::vector out; - out.reserve(vertices.size()); - for (const auto v : vertices) - out.push_back(static_cast(v)); - return out; -} - -template -bool point_in_host_triangle(const AdaptCell& ac, - std::span triangle_vertices, - std::span point, - T tol) -{ - if (ac.tdim != 3 || triangle_vertices.size() != 3 || point.size() != 3) - return false; - - const auto a = point_span(ac.vertex_coords, triangle_vertices[0], 3); - const auto b = point_span(ac.vertex_coords, triangle_vertices[1], 3); - const auto c = point_span(ac.vertex_coords, triangle_vertices[2], 3); - std::array v0 = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; - std::array v1 = {c[0] - a[0], c[1] - a[1], c[2] - a[2]}; - std::array v2 = {point[0] - a[0], point[1] - a[1], point[2] - a[2]}; - const auto normal = geom::cross( - std::span(v0.data(), 3), - std::span(v1.data(), 3)); - const T area2 = geom::norm( - std::span(normal.data(), normal.size())); - if (area2 <= tol) - return false; - - const T plane = - std::fabs(geom::dot( - std::span(v2.data(), 3), - std::span(normal.data(), normal.size()))) / area2; - if (plane > tol) - return false; - - const T d00 = geom::dot( - std::span(v0.data(), 3), - std::span(v0.data(), 3)); - const T d01 = geom::dot( - std::span(v0.data(), 3), - std::span(v1.data(), 3)); - const T d11 = geom::dot( - std::span(v1.data(), 3), - std::span(v1.data(), 3)); - const T d20 = geom::dot( - std::span(v2.data(), 3), - std::span(v0.data(), 3)); - const T d21 = geom::dot( - std::span(v2.data(), 3), - std::span(v1.data(), 3)); - const T denom = d00 * d11 - d01 * d01; - if (std::fabs(denom) <= tol * tol) - return false; - - const T v = (d11 * d20 - d01 * d21) / denom; - const T w = (d00 * d21 - d01 * d20) / denom; - const T u = T(1) - v - w; - return u >= -tol && v >= -tol && w >= -tol - && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; -} - -template -bool point_in_host_face(const AdaptCell& ac, - std::span face_vertices, - std::span point, - T tol) -{ - if (face_vertices.size() == 3) - return point_in_host_triangle(ac, face_vertices, point, tol); - if (face_vertices.size() != 4) - return false; - - const std::array tri0 = { - face_vertices[0], face_vertices[1], face_vertices[3]}; - const std::array tri1 = { - face_vertices[0], face_vertices[3], face_vertices[2]}; - return point_in_host_triangle( - ac, std::span(tri0.data(), tri0.size()), point, tol) - || point_in_host_triangle( - ac, std::span(tri1.data(), tri1.size()), point, tol); -} - -template -std::vector host_boundary_face_normal_for_zero_face_edge( - const AdaptCell& ac, - int zero_face_id, - std::span edge_a, - std::span edge_b, - T tol, - int* host_face_id = nullptr) -{ - if (zero_face_id < 0 || zero_face_id >= ac.n_zero_entities() - || ac.tdim != 3 || edge_a.size() != 3 || edge_b.size() != 3) - { - return {}; - } - if (zero_face_id >= static_cast(ac.zero_entity_host_cell_type.size())) - return {}; - - const auto host_vertices = - zero_entity_host_cell_vertex_ids(ac, zero_face_id); - if (host_vertices.empty()) - return {}; - - const cell::type host_type = - ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; - for (int f = 0; f < cell::num_faces(host_type); ++f) - { - const auto local_face = cell::face_vertices(host_type, f); - if (local_face.size() != 3 && local_face.size() != 4) - continue; - std::array face_vertices = {-1, -1, -1, -1}; - for (std::size_t i = 0; i < local_face.size(); ++i) - face_vertices[i] = - host_vertices[static_cast(local_face[i])]; - - const std::span face_span( - face_vertices.data(), local_face.size()); - if (!point_in_host_face(ac, face_span, edge_a, tol) - || !point_in_host_face(ac, face_span, edge_b, tol)) - { - continue; - } - - const auto normal = geom::face_normal( - point_span(ac.vertex_coords, face_vertices[0], 3), - point_span(ac.vertex_coords, face_vertices[1], 3), - point_span( - ac.vertex_coords, - face_vertices[local_face.size() == 4 ? 3 : 2], - 3), - true, - tol); - if (!normal.degenerate()) - { - if (host_face_id != nullptr) - *host_face_id = f; - return normal.value; - } - } - return {}; -} - -template -int find_zero_edge_entity_by_vertices(const AdaptCell& ac, - int vertex_a, - int vertex_b, - std::uint64_t zero_mask) -{ - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (ac.zero_entity_dim[static_cast(z)] != 1) - continue; - if ((ac.zero_entity_zero_mask[static_cast(z)] & zero_mask) - != zero_mask) - { - continue; - } - - const int edge_id = ac.zero_entity_id[static_cast(z)]; - const auto edge_vertices = - ac.entity_to_vertex[1][static_cast(edge_id)]; - if (edge_vertices.size() != 2) - continue; - const int a = static_cast(edge_vertices[0]); - const int b = static_cast(edge_vertices[1]); - if ((a == vertex_a && b == vertex_b) - || (a == vertex_b && b == vertex_a)) - { - return z; - } - } - return -1; -} - -template -void override_zero_edge_host_from_zero_face(AdaptCell& ac, - int zero_edge_id, - int zero_face_id, - int host_face_id) -{ - if (zero_edge_id < 0 || zero_face_id < 0 - || zero_edge_id >= ac.n_zero_entities() - || zero_face_id >= ac.n_zero_entities()) - { - return; - } - if (zero_edge_id >= static_cast(ac.zero_entity_host_cell_id.size()) - || zero_face_id >= static_cast(ac.zero_entity_host_cell_id.size())) - { - return; - } - - const auto face_host_vertices = - zero_entity_host_cell_vertex_ids(ac, zero_face_id); - if (face_host_vertices.empty()) - return; - - ac.zero_entity_host_cell_id[static_cast(zero_edge_id)] = - ac.zero_entity_host_cell_id[static_cast(zero_face_id)]; - ac.zero_entity_host_cell_type[static_cast(zero_edge_id)] = - ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; - ac.zero_entity_host_face_id[static_cast(zero_edge_id)] = - static_cast(host_face_id); - ac.zero_entity_source_level_set[static_cast(zero_edge_id)] = - ac.zero_entity_source_level_set[static_cast(zero_face_id)]; - - EntityAdjacency updated; - updated.offsets.push_back(std::int32_t(0)); - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (z == zero_edge_id) - { - for (const int v : face_host_vertices) - updated.indices.push_back(static_cast(v)); - } - else if (z < ac.zero_entity_host_cell_vertices.size()) - { - const auto old = ac.zero_entity_host_cell_vertices[ - static_cast(z)]; - for (const auto v : old) - updated.indices.push_back(v); - } - updated.offsets.push_back( - static_cast(updated.indices.size())); - } - ac.zero_entity_host_cell_vertices = std::move(updated); -} - -template -bool check_zero_edge_graph(const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span a, - std::span b, - std::span zero_face_normal, - bool face_boundary_edge_check, - int requested_refinement_edge_id, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics, - int source_cell_id) -{ - ++diagnostics.checked_edges; - - std::vector host_points; - const int order = std::max(graph_options.geometry_order, 1); - for (int k = 0; k <= order; ++k) - { - const T s = T(k) / T(order); - for (std::size_t d = 0; d < a.size(); ++d) - host_points.push_back((T(1) - s) * a[d] + s * b[d]); - } - - const auto parent_entity = - (ls_cell.tdim == 3 && zero_face_normal.size() == 3) - ? common_parent_face_or_cell_for_points( - ls_cell.cell_type, - host_points, - ls_cell.tdim, - graph_options.criteria.tolerance) - : common_parent_entity_for_points( - ls_cell.cell_type, - host_points, - ls_cell.tdim, - graph_options.criteria.tolerance); - - std::vector frame_normal(zero_face_normal.begin(), zero_face_normal.end()); - if (ls_cell.tdim == 3 && parent_entity.dim == 2) - { - const auto parent_normal = geom::parent_face_normal( - ls_cell.cell_type, - parent_entity.id, - true, - graph_options.criteria.tolerance); - if (!parent_normal.degenerate()) - frame_normal = parent_normal.value; - } - - graph_criteria::HostFrame host; - if (!build_edge_host_frame( - a, - b, - std::span(frame_normal.data(), frame_normal.size()), - host, - graph_options.criteria.tolerance)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_host_frame); - return false; - } - - std::vector corrected_points; - corrected_points.reserve(host_points.size()); - for (int i = 0; i <= order; ++i) - { - if (i == 0 || i == order) - { - auto endpoint = point_span(host_points, i, ls_cell.tdim); - corrected_points.insert( - corrected_points.end(), endpoint.begin(), endpoint.end()); - continue; - } - - const GraphNodeKind node_kind = - face_boundary_edge_check - ? GraphNodeKind::face_boundary_edge - : GraphNodeKind::edge_interior; - std::vector corrected; - if (!project_graph_point( - adapt_cell, - local_zero_entity_id, - ls_cell, - parent_entity, - host, - point_span(host_points, i, ls_cell.tdim), - node_kind, - i, - requested_refinement_edge_id, - graph_options, - diagnostics, - source_cell_id, - corrected)) - { - return false; - } - corrected_points.insert( - corrected_points.end(), corrected.begin(), corrected.end()); - } - - const auto ordering = graph_criteria::evaluate_projected_edge_ordering( - std::span(host_points.data(), host_points.size()), - std::span(corrected_points.data(), corrected_points.size()), - ls_cell.tdim, - std::span(host.tangent.data(), host.tangent.size()), - host.h, - graph_options.criteria); - diagnostics.min_edge_gap_ratio = - std::min(diagnostics.min_edge_gap_ratio, ordering.minimum_gap_ratio); - if (!ordering.accepted) - { - mark_graph_failure( - diagnostics, source_cell_id, ordering.failure_reason); - return false; - } - - return true; -} - -template -std::vector graph_push_forward_vector(const std::vector& jacobian, - int gdim, - int tdim, - std::span vector_ref); - -template -bool graph_physical_level_set_gradient( - const LevelSetCell& ls_cell, - std::span point_ref, - std::vector& gradient_phys, - T tol) -{ - if (static_cast(point_ref.size()) != ls_cell.tdim) - return false; - - std::vector gradient_ref(static_cast(ls_cell.tdim), T(0)); - curving::reference_level_set_gradient( - ls_cell, - point_ref, - std::span(gradient_ref.data(), gradient_ref.size())); - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return false; - - if (ls_cell.gdim == ls_cell.tdim) - { - std::vector jt( - static_cast(ls_cell.tdim * ls_cell.tdim), T(0)); - for (int a = 0; a < ls_cell.tdim; ++a) - { - for (int r = 0; r < ls_cell.gdim; ++r) - { - jt[static_cast(a * ls_cell.tdim + r)] = - jacobian[static_cast(r * ls_cell.tdim + a)]; - } - } - std::vector rhs(gradient_ref.begin(), gradient_ref.end()); - return solve_graph_dense_small( - std::move(jt), std::move(rhs), ls_cell.tdim, gradient_phys, tol); - } - - gradient_phys.assign(static_cast(ls_cell.gdim), T(0)); - for (int r = 0; r < ls_cell.gdim; ++r) - { - for (int a = 0; a < ls_cell.tdim; ++a) - { - gradient_phys[static_cast(r)] += - jacobian[static_cast(r * ls_cell.tdim + a)] - * gradient_ref[static_cast(a)]; - } - } - return geom::norm( - std::span(gradient_phys.data(), gradient_phys.size())) - > tol; -} - -template -int best_orthogonal_surface_edge_refinement_id( - const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span ordered_vertices, - const ReadyCellGraphOptions& graph_options, - std::vector* selected_midpoint_ref = nullptr) -{ - const int zdim = - adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) - return -1; - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return -1; - - const cell::type zero_face_type = - adapt_cell.entity_types[2][static_cast(zid)]; - const auto zero_face_edges = cell::edges(zero_face_type); - const T tol = graph_options.criteria.tolerance; - - int selected_edge_id = -1; - T selected_abs_cosine = std::numeric_limits::infinity(); - T selected_length = T(0); - for (const auto& zero_face_edge : zero_face_edges) - { - const int ia = zero_face_edge[0]; - const int ib = zero_face_edge[1]; - if (ia < 0 || ib < 0 - || ia >= static_cast(ordered_vertices.size()) - || ib >= static_cast(ordered_vertices.size())) - { - continue; - } - - const int a = ordered_vertices[static_cast(ia)]; - const int b = ordered_vertices[static_cast(ib)]; - const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); - const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); - - std::vector midpoint(static_cast(ls_cell.tdim), T(0)); - std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); - for (int d = 0; d < ls_cell.tdim; ++d) - { - midpoint[static_cast(d)] = T(0.5) * ( - pa[static_cast(d)] - + pb[static_cast(d)]); - tangent_ref[static_cast(d)] = - pb[static_cast(d)] - - pa[static_cast(d)]; - } - - std::vector tangent_phys(static_cast(ls_cell.gdim), T(0)); - for (int r = 0; r < ls_cell.gdim; ++r) - { - for (int d = 0; d < ls_cell.tdim; ++d) - { - tangent_phys[static_cast(r)] += - jacobian[static_cast(r * ls_cell.tdim + d)] - * tangent_ref[static_cast(d)]; - } - } - - std::vector gradient_phys; - if (!graph_physical_level_set_gradient( - ls_cell, - std::span(midpoint.data(), midpoint.size()), - gradient_phys, - tol)) - { - continue; - } - - const T tangent_norm = geom::norm( - std::span(tangent_phys.data(), tangent_phys.size())); - const T gradient_norm = geom::norm( - std::span(gradient_phys.data(), gradient_phys.size())); - if (tangent_norm <= tol || gradient_norm <= tol) - continue; - - const T abs_cosine = std::fabs(geom::dot( - std::span(gradient_phys.data(), gradient_phys.size()), - std::span(tangent_phys.data(), tangent_phys.size())) - / (gradient_norm * tangent_norm)); - const int edge_id = - graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); - if (edge_id < 0) - continue; - - if (selected_edge_id < 0 - || abs_cosine < selected_abs_cosine - || (std::fabs(abs_cosine - selected_abs_cosine) <= tol - && tangent_norm > selected_length)) - { - selected_edge_id = edge_id; - selected_abs_cosine = abs_cosine; - selected_length = tangent_norm; - if (selected_midpoint_ref != nullptr) - *selected_midpoint_ref = midpoint; - } - } - - return selected_edge_id; -} - -template -int best_midpoint_residual_surface_edge_refinement_id( - const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span ordered_vertices, - const ReadyCellGraphOptions& graph_options, - std::vector* selected_midpoint_ref = nullptr) -{ - const int zdim = - adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) - return -1; - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return -1; - - const cell::type zero_face_type = - adapt_cell.entity_types[2][static_cast(zid)]; - const auto zero_face_edges = cell::edges(zero_face_type); - const T tol = graph_options.criteria.tolerance; - - int selected_edge_id = -1; - T selected_score = -std::numeric_limits::infinity(); - T selected_length = T(0); - for (const auto& zero_face_edge : zero_face_edges) - { - const int ia = zero_face_edge[0]; - const int ib = zero_face_edge[1]; - if (ia < 0 || ib < 0 - || ia >= static_cast(ordered_vertices.size()) - || ib >= static_cast(ordered_vertices.size())) - { - continue; - } - - const int a = ordered_vertices[static_cast(ia)]; - const int b = ordered_vertices[static_cast(ib)]; - const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); - const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); - - std::vector midpoint(static_cast(ls_cell.tdim), T(0)); - std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); - for (int d = 0; d < ls_cell.tdim; ++d) - { - midpoint[static_cast(d)] = T(0.5) * ( - pa[static_cast(d)] - + pb[static_cast(d)]); - tangent_ref[static_cast(d)] = - pb[static_cast(d)] - - pa[static_cast(d)]; - } - - const auto tangent_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(tangent_ref.data(), tangent_ref.size())); - const T tangent_norm = geom::norm( - std::span(tangent_phys.data(), tangent_phys.size())); - if (tangent_norm <= tol) - continue; - - std::vector gradient_phys; - T gradient_norm = T(1); - if (graph_physical_level_set_gradient( - ls_cell, - std::span(midpoint.data(), midpoint.size()), - gradient_phys, - tol)) - { - gradient_norm = std::max( - geom::norm( - std::span( - gradient_phys.data(), gradient_phys.size())), - tol); - } - - const T score = std::fabs(ls_cell.value( - std::span( - midpoint.data(), midpoint.size()))) - / gradient_norm; - const int edge_id = - graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); - if (edge_id < 0) - continue; - - if (selected_edge_id < 0 - || score > selected_score - || (std::fabs(score - selected_score) <= tol - && tangent_norm > selected_length)) - { - selected_edge_id = edge_id; - selected_score = score; - selected_length = tangent_norm; - if (selected_midpoint_ref != nullptr) - *selected_midpoint_ref = midpoint; - } - } - - return selected_edge_id; -} - -template -int best_normal_variation_surface_edge_refinement_id( - const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span ordered_vertices, - const ReadyCellGraphOptions& graph_options, - std::vector* selected_midpoint_ref = nullptr) -{ - const int zdim = - adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) - return -1; - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - return -1; - - const cell::type zero_face_type = - adapt_cell.entity_types[2][static_cast(zid)]; - const auto zero_face_edges = cell::edges(zero_face_type); - const T tol = graph_options.criteria.tolerance; - - int selected_edge_id = -1; - T selected_score = -std::numeric_limits::infinity(); - T selected_length = T(0); - for (const auto& zero_face_edge : zero_face_edges) - { - const int ia = zero_face_edge[0]; - const int ib = zero_face_edge[1]; - if (ia < 0 || ib < 0 - || ia >= static_cast(ordered_vertices.size()) - || ib >= static_cast(ordered_vertices.size())) - { - continue; - } - - const int a = ordered_vertices[static_cast(ia)]; - const int b = ordered_vertices[static_cast(ib)]; - const auto pa = point_span(adapt_cell.vertex_coords, a, ls_cell.tdim); - const auto pb = point_span(adapt_cell.vertex_coords, b, ls_cell.tdim); - - std::vector midpoint(static_cast(ls_cell.tdim), T(0)); - std::vector tangent_ref(static_cast(ls_cell.tdim), T(0)); - for (int d = 0; d < ls_cell.tdim; ++d) - { - midpoint[static_cast(d)] = T(0.5) * ( - pa[static_cast(d)] - + pb[static_cast(d)]); - tangent_ref[static_cast(d)] = - pb[static_cast(d)] - - pa[static_cast(d)]; - } - - const auto tangent_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(tangent_ref.data(), tangent_ref.size())); - const T tangent_norm = geom::norm( - std::span(tangent_phys.data(), tangent_phys.size())); - if (tangent_norm <= tol) - continue; - - std::vector ga; - std::vector gb; - if (!graph_physical_level_set_gradient( - ls_cell, pa, ga, tol) - || !graph_physical_level_set_gradient( - ls_cell, pb, gb, tol)) - { - continue; - } - const T ga_norm = geom::norm( - std::span(ga.data(), ga.size())); - const T gb_norm = geom::norm( - std::span(gb.data(), gb.size())); - if (ga_norm <= tol || gb_norm <= tol) - continue; - - const T abs_cosine = std::fabs(geom::dot( - std::span(ga.data(), ga.size()), - std::span(gb.data(), gb.size())) - / (ga_norm * gb_norm)); - const T score = (T(1) - std::clamp(abs_cosine, T(0), T(1))) - * tangent_norm; - const int edge_id = - graph_refinement_edge_from_failed_face_segment(adapt_cell, a, b); - if (edge_id < 0) - continue; - - if (selected_edge_id < 0 - || score > selected_score - || (std::fabs(score - selected_score) <= tol - && tangent_norm > selected_length)) - { - selected_edge_id = edge_id; - selected_score = score; - selected_length = tangent_norm; - if (selected_midpoint_ref != nullptr) - *selected_midpoint_ref = midpoint; - } - } - - return selected_edge_id; -} - -template -std::vector graph_push_forward_vector(const std::vector& jacobian, - int gdim, - int tdim, - std::span vector_ref) -{ - std::vector out(static_cast(gdim), T(0)); - if (static_cast(vector_ref.size()) != tdim - || static_cast(jacobian.size()) != gdim * tdim) - { - return out; - } - for (int r = 0; r < gdim; ++r) - { - for (int a = 0; a < tdim; ++a) - { - out[static_cast(r)] += - jacobian[static_cast(r * tdim + a)] - * vector_ref[static_cast(a)]; - } - } - return out; -} - -template -std::pair graph_legendre_value_and_derivative(int order, T x) -{ - if (order == 0) - return {T(1), T(0)}; - if (order == 1) - return {x, T(1)}; - - T pm2 = T(1); - T pm1 = x; - for (int n = 2; n <= order; ++n) - { - const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); - pm2 = pm1; - pm1 = p; - } - - const T denom = T(1) - x * x; - if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) - return {pm1, T(0)}; - return {pm1, T(order) * (pm2 - x * pm1) / denom}; -} - -template -std::vector graph_gll_parameters(int order) -{ - order = std::max(order, 1); - std::vector params(static_cast(order + 1), T(0)); - params.front() = T(0); - params.back() = T(1); - if (order == 1) - return params; - - const T pi = std::acos(T(-1)); - const T eps = T(128) * std::numeric_limits::epsilon(); - for (int i = 1; i < order; ++i) - { - T x = -std::cos(pi * T(i) / T(order)); - for (int iter = 0; iter < 32; ++iter) - { - const auto [p, dp] = graph_legendre_value_and_derivative(order, x); - const T denom = T(1) - x * x; - if (std::abs(denom) <= eps) - break; - const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; - if (std::abs(d2p) <= eps) - break; - const T step = dp / d2p; - x -= step; - x = std::clamp(x, -T(1) + eps, T(1) - eps); - if (std::abs(step) <= eps) - break; - } - params[static_cast(i)] = T(0.5) * (x + T(1)); - } - return params; -} - -template -std::vector graph_interpolation_parameters(int order, - curving::NodeFamily family) -{ - order = std::max(order, 1); - if (family == curving::NodeFamily::gll) - return graph_gll_parameters(order); - - std::vector params(static_cast(order + 1), T(0)); - for (int i = 0; i <= order; ++i) - params[static_cast(i)] = T(i) / T(order); - return params; -} - -template -T graph_lagrange_basis_1d(int i, std::span params, T x) -{ - T value = T(1); - const T xi = params[static_cast(i)]; - for (int j = 0; j < static_cast(params.size()); ++j) - { - if (j == i) - continue; - value *= (x - params[static_cast(j)]) - / (xi - params[static_cast(j)]); - } - return value; -} - -template -T graph_warp_factor(int order, T r) -{ - if (order <= 1) - return T(0); - - std::vector equispaced(static_cast(order + 1), T(0)); - auto gll = graph_gll_parameters(order); - for (int i = 0; i <= order; ++i) - { - equispaced[static_cast(i)] = - -T(1) + T(2 * i) / T(order); - gll[static_cast(i)] = - T(2) * gll[static_cast(i)] - T(1); - } - - T warp = T(0); - for (int i = 0; i <= order; ++i) - { - const T Li = graph_lagrange_basis_1d( - i, std::span(equispaced.data(), equispaced.size()), r); - warp += Li * (gll[static_cast(i)] - - equispaced[static_cast(i)]); - } - - const T edge_factor = T(1) - r * r; - if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) - warp /= edge_factor; - return warp; -} - -template -std::array graph_equilateral_to_reference_barycentric(T x, T y) -{ - const T sqrt3 = std::sqrt(T(3)); - std::array w = {}; - w[2] = (sqrt3 * y + T(1)) / T(3); - w[1] = (x + T(1) - w[2]) / T(2); - w[0] = T(1) - w[1] - w[2]; - - T sum = T(0); - for (T& wi : w) - { - if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) - wi = T(0); - if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) - wi = T(1); - wi = std::clamp(wi, T(0), T(1)); - sum += wi; - } - if (sum > T(0)) - for (T& wi : w) - wi /= sum; - return w; -} - -template -std::vector> graph_triangle_barycentric_nodes( - int order, - curving::NodeFamily family) -{ - order = std::max(order, 1); - std::vector> nodes; - nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); - - if (family != curving::NodeFamily::gll) - { - for (int j = 0; j <= order; ++j) - { - for (int i = 0; i <= order - j; ++i) - { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - nodes.push_back({T(1) - u - v, u, v}); - } - } - return nodes; - } - - constexpr std::array alpha_opt = { - T(0.0), T(0.0), T(1.4152), T(0.1001), - T(0.2751), T(0.9800), T(1.0999), T(1.2832), - T(1.3648), T(1.4773), T(1.4959), T(1.5743), - T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; - const T alpha = (order < static_cast(alpha_opt.size())) - ? alpha_opt[static_cast(order)] - : T(5) / T(3); - const T sqrt3 = std::sqrt(T(3)); - const T cos120 = -T(0.5); - const T sin120 = sqrt3 / T(2); - const T cos240 = -T(0.5); - const T sin240 = -sqrt3 / T(2); - - for (int j = 0; j <= order; ++j) - { - for (int i = 0; i <= order - j; ++i) - { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - const T lambda0 = T(1) - u - v; - const T lambda1 = u; - const T lambda2 = v; - - T x = -lambda0 + lambda1; - T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; - - const T L1 = lambda2; - const T L2 = lambda0; - const T L3 = lambda1; - const T warp1 = T(4) * L2 * L3 * graph_warp_factor(order, L3 - L2) - * (T(1) + (alpha * L1) * (alpha * L1)); - const T warp2 = T(4) * L1 * L3 * graph_warp_factor(order, L1 - L3) - * (T(1) + (alpha * L2) * (alpha * L2)); - const T warp3 = T(4) * L1 * L2 * graph_warp_factor(order, L2 - L1) - * (T(1) + (alpha * L3) * (alpha * L3)); - - x += warp1 + cos120 * warp2 + cos240 * warp3; - y += sin120 * warp2 + sin240 * warp3; - nodes.push_back(graph_equilateral_to_reference_barycentric(x, y)); - } - } - return nodes; -} - -template -std::vector graph_triangle_monomials(int order, std::span bary) -{ - const T u = bary[1]; - const T v = bary[2]; - std::vector values; - values.reserve(static_cast((order + 1) * (order + 2) / 2)); - for (int total = 0; total <= order; ++total) - { - for (int j = 0; j <= total; ++j) - { - const int i = total - j; - values.push_back(std::pow(u, i) * std::pow(v, j)); - } - } - return values; -} - -template -std::vector graph_triangle_lagrange_basis( - int order, - curving::NodeFamily family, - std::span bary) -{ - const auto nodes = graph_triangle_barycentric_nodes(order, family); - const int n = static_cast(nodes.size()); - std::vector matrix(static_cast(n * n), T(0)); - for (int row = 0; row < n; ++row) - { - const auto mono = graph_triangle_monomials( - order, - std::span(nodes[static_cast(row)].data(), 3)); - for (int col = 0; col < n; ++col) - matrix[static_cast(col * n + row)] = - mono[static_cast(col)]; - } - - const auto rhs = graph_triangle_monomials(order, bary); - std::vector basis; - if (!solve_graph_dense_small(std::move(matrix), rhs, n, basis, - T(256) * std::numeric_limits::epsilon())) - { - return {}; - } - return basis; -} - -template -std::vector graph_eval_curved_zero_face_ref( - const curving::CurvedZeroEntityState& state, - cell::type entity_type, - int order, - curving::NodeFamily family, - std::span xi) -{ - const int nodes_per_face = - (entity_type == cell::type::quadrilateral) - ? (order + 1) * (order + 1) - : (order + 1) * (order + 2) / 2; - if (nodes_per_face <= 0 - || state.ref_nodes.size() % static_cast(nodes_per_face) != 0) - { - return {}; - } - const int tdim = static_cast( - state.ref_nodes.size() / static_cast(nodes_per_face)); - std::vector out(static_cast(tdim), T(0)); - - if (entity_type == cell::type::quadrilateral) - { - const auto params = graph_interpolation_parameters(order, family); - const T u = xi[0]; - const T v = xi[1]; - int node = 0; - for (int j = 0; j <= order; ++j) - { - const T Lj = graph_lagrange_basis_1d( - j, std::span(params.data(), params.size()), v); - for (int i = 0; i <= order; ++i) - { - const T Li = graph_lagrange_basis_1d( - i, std::span(params.data(), params.size()), u); - const T L = Li * Lj; - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += L * state.ref_nodes[ - static_cast(node * tdim + d)]; - ++node; - } - } - return out; - } - - if (entity_type == cell::type::triangle) - { - const std::array bary = {T(1) - xi[0] - xi[1], xi[0], xi[1]}; - const auto basis = graph_triangle_lagrange_basis( - order, family, std::span(bary.data(), bary.size())); - if (basis.empty()) - return {}; - for (int node = 0; node < static_cast(basis.size()); ++node) - { - const T L = basis[static_cast(node)]; - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += L * state.ref_nodes[ - static_cast(node * tdim + d)]; - } - return out; - } - - return {}; -} - -template -std::vector graph_eval_straight_zero_face_ref( - const AdaptCell& adapt_cell, - std::span vertices, - cell::type entity_type, - std::span xi) -{ - std::vector out(static_cast(adapt_cell.tdim), T(0)); - std::array weights = {}; - int nweights = 0; - if (entity_type == cell::type::triangle) - { - weights = {T(1) - xi[0] - xi[1], xi[0], xi[1], T(0)}; - nweights = 3; - } - else if (entity_type == cell::type::quadrilateral) - { - const T u = xi[0]; - const T v = xi[1]; - weights = {(T(1) - u) * (T(1) - v), - u * (T(1) - v), - (T(1) - u) * v, - u * v}; - nweights = 4; - } - else - { - return {}; - } - - if (static_cast(vertices.size()) < nweights) - return {}; - for (int i = 0; i < nweights; ++i) - { - const int vertex = vertices[static_cast(i)]; - for (int d = 0; d < adapt_cell.tdim; ++d) - { - out[static_cast(d)] += - weights[static_cast(i)] - * adapt_cell.vertex_coords[ - static_cast(vertex * adapt_cell.tdim + d)]; - } - } - return out; -} - -template -T graph_signed_surface_jacobian_ratio(std::span host_du_phys, - std::span host_dv_phys, - std::span corr_du_phys, - std::span corr_dv_phys, - T tol) -{ - if (host_du_phys.size() != 3 || host_dv_phys.size() != 3 - || corr_du_phys.size() != 3 - || corr_dv_phys.size() != 3) - { - return std::numeric_limits::quiet_NaN(); - } - - const auto host_cross = geom::cross( - host_du_phys, host_dv_phys); - const T denom = - host_cross[0] * host_cross[0] - + host_cross[1] * host_cross[1] - + host_cross[2] * host_cross[2]; - if (denom <= tol * tol) - return std::numeric_limits::quiet_NaN(); - - const auto corr_cross = geom::cross(corr_du_phys, corr_dv_phys); - const T numer = - corr_cross[0] * host_cross[0] - + corr_cross[1] * host_cross[1] - + corr_cross[2] * host_cross[2]; - return numer / denom; -} - -template -bool check_zero_face_surface_jacobian( - const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span ordered_vertices, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics, - int source_cell_id) -{ - graph_criteria::FaceQualityReport report; - report.minimum_surface_jacobian_ratio = - std::numeric_limits::infinity(); - - const int dim = ls_cell.tdim; - if (dim != 3 || ls_cell.gdim != 3) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - const int zdim = - adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - const cell::type entity_type = - adapt_cell.entity_types[2][static_cast(zid)]; - if (entity_type != cell::type::triangle - && entity_type != cell::type::quadrilateral) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - const auto curving_options = graph_curving_options(graph_options); - curving::CurvingData curving_data; - const std::array parent_cell_ids = {static_cast(ls_cell.cell_id)}; - const std::array, 1> adapt_cells = {adapt_cell}; - const std::array, 1> level_set_cells = {ls_cell}; - const std::array ls_offsets = {0, 1}; - const auto& state = curving::ensure_curved( - curving_data, - std::span(parent_cell_ids.data(), parent_cell_ids.size()), - std::span>(adapt_cells.data(), adapt_cells.size()), - std::span>( - level_set_cells.data(), level_set_cells.size()), - std::span(ls_offsets.data(), ls_offsets.size()), - 0, - local_zero_entity_id, - curving_options); - if (state.status != curving::CurvingStatus::curved) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - std::vector jacobian; - if (!graph_affine_jacobian(ls_cell, jacobian)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - std::vector> samples; - if (entity_type == cell::type::triangle) - { - samples = {{{T(1) / T(3), T(1) / T(3)}, - {T(0.2), T(0.2)}, - {T(0.6), T(0.2)}, - {T(0.2), T(0.6)}}}; - } - else - { - samples = {{{T(0.5), T(0.5)}, - {T(0.25), T(0.25)}, - {T(0.75), T(0.25)}, - {T(0.25), T(0.75)}, - {T(0.75), T(0.75)}}}; - } - - const T h = T(1.0e-5); - auto eval_curved = [&](std::span xi) - { - return graph_eval_curved_zero_face_ref( - state, entity_type, curving_options.geometry_order, - curving_options.node_family, xi); - }; - auto eval_straight = [&](std::span xi) - { - return graph_eval_straight_zero_face_ref( - adapt_cell, ordered_vertices, entity_type, xi); - }; - - for (int sample_id = 0; sample_id < static_cast(samples.size()); ++sample_id) - { - const auto xi = samples[static_cast(sample_id)]; - const std::array xi_u_plus = {xi[0] + h, xi[1]}; - const std::array xi_u_minus = {xi[0] - h, xi[1]}; - const std::array xi_v_plus = {xi[0], xi[1] + h}; - const std::array xi_v_minus = {xi[0], xi[1] - h}; - - const auto curved_u_plus = eval_curved( - std::span(xi_u_plus.data(), xi_u_plus.size())); - const auto curved_u_minus = eval_curved( - std::span(xi_u_minus.data(), xi_u_minus.size())); - const auto curved_v_plus = eval_curved( - std::span(xi_v_plus.data(), xi_v_plus.size())); - const auto curved_v_minus = eval_curved( - std::span(xi_v_minus.data(), xi_v_minus.size())); - const auto straight_u_plus = eval_straight( - std::span(xi_u_plus.data(), xi_u_plus.size())); - const auto straight_u_minus = eval_straight( - std::span(xi_u_minus.data(), xi_u_minus.size())); - const auto straight_v_plus = eval_straight( - std::span(xi_v_plus.data(), xi_v_plus.size())); - const auto straight_v_minus = eval_straight( - std::span(xi_v_minus.data(), xi_v_minus.size())); - - if (curved_u_plus.empty() || curved_u_minus.empty() - || curved_v_plus.empty() || curved_v_minus.empty() - || straight_u_plus.empty() || straight_u_minus.empty() - || straight_v_plus.empty() || straight_v_minus.empty()) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - std::vector curved_du(static_cast(dim), T(0)); - std::vector curved_dv(static_cast(dim), T(0)); - std::vector straight_du(static_cast(dim), T(0)); - std::vector straight_dv(static_cast(dim), T(0)); - for (int d = 0; d < dim; ++d) - { - curved_du[static_cast(d)] = - (curved_u_plus[static_cast(d)] - - curved_u_minus[static_cast(d)]) / (T(2) * h); - curved_dv[static_cast(d)] = - (curved_v_plus[static_cast(d)] - - curved_v_minus[static_cast(d)]) / (T(2) * h); - straight_du[static_cast(d)] = - (straight_u_plus[static_cast(d)] - - straight_u_minus[static_cast(d)]) / (T(2) * h); - straight_dv[static_cast(d)] = - (straight_v_plus[static_cast(d)] - - straight_v_minus[static_cast(d)]) / (T(2) * h); - } - - const auto curved_du_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(curved_du.data(), curved_du.size())); - const auto curved_dv_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(curved_dv.data(), curved_dv.size())); - const auto straight_du_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(straight_du.data(), straight_du.size())); - const auto straight_dv_phys = graph_push_forward_vector( - jacobian, ls_cell.gdim, ls_cell.tdim, - std::span(straight_dv.data(), straight_dv.size())); - - const T ratio = graph_signed_surface_jacobian_ratio( - std::span(straight_du_phys.data(), straight_du_phys.size()), - std::span(straight_dv_phys.data(), straight_dv_phys.size()), - std::span(curved_du_phys.data(), curved_du_phys.size()), - std::span(curved_dv_phys.data(), curved_dv_phys.size()), - graph_options.criteria.tolerance); - if (!std::isfinite(ratio)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::face_degenerate); - return false; - } - - report.minimum_surface_jacobian_ratio = - std::min(report.minimum_surface_jacobian_ratio, ratio); - if (ratio <= graph_options.criteria.min_surface_jacobian_ratio) - { - report.accepted = false; - report.failure_reason = - graph_criteria::FailureReason::surface_jacobian_not_positive; - report.failed_triangle_index = sample_id; - report.failed_surface_jacobian_ratio = ratio; - diagnostics.min_face_area_ratio = - std::min(diagnostics.min_face_area_ratio, - report.minimum_surface_jacobian_ratio); - observe_failed_face_orientation(diagnostics, report); - int requested_edge_id = -1; - if (entity_type == cell::type::triangle - && ordered_vertices.size() >= 3) - { - const std::array bary = {T(1) - xi[0] - xi[1], xi[0], xi[1]}; - int closest = 0; - if (bary[1] < bary[closest]) - closest = 1; - if (bary[2] < bary[closest]) - closest = 2; - const int a = ordered_vertices[ - static_cast((closest + 1) % 3)]; - const int b = ordered_vertices[ - static_cast((closest + 2) % 3)]; - requested_edge_id = - graph_refinement_edge_from_failed_face_segment( - adapt_cell, a, b); - } - else if (entity_type == cell::type::quadrilateral - && ordered_vertices.size() >= 4) - { - std::array dist = {xi[1], T(1) - xi[0], T(1) - xi[1], xi[0]}; - int side = 0; - for (int i = 1; i < 4; ++i) - if (dist[static_cast(i)] - < dist[static_cast(side)]) - side = i; - const std::array, 4> side_vertices = {{ - {{0, 1}}, {{1, 3}}, {{2, 3}}, {{0, 2}}}}; - const int a = ordered_vertices[static_cast( - side_vertices[static_cast(side)][0])]; - const int b = ordered_vertices[static_cast( - side_vertices[static_cast(side)][1])]; - requested_edge_id = - graph_refinement_edge_from_failed_face_segment( - adapt_cell, a, b); - } - mark_graph_refinement_request( - diagnostics, 1, requested_edge_id); - mark_graph_failure( - diagnostics, source_cell_id, report.failure_reason); - return false; - } - } - - diagnostics.min_face_area_ratio = - std::min(diagnostics.min_face_area_ratio, - report.minimum_surface_jacobian_ratio); - return true; -} - -template -bool check_zero_face_graph(const AdaptCell& adapt_cell, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span ordered_vertices, - const std::vector& vertex_coords, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics, - int source_cell_id) -{ - ++diagnostics.checked_faces; - - std::vector face_vertices; - face_vertices.reserve(ordered_vertices.size() * static_cast(ls_cell.tdim)); - for (const int v : ordered_vertices) - { - auto p = point_span(vertex_coords, v, ls_cell.tdim); - face_vertices.insert(face_vertices.end(), p.begin(), p.end()); - } - - graph_criteria::HostFrame host; - if (!build_face_host_frame( - face_vertices, host, graph_options.criteria.tolerance)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_host_frame); - return false; - } - - const int nverts = static_cast(ordered_vertices.size()); - const int zdim = - adapt_cell.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - adapt_cell.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2 || zid < 0 || zid >= adapt_cell.n_entities(2)) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - const cell::type zero_face_type = - adapt_cell.entity_types[2][static_cast(zid)]; - const auto zero_face_edges = cell::edges(zero_face_type); - const geom::ParentEntity cell_interior{ls_cell.tdim, -1}; - int longest_face_edge_id = -1; - T longest_face_edge_length = T(0); - int face_interior_node_index = 1; - for (const auto& zero_face_edge : zero_face_edges) - { - const int ia = zero_face_edge[0]; - const int ib = zero_face_edge[1]; - if (ia < 0 || ib < 0 || ia >= nverts || ib >= nverts) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_input); - return false; - } - - const int a = ordered_vertices[static_cast(ia)]; - const int b = ordered_vertices[static_cast(ib)]; - const auto pa = point_span(vertex_coords, a, ls_cell.tdim); - const auto pb = point_span(vertex_coords, b, ls_cell.tdim); - const auto edge_delta = geom::subtract(pb, pa); - const T edge_length = geom::norm( - std::span(edge_delta.data(), edge_delta.size())); - const int adapt_edge_id = find_adapt_edge_by_vertices( - adapt_cell, a, b); - if (adapt_edge_id >= 0 && edge_length > longest_face_edge_length) - { - longest_face_edge_length = edge_length; - longest_face_edge_id = adapt_edge_id; - } - - const std::uint64_t face_zero_mask = - adapt_cell.zero_entity_zero_mask[ - static_cast(local_zero_entity_id)]; - const int zero_edge_id = - find_zero_edge_entity_by_vertices( - adapt_cell, a, b, face_zero_mask); - if (zero_edge_id < 0) - { - mark_graph_failure( - diagnostics, - source_cell_id, - graph_criteria::FailureReason::invalid_host_frame); - return false; - } - - int host_face_id = -1; - auto boundary_host_normal = - host_boundary_face_normal_for_zero_face_edge( - adapt_cell, - local_zero_entity_id, - pa, - pb, - graph_options.criteria.tolerance, - &host_face_id); - if (boundary_host_normal.empty() || host_face_id < 0) - { - // This is an interior zero edge, for example the artificial - // diagonal introduced by triangulating a zero quadrilateral. It - // still curves as a zero edge, but its admissible host is the - // uncut subcell volume and graph checks use face-interior rules. - AdaptCell edge_context = adapt_cell; - override_zero_edge_host_from_zero_face( - edge_context, - zero_edge_id, - local_zero_entity_id, - -1); - - const int order = std::max(graph_options.geometry_order, 1); - for (int k = 1; k < order; ++k) - { - const T s = T(k) / T(order); - std::vector seed(static_cast(ls_cell.tdim), T(0)); - for (int d = 0; d < ls_cell.tdim; ++d) - { - seed[static_cast(d)] = - (T(1) - s) * pa[static_cast(d)] - + s * pb[static_cast(d)]; - } - - std::vector corrected; - if (!project_graph_point( - edge_context, - zero_edge_id, - ls_cell, - cell_interior, - host, - std::span(seed.data(), seed.size()), - GraphNodeKind::face_interior, - face_interior_node_index++, - adapt_edge_id, - graph_options, - diagnostics, - source_cell_id, - corrected)) - { - return false; - } - } - continue; - } - - AdaptCell edge_context = adapt_cell; - override_zero_edge_host_from_zero_face( - edge_context, - zero_edge_id, - local_zero_entity_id, - host_face_id); - if (!check_zero_edge_graph( - edge_context, - zero_edge_id, - ls_cell, - pa, - pb, - std::span( - boundary_host_normal.data(), boundary_host_normal.size()), - true, - adapt_edge_id, - graph_options, - diagnostics, - source_cell_id)) - { - return false; - } - } - - std::vector face_center(static_cast(ls_cell.tdim), T(0)); - for (const int v : ordered_vertices) - { - const auto p = point_span(vertex_coords, v, ls_cell.tdim); - for (int d = 0; d < ls_cell.tdim; ++d) - face_center[static_cast(d)] += p[static_cast(d)]; - } - for (T& value : face_center) - value /= static_cast(nverts); - - std::vector corrected_face_center; - int face_center_refinement_edge_id = longest_face_edge_id; - std::vector face_center_refinement_point; - if (graph_options.refinement_mode - == GraphRefinementMode::green_orthogonal_surface_edge) - { - const int orthogonal_edge_id = - best_orthogonal_surface_edge_refinement_id( - adapt_cell, - local_zero_entity_id, - ls_cell, - ordered_vertices, - graph_options, - &face_center_refinement_point); - if (orthogonal_edge_id >= 0) - face_center_refinement_edge_id = orthogonal_edge_id; - } - else if (graph_options.refinement_mode - == GraphRefinementMode::green_midpoint_residual) - { - const int residual_edge_id = - best_midpoint_residual_surface_edge_refinement_id( - adapt_cell, - local_zero_entity_id, - ls_cell, - ordered_vertices, - graph_options, - &face_center_refinement_point); - if (residual_edge_id >= 0) - face_center_refinement_edge_id = residual_edge_id; - } - else if (graph_options.refinement_mode - == GraphRefinementMode::green_normal_variation) - { - const int normal_variation_edge_id = - best_normal_variation_surface_edge_refinement_id( - adapt_cell, - local_zero_entity_id, - ls_cell, - ordered_vertices, - graph_options, - &face_center_refinement_point); - if (normal_variation_edge_id >= 0) - face_center_refinement_edge_id = normal_variation_edge_id; - } - if (!project_graph_point( - adapt_cell, - local_zero_entity_id, - ls_cell, - cell_interior, - host, - std::span(face_center.data(), face_center.size()), - GraphNodeKind::face_interior, - 0, - face_center_refinement_edge_id, - graph_options, - diagnostics, - source_cell_id, - corrected_face_center)) - { - mark_graph_refinement_point( - diagnostics, - std::span( - face_center_refinement_point.data(), - face_center_refinement_point.size())); - return false; - } - - return check_zero_face_surface_jacobian( - adapt_cell, - local_zero_entity_id, - ls_cell, - ordered_vertices, - graph_options, - diagnostics, - source_cell_id); -} - -template -std::vector adapt_zero_entity_vertex_ids(const AdaptCell& ac, - int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim == 0) - return {zid}; - - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - std::vector out; - out.reserve(verts.size()); - for (const auto v : verts) - out.push_back(static_cast(v)); - return out; -} - -inline bool contains_vertex_id(std::span vertices, int vertex_id) -{ - for (const int v : vertices) - { - if (v == vertex_id) - return true; - } - return false; -} - -template -std::vector face_normal_for_zero_entity(const AdaptCell& ac, - int local_zero_entity_id, - T tol) -{ - std::vector normal(static_cast(ac.tdim), T(0)); - if (ac.tdim != 3 - || ac.zero_entity_dim[static_cast(local_zero_entity_id)] != 2) - { - return normal; - } - - const auto face_vertices = - adapt_zero_entity_vertex_ids(ac, local_zero_entity_id); - std::vector face_coords; - face_coords.reserve(face_vertices.size() * 3); - for (const int v : face_vertices) - { - auto p = point_span(ac.vertex_coords, v, 3); - face_coords.insert(face_coords.end(), p.begin(), p.end()); - } - - graph_criteria::HostFrame host; - if (!build_face_host_frame(face_coords, host, tol)) - return normal; - return host.normal; -} - -template -std::vector face_normal_for_zero_edge(const AdaptCell& ac, - int local_zero_edge_id, - int level_set_id, - T tol) -{ - std::vector normal(static_cast(ac.tdim), T(0)); - if (ac.tdim != 3 - || ac.zero_entity_dim[static_cast(local_zero_edge_id)] != 1) - { - return normal; - } - - const auto edge_vertices = - adapt_zero_entity_vertex_ids(ac, local_zero_edge_id); - if (edge_vertices.size() != 2) - return normal; - - const std::uint64_t bit = std::uint64_t(1) << level_set_id; - T best_norm = T(0); - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (ac.zero_entity_dim[static_cast(z)] != 2) - continue; - if ((ac.zero_entity_zero_mask[static_cast(z)] & bit) == 0) - continue; - - const auto face_vertices = adapt_zero_entity_vertex_ids(ac, z); - if (!contains_vertex_id(std::span(face_vertices.data(), face_vertices.size()), - edge_vertices[0]) - || !contains_vertex_id( - std::span(face_vertices.data(), face_vertices.size()), - edge_vertices[1])) - { - continue; - } - - auto candidate = face_normal_for_zero_entity(ac, z, tol); - const T n = geom::norm( - std::span(candidate.data(), candidate.size())); - if (n > best_norm) - { - best_norm = n; - normal = std::move(candidate); - } - } - - if (best_norm <= tol) - std::fill(normal.begin(), normal.end(), T(0)); - return normal; -} - -template -ZeroEntityGraphDiagnostics make_zero_entity_graph_record( - int local_zero_entity_id, - int level_set_id, - int dimension, - std::uint64_t zero_mask, - int host_cell_id, - cell::type host_cell_type, - int host_face_id, - int source_level_set, - const ReadyCellGraphDiagnostics& entity_diag) -{ - ZeroEntityGraphDiagnostics record; - record.local_zero_entity_id = local_zero_entity_id; - record.level_set_id = level_set_id; - record.dimension = dimension; - record.zero_mask = zero_mask; - record.host_cell_id = host_cell_id; - record.host_cell_type = host_cell_type; - record.host_face_id = host_face_id; - record.source_level_set = source_level_set; - record.accepted = entity_diag.accepted; - record.checked_edges = entity_diag.checked_edges; - record.checked_faces = entity_diag.checked_faces; - record.failed_checks = entity_diag.failed_checks; - record.failure_reason = entity_diag.accepted - ? graph_criteria::FailureReason::none - : entity_diag.first_failure_reason; - record.min_true_transversality = entity_diag.min_true_transversality; - record.min_host_normal_alignment = entity_diag.min_host_normal_alignment; - record.max_drift_amplification = entity_diag.max_drift_amplification; - record.max_relative_correction_distance = - entity_diag.max_relative_correction_distance; - record.max_relative_tangential_shift = - entity_diag.max_relative_tangential_shift; - record.min_edge_gap_ratio = entity_diag.min_edge_gap_ratio; - record.min_face_area_ratio = entity_diag.min_face_area_ratio; - record.min_level_set_gradient_host_alignment = - entity_diag.min_level_set_gradient_host_alignment; - record.failed_face_triangle_index = - entity_diag.first_failed_face_triangle_index; - record.failed_face_area_ratio = - entity_diag.first_failed_face_area_ratio; - record.failed_projection_seed = entity_diag.first_failed_projection_seed; - record.failed_projection_direction = - entity_diag.first_failed_projection_direction; - record.failed_projection_clip_lo = - entity_diag.first_failed_projection_clip_lo; - record.failed_projection_clip_hi = - entity_diag.first_failed_projection_clip_hi; - record.failed_projection_root_t = - entity_diag.first_failed_projection_root_t; - record.requested_refinement_entity_dim = - entity_diag.first_requested_refinement_entity_dim; - record.requested_refinement_entity_id = - entity_diag.first_requested_refinement_entity_id; - record.requested_refinement_point = - entity_diag.first_requested_refinement_point; - record.nodes = entity_diag.nodes; - return record; -} - -template -void accumulate_zero_entity_graph_record(ReadyCellGraphDiagnostics& diagnostics, - const ZeroEntityGraphDiagnostics& record, - int source_cell_id) -{ - diagnostics.checked_edges += record.checked_edges; - diagnostics.checked_faces += record.checked_faces; - diagnostics.min_true_transversality = - std::min(diagnostics.min_true_transversality, - record.min_true_transversality); - diagnostics.min_host_normal_alignment = - std::min(diagnostics.min_host_normal_alignment, - record.min_host_normal_alignment); - diagnostics.max_drift_amplification = - std::max(diagnostics.max_drift_amplification, - record.max_drift_amplification); - diagnostics.max_relative_correction_distance = - std::max(diagnostics.max_relative_correction_distance, - record.max_relative_correction_distance); - diagnostics.max_relative_tangential_shift = - std::max(diagnostics.max_relative_tangential_shift, - record.max_relative_tangential_shift); - diagnostics.min_edge_gap_ratio = - std::min(diagnostics.min_edge_gap_ratio, record.min_edge_gap_ratio); - diagnostics.min_face_area_ratio = - std::min(diagnostics.min_face_area_ratio, record.min_face_area_ratio); - diagnostics.min_level_set_gradient_host_alignment = - std::min(diagnostics.min_level_set_gradient_host_alignment, - record.min_level_set_gradient_host_alignment); - if (diagnostics.first_failed_face_triangle_index < 0 - && record.failed_face_triangle_index >= 0) - { - diagnostics.first_failed_face_triangle_index = - record.failed_face_triangle_index; - diagnostics.first_failed_face_area_ratio = - record.failed_face_area_ratio; - } - for (const auto& node : record.nodes) - diagnostics.nodes.push_back(node); - if (!record.accepted) - { - mark_graph_refinement_request( - diagnostics, - record.requested_refinement_entity_dim, - record.requested_refinement_entity_id); - mark_graph_refinement_point( - diagnostics, - std::span( - record.requested_refinement_point.data(), - record.requested_refinement_point.size())); - const int failed_cell_id = - source_cell_id >= 0 ? source_cell_id : record.host_cell_id; - mark_graph_failure(diagnostics, failed_cell_id, record.failure_reason); - } -} - -template -void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics) -{ - const int graph_refinements = diagnostics.graph_refinements; - diagnostics = ReadyCellGraphDiagnostics{}; - diagnostics.graph_refinements = graph_refinements; - if (!graph_options.enabled) - return; - - const std::uint64_t bit = std::uint64_t(1) << level_set_id; - for (int z = 0; z < adapt_cell.n_zero_entities(); ++z) - { - if ((adapt_cell.zero_entity_zero_mask[static_cast(z)] & bit) == 0) - continue; - - const int zdim = adapt_cell.zero_entity_dim[static_cast(z)]; - if (zdim != 1 && zdim != 2) - continue; - - ReadyCellGraphDiagnostics entity_diag; - const auto vertices = adapt_zero_entity_vertex_ids(adapt_cell, z); - if (zdim == 1) - { - if (vertices.size() != 2) - { - mark_graph_failure( - entity_diag, -1, graph_criteria::FailureReason::invalid_input); - } - else - { - const auto zero_face_normal = - face_normal_for_zero_edge( - adapt_cell, z, level_set_id, - graph_options.criteria.tolerance); - (void)check_zero_edge_graph( - adapt_cell, - z, - ls_cell, - point_span(adapt_cell.vertex_coords, vertices[0], adapt_cell.tdim), - point_span(adapt_cell.vertex_coords, vertices[1], adapt_cell.tdim), - std::span(zero_face_normal.data(), zero_face_normal.size()), - false, - find_adapt_edge_by_vertices( - adapt_cell, vertices[0], vertices[1]), - graph_options, - entity_diag, - -1); - } - } - else - { - if (adapt_cell.tdim != 3 || vertices.size() < 3) - { - mark_graph_failure( - entity_diag, -1, graph_criteria::FailureReason::invalid_input); - } - else - { - const auto face_normal = - face_normal_for_zero_entity( - adapt_cell, z, graph_options.criteria.tolerance); - const T normal_norm = geom::norm( - std::span(face_normal.data(), face_normal.size())); - if (normal_norm <= graph_options.criteria.tolerance) - { - mark_graph_failure( - entity_diag, -1, - graph_criteria::FailureReason::invalid_host_frame); - } - else - { - (void)check_zero_face_graph( - adapt_cell, - z, - ls_cell, - std::span(vertices.data(), vertices.size()), - adapt_cell.vertex_coords, - graph_options, - entity_diag, - -1); - } - } - } - - auto record = - make_zero_entity_graph_record( - z, - level_set_id, - zdim, - adapt_cell.zero_entity_zero_mask[static_cast(z)], - z < static_cast(adapt_cell.zero_entity_host_cell_id.size()) - ? adapt_cell.zero_entity_host_cell_id[static_cast(z)] - : -1, - z < static_cast(adapt_cell.zero_entity_host_cell_type.size()) - ? adapt_cell.zero_entity_host_cell_type[static_cast(z)] - : cell::type::point, - z < static_cast(adapt_cell.zero_entity_host_face_id.size()) - ? adapt_cell.zero_entity_host_face_id[static_cast(z)] - : -1, - z < static_cast(adapt_cell.zero_entity_source_level_set.size()) - ? adapt_cell.zero_entity_source_level_set[static_cast(z)] - : -1, - entity_diag); - accumulate_zero_entity_graph_record(diagnostics, record, -1); - diagnostics.zero_entities.push_back(std::move(record)); - } -} - -template -int find_top_cell_incident_to_edge(const AdaptCell& adapt_cell, int edge_id) -{ - if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) - return -1; - - const auto edge_vertices = - adapt_cell.entity_to_vertex[1][static_cast(edge_id)]; - if (edge_vertices.size() != 2) - return -1; - - const int a = static_cast(edge_vertices[0]); - const int b = static_cast(edge_vertices[1]); - const int tdim = adapt_cell.tdim; - for (int c = 0; c < adapt_cell.n_entities(tdim); ++c) - { - const auto verts = - adapt_cell.entity_to_vertex[tdim][static_cast(c)]; - bool has_a = false; - bool has_b = false; - for (const auto v : verts) - { - has_a = has_a || static_cast(v) == a; - has_b = has_b || static_cast(v) == b; - } - if (has_a && has_b) - return c; - } - return -1; -} - -template -ReadyCellGraphDiagnostics check_processed_ready_to_cut_cell_graphs( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - T zero_tol, - T sign_tol, - int edge_max_depth, - bool triangulate_cut_parts, - const ReadyCellGraphOptions& graph_options) -{ - ReadyCellGraphDiagnostics diagnostics; - if (!graph_options.enabled) - return diagnostics; - - bool has_ready = false; - const int tdim = adapt_cell.tdim; - for (int c = 0; c < adapt_cell.n_entities(tdim); ++c) - { - if (adapt_cell.get_cell_cert_tag(level_set_id, c) - == CellCertTag::ready_to_cut) - { - has_ready = true; - break; - } - } - if (!has_ready) - return diagnostics; - - AdaptCell temporary = adapt_cell; - process_ready_to_cut_cells( - temporary, - ls_cell, - level_set_id, - zero_tol, - sign_tol, - edge_max_depth, - triangulate_cut_parts); - fill_all_vertex_signs_from_level_set( - temporary, ls_cell, level_set_id, zero_tol); - build_edges(temporary); - if (temporary.tdim == 3) - build_faces(temporary); - recompute_active_level_set_masks(temporary, level_set_id + 1); - rebuild_zero_entity_inventory(temporary); - - populate_committed_zero_entity_graph_diagnostics( - temporary, ls_cell, level_set_id, graph_options, diagnostics); - - if (diagnostics.zero_entities.empty()) - { - mark_graph_failure( - diagnostics, -1, graph_criteria::FailureReason::invalid_input); - } - - if (!diagnostics.accepted && diagnostics.first_failed_cell < 0 - && diagnostics.first_requested_refinement_entity_dim == 1) - { - diagnostics.first_failed_cell = find_top_cell_incident_to_edge( - adapt_cell, diagnostics.first_requested_refinement_entity_id); - } - - return diagnostics; -} - -// ===================================================================== -// process_ready_to_cut_cells -// ===================================================================== - -template -void process_ready_to_cut_cells(AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - T zero_tol, - T sign_tol, - int edge_max_depth, - bool triangulate_cut_parts) -{ - const int tdim = adapt_cell.tdim; - const int n_cells = adapt_cell.n_entities(tdim); - - bool has_ready = false; - for (int c = 0; c < n_cells; ++c) - { - if (adapt_cell.get_cell_cert_tag(level_set_id, c) == CellCertTag::ready_to_cut) - { - has_ready = true; - break; - } - } - if (!has_ready) - return; - - const std::vector old_types = adapt_cell.entity_types[tdim]; - const EntityAdjacency old_cells = adapt_cell.entity_to_vertex[tdim]; - const auto edge_lookup = build_leaf_edge_lookup(adapt_cell); - - EntityAdjacency new_cells; - new_cells.offsets.push_back(0); - std::vector new_types; - std::vector old_cell_ids_for_new_cells; - std::vector source_cell_ids_for_new_cells; - std::vector refinement_reasons_for_new_cells; - std::vector explicit_current_ls_tags; - - for (int c = 0; c < n_cells; ++c) - { - auto old_cell_vertices = old_cells[static_cast(c)]; - const cell::type leaf_cell_type = old_types[static_cast(c)]; - - if (adapt_cell.get_cell_cert_tag(level_set_id, c) != CellCertTag::ready_to_cut) - { - std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); - append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); - old_cell_ids_for_new_cells.push_back(c); - source_cell_ids_for_new_cells.push_back(c); - refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); - explicit_current_ls_tags.push_back(CellCertTag::not_classified); - continue; - } - - if (leaf_cell_type != cell::type::triangle - && leaf_cell_type != cell::type::tetrahedron) - { - throw std::runtime_error( - "process_ready_to_cut_cells: ready_to_cut only implemented for triangle and tetrahedron leaves"); - } - - std::vector vertex_coords( - static_cast(old_cell_vertices.size() * tdim), T(0)); - for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) - { - const int gv = old_cell_vertices[j]; - for (int d = 0; d < tdim; ++d) - { - vertex_coords[static_cast(j * tdim + d)] = - adapt_cell.vertex_coords[static_cast(gv * tdim + d)]; - } - } - const std::vector ls_values = - gather_leaf_cell_vertex_level_set_values(adapt_cell, ls_cell, c); - - const std::uint64_t current_level_set_bit = std::uint64_t(1) << level_set_id; - std::vector current_level_set_zero(old_cell_vertices.size(), false); - bool has_strict_negative = false; - bool has_strict_positive = false; - bool has_zero = false; - for (std::size_t j = 0; j < ls_values.size(); ++j) - { - const T value = ls_values[j]; - const int gv = old_cell_vertices[j]; - current_level_set_zero[j] = - (adapt_cell.zero_mask_per_vertex[static_cast(gv)] - & current_level_set_bit) != 0; - has_strict_negative = has_strict_negative || value < -zero_tol; - has_strict_positive = has_strict_positive || value > zero_tol; - has_zero = has_zero || current_level_set_zero[j]; - } - if (!(has_strict_negative && has_strict_positive)) - { - std::vector copy(old_cell_vertices.begin(), old_cell_vertices.end()); - append_top_cell_local(new_types, new_cells, leaf_cell_type, std::span(copy)); - old_cell_ids_for_new_cells.push_back(c); - CellCertTag copied_tag = CellCertTag::zero; - if (has_strict_negative) - copied_tag = CellCertTag::negative; - else if (has_strict_positive) - copied_tag = CellCertTag::positive; - else if (!has_zero) - copied_tag = CellCertTag::not_classified; - explicit_current_ls_tags.push_back(copied_tag); - continue; - } - - std::array old_edge_ids_by_local_edge; - old_edge_ids_by_local_edge.fill(-1); - const auto ledges = cell::edges(leaf_cell_type); - for (std::size_t le = 0; le < ledges.size(); ++le) - { - const int a = old_cell_vertices[static_cast(ledges[le][0])]; - const int b = old_cell_vertices[static_cast(ledges[le][1])]; - const std::pair key = {std::min(a, b), std::max(a, b)}; - auto edge_it = edge_lookup.find(key); - if (edge_it == edge_lookup.end()) - { - throw std::runtime_error( - "process_ready_to_cut_cells: missing leaf edge for ready-to-cut cell " - + std::to_string(c) + " type=" - + cell_type_to_str(leaf_cell_type) + " local_edge=" - + std::to_string(le) + " key=(" - + std::to_string(key.first) + "," - + std::to_string(key.second) + ")"); - } - old_edge_ids_by_local_edge[le] = edge_it->second; - } - - std::map token_to_vertex; - for (std::size_t j = 0; j < old_cell_vertices.size(); ++j) - token_to_vertex[100 + static_cast(j)] = old_cell_vertices[j]; - for (std::size_t le = 0; le < ledges.size(); ++le) - { - const int old_edge_id = old_edge_ids_by_local_edge[le]; - if (old_edge_id < 0) - continue; - if (adapt_cell.get_edge_root_tag(level_set_id, old_edge_id) - != EdgeRootTag::one_root) - { - continue; + continue; } const int root_vertex_id = ensure_one_root_vertex_on_edge( @@ -4746,6 +1663,126 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, } else { + if (has_zero) + { + auto append_zero_vertex_tetra_side = + [&](bool negative_side, CellCertTag side_tag) + { + std::vector zero_vertices; + std::vector inside_vertices; + std::vector outside_vertices; + + for (int lv = 0; lv < 4; ++lv) + { + const T value = ls_values[static_cast(lv)]; + if (current_level_set_zero[static_cast(lv)]) + { + zero_vertices.push_back(lv); + continue; + } + + const bool inside = + negative_side ? (value < -zero_tol) : (value > zero_tol); + if (inside) + inside_vertices.push_back(lv); + else + outside_vertices.push_back(lv); + } + + auto edge_token = [&](int a, int b) + { + const int token = + basix_edge_id_for_vertices(leaf_cell_type, a, b); + if (token < 0) + { + throw std::runtime_error( + "process_ready_to_cut_cells: missing tetra edge token"); + } + return token; + }; + + auto append_tet_tokens = [&](const std::array& tokens) + { + std::array mapped; + for (std::size_t j = 0; j < tokens.size(); ++j) + { + auto token_it = token_to_vertex.find(tokens[j]); + if (token_it == token_to_vertex.end()) + { + throw std::runtime_error( + "process_ready_to_cut_cells: missing zero-vertex " + "tetra token " + std::to_string(tokens[j])); + } + mapped[j] = token_it->second; + } + + std::set unique(mapped.begin(), mapped.end()); + if (unique.size() != mapped.size()) + return; + + append_top_cell_local( + new_types, new_cells, cell::type::tetrahedron, + std::span(mapped)); + old_cell_ids_for_new_cells.push_back(-1); + source_cell_ids_for_new_cells.push_back(c); + refinement_reasons_for_new_cells.push_back( + CellRefinementReason::cut_level_set); + explicit_current_ls_tags.push_back(side_tag); + }; + + if (zero_vertices.size() == 1) + { + const int z = zero_vertices[0]; + if (inside_vertices.size() == 1 + && outside_vertices.size() == 2) + { + const int a = inside_vertices[0]; + append_tet_tokens({ + 100 + z, + 100 + a, + edge_token(a, outside_vertices[0]), + edge_token(a, outside_vertices[1]), + }); + return; + } + + if (inside_vertices.size() == 2 + && outside_vertices.size() == 1) + { + const int a = inside_vertices[0]; + const int b = inside_vertices[1]; + const int o = outside_vertices[0]; + const int ra = edge_token(a, o); + const int rb = edge_token(b, o); + append_tet_tokens({100 + z, 100 + a, 100 + b, rb}); + append_tet_tokens({100 + z, 100 + a, rb, ra}); + return; + } + } + + if (zero_vertices.size() == 2 + && inside_vertices.size() == 1 + && outside_vertices.size() == 1) + { + append_tet_tokens({ + 100 + zero_vertices[0], + 100 + zero_vertices[1], + 100 + inside_vertices[0], + edge_token(inside_vertices[0], outside_vertices[0]), + }); + return; + } + + throw std::runtime_error( + "process_ready_to_cut_cells: unsupported zero-vertex " + "tetrahedron clipping case"); + }; + + append_zero_vertex_tetra_side(true, CellCertTag::negative); + append_zero_vertex_tetra_side(false, CellCertTag::positive); + continue; + } + cell::CutCell negative_part; cell::CutCell positive_part; cell::tetrahedron::cut( @@ -4787,108 +1824,20 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, == CellRefinementReason::cut_level_set && c < static_cast(adapt_cell.entity_source_level_set[tdim].size())) { - adapt_cell.entity_source_level_set[tdim][static_cast(c)] = - static_cast(level_set_id); - } - } - - const int new_num_cells = adapt_cell.n_entities(tdim); - for (int c = 0; c < new_num_cells; ++c) - { - if (explicit_current_ls_tags[static_cast(c)] == CellCertTag::not_classified) - continue; - adapt_cell.set_cell_cert_tag( - level_set_id, c, explicit_current_ls_tags[static_cast(c)]); - } - -} - -template -ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - const ReadyCellGraphOptions& graph_options) -{ - ReadyCellGraphDiagnostics diagnostics; - - if (!graph_options.enabled) - return diagnostics; - - const int tdim = adapt_cell.tdim; - const int n_cells = adapt_cell.n_entities(tdim); - - for (int c = 0; c < n_cells; ++c) - { - if (adapt_cell.get_cell_cert_tag(level_set_id, c) - != CellCertTag::ready_to_cut) - { - continue; - } - - ++diagnostics.checked_cells; - - const cell::type leaf_cell_type = - adapt_cell.entity_types[tdim][static_cast(c)]; - if (leaf_cell_type != cell::type::triangle - && leaf_cell_type != cell::type::tetrahedron) - { - mark_graph_failure( - diagnostics, - c, - graph_criteria::FailureReason::invalid_input); - return diagnostics; - } - - AdaptCell temporary = adapt_cell; - for (int other = 0; other < n_cells; ++other) - { - if (other != c - && temporary.get_cell_cert_tag(level_set_id, other) - == CellCertTag::ready_to_cut) - { - temporary.set_cell_cert_tag( - level_set_id, other, CellCertTag::not_classified); - } - } - - process_ready_to_cut_cells( - temporary, - ls_cell, - level_set_id, - graph_options.criteria.tolerance, - graph_options.criteria.tolerance, - /*edge_max_depth=*/20, - /*triangulate_cut_parts=*/false); - - ReadyCellGraphDiagnostics temporary_zero_diag; - populate_committed_zero_entity_graph_diagnostics( - temporary, - ls_cell, - level_set_id, - graph_options, - temporary_zero_diag); - - bool checked_zero_entity = false; - for (const auto& record : temporary_zero_diag.zero_entities) - { - checked_zero_entity = true; - accumulate_zero_entity_graph_record(diagnostics, record, c); - if (!diagnostics.accepted) - return diagnostics; + adapt_cell.entity_source_level_set[tdim][static_cast(c)] = + static_cast(level_set_id); } + } - if (!checked_zero_entity) - { - mark_graph_failure( - diagnostics, - c, - graph_criteria::FailureReason::invalid_input); - return diagnostics; - } + const int new_num_cells = adapt_cell.n_entities(tdim); + for (int c = 0; c < new_num_cells; ++c) + { + if (explicit_current_ls_tags[static_cast(c)] == CellCertTag::not_classified) + continue; + adapt_cell.set_cell_cert_tag( + level_set_id, c, explicit_current_ls_tags[static_cast(c)]); } - return diagnostics; } template @@ -4957,355 +1906,6 @@ bool refine_ready_cell_on_largest_midpoint_value( return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); } -template -T graph_requested_edge_split_parameter( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int edge_id, - T zero_tol, - T sign_tol, - int edge_max_depth) -{ - const T fallback = T(0.5); - const int n_edges = adapt_cell.n_entities(1); - if (edge_id < 0 || edge_id >= n_edges) - return fallback; - - const T endpoint_tol = - std::max(zero_tol, T(64) * std::numeric_limits::epsilon()); - auto usable = [&](T t) - { - return std::isfinite(t) && t > endpoint_tol - && t < T(1) - endpoint_tol; - }; - - const auto idx = - static_cast(level_set_id * n_edges + edge_id); - if (idx < adapt_cell.edge_one_root_has_value.size() - && adapt_cell.edge_one_root_has_value[idx] - && idx < adapt_cell.edge_one_root_param.size() - && usable(adapt_cell.edge_one_root_param[idx])) - { - return adapt_cell.edge_one_root_param[idx]; - } - - if (adapt_cell.get_edge_root_tag(level_set_id, edge_id) - != EdgeRootTag::one_root) - { - return fallback; - } - - std::vector edge_coeffs; - gather_adapt_edge_bernstein(adapt_cell, ls_cell, edge_id, edge_coeffs); - - T root_t = fallback; - if (locate_one_root_parameter( - std::span(edge_coeffs), zero_tol, sign_tol, - edge_max_depth, root_t) - && usable(root_t)) - { - return root_t; - } - - return fallback; -} - -template -bool refine_green_on_requested_graph_edge(AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int edge_id, - T zero_tol, - T sign_tol, - int edge_max_depth) -{ - if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) - return false; - - const int n_edges = adapt_cell.n_entities(1); - if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) - adapt_cell.resize_edge_root_tags(level_set_id + 1); - if (adapt_cell.edge_green_split_has_value.size() - < static_cast((level_set_id + 1) * n_edges)) - { - adapt_cell.resize_green_split_data(level_set_id + 1); - } - - const auto idx = - static_cast(level_set_id * n_edges + edge_id); - adapt_cell.set_edge_root_tag( - level_set_id, edge_id, EdgeRootTag::multiple_roots); - adapt_cell.edge_green_split_param[idx] = - graph_requested_edge_split_parameter( - adapt_cell, ls_cell, level_set_id, edge_id, - zero_tol, sign_tol, edge_max_depth); - adapt_cell.edge_green_split_has_value[idx] = 1; - return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); -} - -template -bool refine_green_on_requested_graph_edge_midpoint(AdaptCell& adapt_cell, - int level_set_id, - int edge_id) -{ - if (edge_id < 0 || edge_id >= adapt_cell.n_entities(1)) - return false; - - const int n_edges = adapt_cell.n_entities(1); - if (adapt_cell.edge_root_tag_num_level_sets <= level_set_id) - adapt_cell.resize_edge_root_tags(level_set_id + 1); - if (adapt_cell.edge_green_split_has_value.size() - < static_cast((level_set_id + 1) * n_edges)) - { - adapt_cell.resize_green_split_data(level_set_id + 1); - } - - const auto idx = - static_cast(level_set_id * n_edges + edge_id); - adapt_cell.set_edge_root_tag( - level_set_id, edge_id, EdgeRootTag::multiple_roots); - adapt_cell.edge_green_split_param[idx] = T(0.5); - adapt_cell.edge_green_split_has_value[idx] = 1; - return refine_green_on_multiple_root_edges(adapt_cell, level_set_id); -} - -template -T graph_tetra_signed_volume6(const AdaptCell& adapt_cell, - std::span tet) -{ - if (adapt_cell.tdim != 3 || tet.size() != 4) - return T(0); - const auto p0 = point_span(adapt_cell.vertex_coords, tet[0], 3); - const auto p1 = point_span(adapt_cell.vertex_coords, tet[1], 3); - const auto p2 = point_span(adapt_cell.vertex_coords, tet[2], 3); - const auto p3 = point_span(adapt_cell.vertex_coords, tet[3], 3); - std::array a = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - std::array b = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - std::array c = {p3[0] - p0[0], p3[1] - p0[1], p3[2] - p0[2]}; - const auto axb = geom::cross( - std::span(a.data(), a.size()), - std::span(b.data(), b.size())); - return geom::dot( - std::span(axb.data(), axb.size()), - std::span(c.data(), c.size())); -} - -template -bool graph_point_barycentric_in_tetra( - const AdaptCell& adapt_cell, - std::span tet_vertices, - std::span point, - std::array& bary, - T tol) -{ - if (adapt_cell.tdim != 3 || tet_vertices.size() != 4 || point.size() != 3) - return false; - - const auto p0 = point_span(adapt_cell.vertex_coords, tet_vertices[0], 3); - std::vector A(9, T(0)); - std::vector rhs(3, T(0)); - for (int col = 0; col < 3; ++col) - { - const auto pc = point_span( - adapt_cell.vertex_coords, tet_vertices[static_cast(col + 1)], 3); - for (int row = 0; row < 3; ++row) - { - A[static_cast(row * 3 + col)] = - pc[static_cast(row)] - p0[static_cast(row)]; - } - } - for (int row = 0; row < 3; ++row) - rhs[static_cast(row)] = - point[static_cast(row)] - p0[static_cast(row)]; - - std::vector x; - if (!solve_graph_dense_small(std::move(A), std::move(rhs), 3, x, tol)) - return false; - - bary[1] = x[0]; - bary[2] = x[1]; - bary[3] = x[2]; - bary[0] = T(1) - bary[1] - bary[2] - bary[3]; - - T sum = T(0); - for (const T value : bary) - { - if (value < -tol || value > T(1) + tol) - return false; - sum += value; - } - return std::fabs(sum - T(1)) <= T(16) * tol; -} - -template -bool refine_green_on_requested_graph_point(AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int failed_cell_id, - std::span point, - T zero_tol) -{ - if (adapt_cell.tdim != 3 || point.size() != 3) - return false; - - const T tol = std::max(zero_tol, T(1024) * std::numeric_limits::epsilon()); - struct SplitCell - { - int cell_id = -1; - std::array bary = {}; - }; - std::vector split_cells; - - auto consider_cell = [&](int c) - { - if (c < 0 || c >= adapt_cell.n_entities(3)) - return; - if (adapt_cell.entity_types[3][static_cast(c)] - != cell::type::tetrahedron) - { - return; - } - const auto verts = - adapt_cell.entity_to_vertex[3][static_cast(c)]; - std::array bary = {}; - if (graph_point_barycentric_in_tetra( - adapt_cell, verts, point, bary, tol)) - { - split_cells.push_back({c, bary}); - } - }; - - consider_cell(failed_cell_id); - if (split_cells.empty()) - { - for (int c = 0; c < adapt_cell.n_entities(3); ++c) - consider_cell(c); - } - if (split_cells.empty()) - return false; - - for (int v = 0; v < adapt_cell.n_vertices(); ++v) - { - const auto existing = point_span(adapt_cell.vertex_coords, v, 3); - const auto delta = geom::subtract(existing, point); - if (geom::norm(std::span(delta.data(), delta.size())) <= tol) - return false; - } - - std::vector parent_param(point.begin(), point.end()); - const int new_v = append_vertex_with_parent_info( - adapt_cell, - point, - adapt_cell.tdim, - adapt_cell.parent_cell_id, - std::span(parent_param.data(), parent_param.size()), - -1); - set_vertex_sign_for_level_set( - adapt_cell, - new_v, - level_set_id, - ls_cell.value(point), - zero_tol); - - std::set split_cell_ids; - for (const auto& split : split_cells) - split_cell_ids.insert(split.cell_id); - - EntityAdjacency new_cells; - new_cells.offsets.push_back(0); - std::vector new_types; - std::vector old_cell_ids_for_new_cells; - std::vector source_cell_ids_for_new_cells; - std::vector refinement_reasons_for_new_cells; - - const std::array, 4> opposite_faces = {{ - {{1, 2, 3}}, - {{0, 2, 3}}, - {{0, 1, 3}}, - {{0, 1, 2}}, - }}; - - for (int c = 0; c < adapt_cell.n_entities(3); ++c) - { - const auto verts = - adapt_cell.entity_to_vertex[3][static_cast(c)]; - if (split_cell_ids.find(c) == split_cell_ids.end()) - { - std::vector copy(verts.begin(), verts.end()); - append_top_cell_local( - new_types, new_cells, adapt_cell.entity_types[3][static_cast(c)], - std::span(copy.data(), copy.size())); - old_cell_ids_for_new_cells.push_back(c); - source_cell_ids_for_new_cells.push_back(c); - refinement_reasons_for_new_cells.push_back(CellRefinementReason::none); - continue; - } - - std::array old_tet = { - static_cast(verts[0]), - static_cast(verts[1]), - static_cast(verts[2]), - static_cast(verts[3])}; - const T old_volume = graph_tetra_signed_volume6( - adapt_cell, - std::span(old_tet.data(), old_tet.size())); - - for (const auto& face : opposite_faces) - { - std::array child = { - new_v, - old_tet[static_cast(face[0])], - old_tet[static_cast(face[1])], - old_tet[static_cast(face[2])]}; - T child_volume = graph_tetra_signed_volume6( - adapt_cell, - std::span(child.data(), child.size())); - if (std::fabs(child_volume) <= tol * std::max(T(1), std::fabs(old_volume))) - continue; - if (old_volume * child_volume < T(0)) - { - std::swap(child[1], child[2]); - child_volume = -child_volume; - } - (void)child_volume; - append_top_cell_local( - new_types, new_cells, cell::type::tetrahedron, - std::span(child.data(), child.size())); - old_cell_ids_for_new_cells.push_back(-1); - source_cell_ids_for_new_cells.push_back(c); - refinement_reasons_for_new_cells.push_back( - CellRefinementReason::graph_green_edge); - } - } - - apply_topology_update_preserve_certification( - adapt_cell, - std::move(new_types), - std::move(new_cells), - std::span( - old_cell_ids_for_new_cells.data(), old_cell_ids_for_new_cells.size()), - std::span( - source_cell_ids_for_new_cells.data(), source_cell_ids_for_new_cells.size()), - std::span( - refinement_reasons_for_new_cells.data(), - refinement_reasons_for_new_cells.size())); - return true; -} - -template -bool refine_red_on_graph_failed_cell(AdaptCell& adapt_cell, - int level_set_id, - int cell_id) -{ - if (cell_id < 0 || cell_id >= adapt_cell.n_entities(adapt_cell.tdim)) - return false; - - adapt_cell.set_cell_cert_tag( - level_set_id, cell_id, CellCertTag::ambiguous); - return refine_red_on_ambiguous_cells(adapt_cell, level_set_id); -} - // ===================================================================== // certify_and_refine // ===================================================================== @@ -5389,160 +1989,27 @@ void certify_and_refine(AdaptCell& adapt_cell, } template -ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( - AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int max_iterations, - T zero_tol, T sign_tol, - int edge_max_depth, - bool triangulate_cut_parts, - const ReadyCellGraphOptions& graph_options) +void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, + const LevelSetCell& ls_cell, + int level_set_id, + int max_iterations, + T zero_tol, T sign_tol, + int edge_max_depth, + bool triangulate_cut_parts) { fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); - - ReadyCellGraphDiagnostics diagnostics; - for (int graph_iter = 0; graph_iter <= graph_options.max_refinements; ++graph_iter) - { - certify_and_refine(adapt_cell, ls_cell, level_set_id, - max_iterations, zero_tol, sign_tol, edge_max_depth); - fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); - - diagnostics = check_processed_ready_to_cut_cell_graphs( - adapt_cell, ls_cell, level_set_id, - zero_tol, sign_tol, edge_max_depth, - triangulate_cut_parts, graph_options); - if (!diagnostics.accepted - && diagnostics.first_requested_refinement_entity_dim != 1) - { - diagnostics = check_ready_to_cut_cell_graphs( - adapt_cell, ls_cell, level_set_id, graph_options); - } - diagnostics.graph_refinements = graph_iter; - - if (diagnostics.accepted) - break; - - if (graph_iter >= graph_options.max_refinements) - { - break; - } - - bool refined = false; - if (graph_options.refinement_mode - == GraphRefinementMode::red_failed_cell) - { - refined = refine_red_on_graph_failed_cell( - adapt_cell, level_set_id, diagnostics.first_failed_cell); - } - else if (graph_options.refinement_mode - == GraphRefinementMode::green_orthogonal_surface_edge - && diagnostics.first_requested_refinement_entity_dim == 1) - { - if (!diagnostics.first_requested_refinement_point.empty()) - { - refined = refine_green_on_requested_graph_point( - adapt_cell, - ls_cell, - level_set_id, - diagnostics.first_failed_cell, - std::span( - diagnostics.first_requested_refinement_point.data(), - diagnostics.first_requested_refinement_point.size()), - zero_tol); - } - if (!refined) - { - refined = refine_green_on_requested_graph_edge_midpoint( - adapt_cell, - level_set_id, - diagnostics.first_requested_refinement_entity_id); - } - } - else if ((graph_options.refinement_mode - == GraphRefinementMode::green_midpoint_residual - || graph_options.refinement_mode - == GraphRefinementMode::green_normal_variation) - && !diagnostics.first_requested_refinement_point.empty()) - { - refined = refine_green_on_requested_graph_point( - adapt_cell, - ls_cell, - level_set_id, - diagnostics.first_failed_cell, - std::span( - diagnostics.first_requested_refinement_point.data(), - diagnostics.first_requested_refinement_point.size()), - zero_tol); - } - else if (diagnostics.first_requested_refinement_entity_dim == 1) - { - refined = refine_green_on_requested_graph_edge( - adapt_cell, - ls_cell, - level_set_id, - diagnostics.first_requested_refinement_entity_id, - zero_tol, - sign_tol, - edge_max_depth); - } - - if (!refined && diagnostics.first_failed_cell >= 0) - { - refined = refine_red_on_graph_failed_cell( - adapt_cell, level_set_id, diagnostics.first_failed_cell); - } - - if (!refined) - { - refined = refine_ready_cell_on_largest_midpoint_value( - adapt_cell, ls_cell, level_set_id, diagnostics.first_failed_cell); - } - - if (!refined) - { - break; - } - - fill_all_vertex_signs_from_level_set( - adapt_cell, ls_cell, level_set_id, zero_tol); - } - + certify_and_refine(adapt_cell, ls_cell, level_set_id, + max_iterations, zero_tol, sign_tol, edge_max_depth); + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); process_ready_to_cut_cells(adapt_cell, ls_cell, level_set_id, zero_tol, sign_tol, edge_max_depth, triangulate_cut_parts); - fill_all_vertex_signs_from_level_set( - adapt_cell, ls_cell, level_set_id, zero_tol); + fill_all_vertex_signs_from_level_set(adapt_cell, ls_cell, level_set_id, zero_tol); build_edges(adapt_cell); if (adapt_cell.tdim == 3) build_faces(adapt_cell); recompute_active_level_set_masks(adapt_cell, level_set_id + 1); rebuild_zero_entity_inventory(adapt_cell); - populate_committed_zero_entity_graph_diagnostics( - adapt_cell, ls_cell, level_set_id, graph_options, diagnostics); - return diagnostics; -} - -template -void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int max_iterations, - T zero_tol, T sign_tol, - int edge_max_depth, - bool triangulate_cut_parts) -{ - const ReadyCellGraphOptions graph_options; - (void)certify_refine_graph_check_and_process_ready_cells( - adapt_cell, - ls_cell, - level_set_id, - max_iterations, - zero_tol, - sign_tol, - edge_max_depth, - triangulate_cut_parts, - graph_options); } // ===================================================================== @@ -5626,52 +2093,6 @@ template void process_ready_to_cut_cells(AdaptCell&, const LevelSetCell&, int, float, float, int, bool); -template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&); - -template void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&, - ReadyCellGraphDiagnostics&); -template void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&, - ReadyCellGraphDiagnostics&); -template void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&, - ReadyCellGraphDiagnostics&); -template void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell&, - const LevelSetCell&, - int, - const ReadyCellGraphOptions&, - ReadyCellGraphDiagnostics&); - template bool refine_ready_cell_on_largest_midpoint_value( AdaptCell&, const LevelSetCell&, @@ -5716,49 +2137,4 @@ template void certify_refine_and_process_ready_cells(AdaptCell&, const LevelSetCell&, int, int, float, float, int, bool); -template ReadyCellGraphDiagnostics -certify_refine_graph_check_and_process_ready_cells( - AdaptCell&, - const LevelSetCell&, - int, - int, - double, - double, - int, - bool, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics -certify_refine_graph_check_and_process_ready_cells( - AdaptCell&, - const LevelSetCell&, - int, - int, - float, - float, - int, - bool, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics -certify_refine_graph_check_and_process_ready_cells( - AdaptCell&, - const LevelSetCell&, - int, - int, - double, - double, - int, - bool, - const ReadyCellGraphOptions&); -template ReadyCellGraphDiagnostics -certify_refine_graph_check_and_process_ready_cells( - AdaptCell&, - const LevelSetCell&, - int, - int, - float, - float, - int, - bool, - const ReadyCellGraphOptions&); - } // namespace cutcells diff --git a/cpp/src/cell_certification.h b/cpp/src/cell_certification.h index cde3a82..c2f3799 100644 --- a/cpp/src/cell_certification.h +++ b/cpp/src/cell_certification.h @@ -7,171 +7,16 @@ #include #include -#include #include #include #include "adapt_cell.h" #include "cell_types.h" -#include "graph_criteria.h" #include "level_set_cell.h" namespace cutcells { -enum class GraphProjectionMode : std::uint8_t -{ - straight_zero_entity_normal = 0, - level_set_gradient = 1 -}; - -enum class GraphRefinementMode : std::uint8_t -{ - green_edge = 0, - red_failed_cell = 1, - green_orthogonal_surface_edge = 2, - green_midpoint_residual = 3, - green_normal_variation = 4 -}; - -template -struct ReadyCellGraphOptions -{ - bool enabled = true; - int geometry_order = 2; - int max_refinements = 5; - GraphProjectionMode projection_mode = - GraphProjectionMode::level_set_gradient; - GraphRefinementMode refinement_mode = - GraphRefinementMode::green_midpoint_residual; - graph_criteria::Options criteria = {}; - T min_level_set_gradient_host_alignment = T(0.9); -}; - -enum class GraphNodeKind : std::uint8_t -{ - edge_endpoint = 0, - edge_interior = 1, - face_boundary_edge = 2, - face_interior = 3 -}; - -template -struct GraphNodeDiagnostics -{ - int node_index = -1; - GraphNodeKind node_kind = GraphNodeKind::edge_interior; - bool accepted = true; - bool fallback_used = false; - graph_criteria::DirectionKind selected_direction_kind = - graph_criteria::DirectionKind::projected_level_set_gradient; - graph_criteria::FailureReason failure_reason = - graph_criteria::FailureReason::none; - int parent_entity_dim = -1; - int parent_entity_id = -1; - int requested_refinement_entity_dim = -1; - int requested_refinement_entity_id = -1; - - T level_set_gradient_host_alignment = std::numeric_limits::quiet_NaN(); - T level_set_gradient_angle_to_tangent_deg = - std::numeric_limits::quiet_NaN(); - T selected_host_alignment = std::numeric_limits::quiet_NaN(); - T drift_amplification = std::numeric_limits::quiet_NaN(); - T relative_correction_distance = std::numeric_limits::quiet_NaN(); - T relative_tangential_shift = std::numeric_limits::quiet_NaN(); - T true_transversality = std::numeric_limits::quiet_NaN(); - - std::vector seed; - std::vector corrected; - std::vector selected_direction; - std::vector level_set_gradient_direction; - std::vector straight_helper_normal; -}; - -template -struct ZeroEntityGraphDiagnostics -{ - int local_zero_entity_id = -1; - int level_set_id = -1; - int dimension = -1; - std::uint64_t zero_mask = 0; - int host_cell_id = -1; - cell::type host_cell_type = cell::type::point; - int host_face_id = -1; - int source_level_set = -1; - bool accepted = true; - int checked_edges = 0; - int checked_faces = 0; - int failed_checks = 0; - graph_criteria::FailureReason failure_reason = - graph_criteria::FailureReason::none; - - T min_true_transversality = std::numeric_limits::infinity(); - T min_host_normal_alignment = std::numeric_limits::infinity(); - T max_drift_amplification = T(0); - T max_relative_correction_distance = T(0); - T max_relative_tangential_shift = T(0); - T min_edge_gap_ratio = std::numeric_limits::infinity(); - T min_face_area_ratio = std::numeric_limits::infinity(); - T min_level_set_gradient_host_alignment = - std::numeric_limits::infinity(); - int failed_face_triangle_index = -1; - T failed_face_area_ratio = std::numeric_limits::quiet_NaN(); - - std::vector failed_projection_seed; - std::vector failed_projection_direction; - T failed_projection_clip_lo = std::numeric_limits::quiet_NaN(); - T failed_projection_clip_hi = std::numeric_limits::quiet_NaN(); - T failed_projection_root_t = std::numeric_limits::quiet_NaN(); - - int requested_refinement_entity_dim = -1; - int requested_refinement_entity_id = -1; - std::vector requested_refinement_point; - std::vector> nodes; -}; - -template -struct ReadyCellGraphDiagnostics -{ - bool accepted = true; - int checked_cells = 0; - int checked_edges = 0; - int checked_faces = 0; - int failed_checks = 0; - int graph_refinements = 0; - int first_failed_cell = -1; - graph_criteria::FailureReason first_failure_reason = - graph_criteria::FailureReason::none; - - T min_true_transversality = std::numeric_limits::infinity(); - T min_host_normal_alignment = std::numeric_limits::infinity(); - T max_drift_amplification = T(0); - T max_relative_correction_distance = T(0); - T max_relative_tangential_shift = T(0); - T min_edge_gap_ratio = std::numeric_limits::infinity(); - T min_face_area_ratio = std::numeric_limits::infinity(); - T min_level_set_gradient_host_alignment = - std::numeric_limits::infinity(); - int first_failed_face_triangle_index = -1; - T first_failed_face_area_ratio = std::numeric_limits::quiet_NaN(); - - std::vector first_failed_projection_seed; - std::vector first_failed_projection_direction; - T first_failed_projection_clip_lo = std::numeric_limits::quiet_NaN(); - T first_failed_projection_clip_hi = std::numeric_limits::quiet_NaN(); - T first_failed_projection_root_t = std::numeric_limits::quiet_NaN(); - int first_requested_refinement_entity_dim = -1; - int first_requested_refinement_entity_id = -1; - std::vector first_requested_refinement_point; - - /// Diagnostics attached to the committed AdaptCell zero entities. These - /// are populated after the linear cut is committed and before any lazy - /// high-order curving can run, so visualization can display the graph - /// criterion on the zero edge/face where it is evaluated. - std::vector> zero_entities; - std::vector> nodes; -}; - // ===================================================================== // Exact subcell Bernstein restriction // ===================================================================== @@ -308,28 +153,6 @@ void process_ready_to_cut_cells(AdaptCell& adapt_cell, int edge_max_depth, bool triangulate_cut_parts = false); -/// Run graph diagnostics on all current ready_to_cut cells without mutating -/// AdaptCell topology, masks, or provenance. The diagnostic cut is a temporary -/// P1/linear interface built from the current uncut ready leaf cells. -template -ReadyCellGraphDiagnostics check_ready_to_cut_cell_graphs( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - const ReadyCellGraphOptions& graph_options = {}); - -/// Populate per-committed-zero-entity graph diagnostics on an existing -/// diagnostic object. This is used after final zero-entity inventory rebuilds -/// so visualization data is keyed to the AdaptCell zero entities that users -/// inspect. -template -void populate_committed_zero_entity_graph_diagnostics( - const AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - const ReadyCellGraphOptions& graph_options, - ReadyCellGraphDiagnostics& diagnostics); - /// Green-refine a ready leaf cell through the midpoint of the cell edge whose /// midpoint has the largest level-set value. template @@ -358,7 +181,7 @@ bool refine_ready_cell_on_largest_midpoint_value( /// @param max_iterations Maximum number of refine-reclassify iterations. /// @param zero_tol Tolerance for all-zero. /// @param sign_tol Tolerance for all-positive / all-negative. -/// @param edge_max_depth Maximum subdivision depth for edge root search. +/// @param edge_max_depth Maximum subdivision depth for edge topology classification. template void certify_and_refine(AdaptCell& adapt_cell, const LevelSetCell& ls_cell, @@ -381,22 +204,4 @@ void certify_refine_and_process_ready_cells(AdaptCell& adapt_cell, int edge_max_depth, bool triangulate_cut_parts = false); -/// Full local pipeline with graph preflight: -/// 1. stamp vertex signs -/// 2. certify + green/red refine until stable -/// 3. build temporary P1 cuts for graph checks on ready cells -/// 4. on graph failure, discard the temporary cut and green-refine the -/// original uncut ready cell, then recurse -/// 5. replace ready_to_cut cells by the committed cut decomposition -template -ReadyCellGraphDiagnostics certify_refine_graph_check_and_process_ready_cells( - AdaptCell& adapt_cell, - const LevelSetCell& ls_cell, - int level_set_id, - int max_iterations, - T zero_tol, T sign_tol, - int edge_max_depth, - bool triangulate_cut_parts = false, - const ReadyCellGraphOptions& graph_options = {}); - } // namespace cutcells diff --git a/cpp/src/curving.cpp b/cpp/src/curving.cpp deleted file mode 100644 index 1eb52a7..0000000 --- a/cpp/src/curving.cpp +++ /dev/null @@ -1,4952 +0,0 @@ -// Copyright (c) 2026 ONERA -// Authors: Susanne Claus -// This file is part of CutCells -// SPDX-License-Identifier: MIT - -#include "curving.h" - -#include "cell_topology.h" -#include "edge_root.h" -#include "geometric_quantity.h" -#include "mapping.h" -#include "reference_cell.h" - -#include -#include -#include -#include -#include -#include - -namespace cutcells::curving -{ -namespace -{ - -template -bool solve_dense_small(std::vector A, std::vector b, int n, std::vector& x); - -template -T local_dot(std::span a, std::span b) -{ - T value = T(0); - for (std::size_t i = 0; i < a.size(); ++i) - value += a[i] * b[i]; - return value; -} - -template -T local_norm(std::span a) -{ - return std::sqrt(local_dot(a, a)); -} - -template -std::array local_cross(std::span a, std::span b) -{ - return {a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0]}; -} - -template -std::span vertex_ref_point(const AdaptCell& ac, int vertex_id) -{ - return std::span( - ac.vertex_coords.data() - + static_cast(vertex_id) - * static_cast(ac.tdim), - static_cast(ac.tdim)); -} - -template -bool vertex_list_contains(std::span vertices, int vertex) -{ - for (const auto v : vertices) - { - if (static_cast(v) == vertex) - return true; - } - return false; -} - -template -bool cell_contains_vertices(std::span cell_vertices, - std::span query_vertices) -{ - for (const int v : query_vertices) - { - if (!vertex_list_contains(cell_vertices, v)) - return false; - } - return true; -} - -template -std::vector local_zero_entity_vertex_ids(const AdaptCell& ac, - int local_zero_entity_id) -{ - if (local_zero_entity_id < 0 - || local_zero_entity_id >= ac.n_zero_entities()) - { - return {}; - } - - const int zdim = - ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = - ac.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim == 0) - return {zid}; - - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - std::vector out; - out.reserve(verts.size()); - for (const auto v : verts) - out.push_back(static_cast(v)); - return out; -} - -template -std::vector zero_entity_host_cell_vertex_ids(const AdaptCell& ac, - int local_zero_entity_id) -{ - if (local_zero_entity_id < 0 - || local_zero_entity_id >= ac.zero_entity_host_cell_vertices.size()) - { - return {}; - } - auto verts = ac.zero_entity_host_cell_vertices[ - static_cast(local_zero_entity_id)]; - std::vector out; - out.reserve(verts.size()); - for (const auto v : verts) - out.push_back(static_cast(v)); - return out; -} - -template -bool zero_entity_has_explicit_host_cell(const AdaptCell& ac, - int local_zero_entity_id) -{ - return !zero_entity_host_cell_vertex_ids(ac, local_zero_entity_id).empty(); -} - -template -void append_unique_host_vertex(const AdaptCell& ac, - int vertex_id, - std::vector& vertex_ids, - std::vector& coords) -{ - if (vertex_list_contains( - std::span(vertex_ids.data(), vertex_ids.size()), - vertex_id)) - { - return; - } - vertex_ids.push_back(vertex_id); - const auto p = vertex_ref_point(ac, vertex_id); - coords.insert(coords.end(), p.begin(), p.end()); -} - -template -struct LocalHostDomain -{ - int dimension = -1; - bool ordered_boundary = false; - std::vector vertices; - - bool valid(int tdim) const - { - if (dimension <= 0 || dimension > tdim) - return false; - const int nverts = - static_cast(vertices.size() / static_cast(tdim)); - return nverts >= dimension + 1; - } -}; - -template -bool face_is_zero_for_mask(const AdaptCell& ac, - std::span face_vertices, - std::uint64_t zero_mask) -{ - if (zero_mask == 0) - return false; - for (const int v : face_vertices) - { - if ((ac.zero_mask_per_vertex[static_cast(v)] & zero_mask) - != zero_mask) - { - return false; - } - } - return true; -} - -template -LocalHostDomain local_host_domain_for_zero_entity( - const AdaptCell& ac, - int local_zero_entity_id) -{ - LocalHostDomain host; - if ((ac.tdim != 2 && ac.tdim != 3) || local_zero_entity_id < 0 - || local_zero_entity_id >= ac.n_zero_entities()) - { - return host; - } - - const int zdim = - ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const auto zverts = - local_zero_entity_vertex_ids(ac, local_zero_entity_id); - if (zverts.empty()) - return host; - - const auto explicit_host_vertices = - zero_entity_host_cell_vertex_ids(ac, local_zero_entity_id); - - if (ac.tdim == 2 && zdim == 1 && !explicit_host_vertices.empty()) - { - host.dimension = 2; - host.ordered_boundary = true; - for (const int vertex_id : explicit_host_vertices) - { - const auto p = vertex_ref_point(ac, vertex_id); - host.vertices.insert(host.vertices.end(), p.begin(), p.end()); - } - return host; - } - - if (zdim == 1 - && local_zero_entity_id - < static_cast(ac.zero_entity_host_face_id.size()) - && local_zero_entity_id - < static_cast(ac.zero_entity_host_cell_type.size()) - && !explicit_host_vertices.empty()) - { - const int host_face_id = - ac.zero_entity_host_face_id[static_cast(local_zero_entity_id)]; - const cell::type host_type = - ac.zero_entity_host_cell_type[static_cast(local_zero_entity_id)]; - if (host_face_id >= 0 - && host_face_id < cell::num_faces(host_type)) - { - auto face_vertices = cell::face_vertices(host_type, host_face_id); - host.dimension = 2; - host.ordered_boundary = true; - for (const auto local_v : face_vertices) - { - if (local_v < 0 - || local_v >= static_cast(explicit_host_vertices.size())) - { - host.vertices.clear(); - host.dimension = -1; - return host; - } - const int vertex_id = - explicit_host_vertices[static_cast(local_v)]; - const auto p = vertex_ref_point(ac, vertex_id); - host.vertices.insert(host.vertices.end(), p.begin(), p.end()); - } - return host; - } - } - - if (zdim == 1 && !explicit_host_vertices.empty()) - { - std::vector ids; - for (const int v : explicit_host_vertices) - append_unique_host_vertex(ac, v, ids, host.vertices); - if (static_cast(ids.size()) >= 4) - host.dimension = 3; - return host; - } - - if (zdim == 2) - { - if (!explicit_host_vertices.empty()) - { - std::vector ids; - for (const int v : explicit_host_vertices) - append_unique_host_vertex(ac, v, ids, host.vertices); - if (static_cast(ids.size()) >= 4) - host.dimension = 3; - return host; - } - - const int n_cells = ac.n_entities(ac.tdim); - std::vector ids; - for (int c = 0; c < n_cells; ++c) - { - const auto cell_vertices = - ac.entity_to_vertex[ac.tdim][static_cast(c)]; - bool touches_zero_face = false; - for (const int v : zverts) - touches_zero_face = touches_zero_face - || vertex_list_contains( - cell_vertices, v); - if (!touches_zero_face) - { - continue; - } - - for (const auto v : cell_vertices) - append_unique_host_vertex(ac, static_cast(v), ids, host.vertices); - } - - if (static_cast(ids.size()) >= 4) - host.dimension = 3; - } - - return host; -} - -template -bool clip_interval_by_halfspace(std::span x0, - std::span direction, - std::span normal, - T offset, - T tol, - T& lo, - T& hi) -{ - const T a = local_dot(direction, normal); - const T b = local_dot(x0, normal) + offset; - if (std::fabs(a) <= tol) - return b >= -tol; - - const T t = (-tol - b) / a; - if (a > T(0)) - lo = std::max(lo, t); - else - hi = std::min(hi, t); - return lo <= hi; -} - -template -bool clip_line_interval_in_ordered_face(const LocalHostDomain& host, - int tdim, - std::span x0, - std::span direction, - T tol, - T& lo, - T& hi) -{ - if (tdim != 3 || !host.ordered_boundary) - return false; - const int nverts = - static_cast(host.vertices.size() / static_cast(tdim)); - if (nverts < 3) - return false; - - const auto p0 = std::span(host.vertices.data(), 3); - const auto p1 = std::span(host.vertices.data() + 3, 3); - const auto p2 = std::span(host.vertices.data() + 6, 3); - std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - auto normal = local_cross( - std::span(e0.data(), 3), - std::span(e1.data(), 3)); - const T normal_norm = - local_norm(std::span(normal.data(), 3)); - const T direction_norm = local_norm(direction); - if (normal_norm <= tol || direction_norm <= tol) - return false; - const T plane_seed = (x0[0] - p0[0]) * normal[0] - + (x0[1] - p0[1]) * normal[1] - + (x0[2] - p0[2]) * normal[2]; - const T plane_dir = local_dot( - direction, std::span(normal.data(), 3)); - if (std::fabs(plane_seed) > tol * normal_norm - || std::fabs(plane_dir) > tol * normal_norm * direction_norm) - { - return false; - } - - std::array centroid = {T(0), T(0), T(0)}; - for (int i = 0; i < nverts; ++i) - { - const auto p = std::span( - host.vertices.data() + static_cast(3 * i), 3); - for (int d = 0; d < 3; ++d) - centroid[static_cast(d)] += p[static_cast(d)]; - } - for (T& x : centroid) - x /= static_cast(nverts); - - for (int i = 0; i < nverts; ++i) - { - const auto a = std::span( - host.vertices.data() + static_cast(3 * i), 3); - const auto b = std::span( - host.vertices.data() - + static_cast(3 * ((i + 1) % nverts)), 3); - std::array edge = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; - auto inward = local_cross( - std::span(normal.data(), 3), - std::span(edge.data(), 3)); - std::array ca = { - centroid[0] - a[0], centroid[1] - a[1], centroid[2] - a[2]}; - if (local_dot( - std::span(inward.data(), 3), - std::span(ca.data(), 3)) < T(0)) - { - for (T& v : inward) - v = -v; - } - const T offset = -local_dot( - std::span(inward.data(), 3), a); - if (!clip_interval_by_halfspace( - x0, - direction, - std::span(inward.data(), 3), - offset, - tol, - lo, - hi)) - { - return false; - } - } - return lo <= hi; -} - -template -bool clip_line_interval_in_ordered_polygon_2d(const LocalHostDomain& host, - int tdim, - std::span x0, - std::span direction, - T tol, - T& lo, - T& hi) -{ - if (tdim != 2 || !host.ordered_boundary) - return false; - const int nverts = - static_cast(host.vertices.size() / static_cast(tdim)); - if (nverts < 3) - return false; - - std::array centroid = {T(0), T(0)}; - for (int i = 0; i < nverts; ++i) - { - centroid[0] += host.vertices[static_cast(2 * i)]; - centroid[1] += host.vertices[static_cast(2 * i + 1)]; - } - centroid[0] /= static_cast(nverts); - centroid[1] /= static_cast(nverts); - - for (int i = 0; i < nverts; ++i) - { - const auto a = std::span( - host.vertices.data() + static_cast(2 * i), 2); - const auto b = std::span( - host.vertices.data() - + static_cast(2 * ((i + 1) % nverts)), 2); - std::array edge = {b[0] - a[0], b[1] - a[1]}; - std::array inward = {-edge[1], edge[0]}; - std::array ca = {centroid[0] - a[0], centroid[1] - a[1]}; - if (local_dot( - std::span(inward.data(), 2), - std::span(ca.data(), 2)) < T(0)) - { - for (T& v : inward) - v = -v; - } - const T offset = -local_dot( - std::span(inward.data(), 2), a); - if (!clip_interval_by_halfspace( - x0, - direction, - std::span(inward.data(), 2), - offset, - tol, - lo, - hi)) - { - return false; - } - } - return lo <= hi; -} - -template -bool clip_line_interval_in_convex_hull(const LocalHostDomain& host, - int tdim, - std::span x0, - std::span direction, - T tol, - T& lo, - T& hi) -{ - const int nverts = - static_cast(host.vertices.size() / static_cast(tdim)); - if (host.dimension == 2) - { - if (tdim == 3) - return clip_line_interval_in_ordered_face( - host, tdim, x0, direction, tol, lo, hi); - if (tdim == 2) - return clip_line_interval_in_ordered_polygon_2d( - host, tdim, x0, direction, tol, lo, hi); - return false; - } - - if (host.dimension != 3 || tdim != 3 || nverts < 4) - return false; - - int accepted_planes = 0; - for (int i = 0; i < nverts; ++i) - { - const auto pi = std::span( - host.vertices.data() + static_cast(3 * i), 3); - for (int j = i + 1; j < nverts; ++j) - { - const auto pj = std::span( - host.vertices.data() + static_cast(3 * j), 3); - for (int k = j + 1; k < nverts; ++k) - { - const auto pk = std::span( - host.vertices.data() + static_cast(3 * k), 3); - std::array e0 = { - pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]}; - std::array e1 = { - pk[0] - pi[0], pk[1] - pi[1], pk[2] - pi[2]}; - auto normal = local_cross( - std::span(e0.data(), 3), - std::span(e1.data(), 3)); - const T nn = local_norm( - std::span(normal.data(), 3)); - if (nn <= tol) - continue; - - bool has_pos = false; - bool has_neg = false; - for (int m = 0; m < nverts; ++m) - { - const auto pm = std::span( - host.vertices.data() + static_cast(3 * m), 3); - const T s = (pm[0] - pi[0]) * normal[0] - + (pm[1] - pi[1]) * normal[1] - + (pm[2] - pi[2]) * normal[2]; - has_pos = has_pos || s > tol * nn; - has_neg = has_neg || s < -tol * nn; - if (has_pos && has_neg) - break; - } - if (has_pos && has_neg) - continue; - if (!has_pos && !has_neg) - continue; - if (has_neg) - { - for (T& v : normal) - v = -v; - } - const T offset = -local_dot( - std::span(normal.data(), 3), pi); - if (!clip_interval_by_halfspace( - x0, - direction, - std::span(normal.data(), 3), - offset, - tol, - lo, - hi)) - { - return false; - } - ++accepted_planes; - } - } - } - - return accepted_planes > 0 && lo <= hi; -} - -template -std::vector project_to_local_host_space(const AdaptCell& ac, - int local_zero_entity_id, - std::span direction, - T tol) -{ - std::vector out(direction.begin(), direction.end()); - const auto host = local_host_domain_for_zero_entity( - ac, local_zero_entity_id); - if (!host.valid(ac.tdim) || host.dimension == ac.tdim) - return out; - - if (host.dimension == 2 && ac.tdim == 3) - { - const int nverts = static_cast( - host.vertices.size() / static_cast(ac.tdim)); - if (nverts < 3) - return out; - const auto p0 = std::span(host.vertices.data(), 3); - const auto p1 = std::span(host.vertices.data() + 3, 3); - const auto p2 = std::span(host.vertices.data() + 6, 3); - std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - auto normal = local_cross( - std::span(e0.data(), 3), - std::span(e1.data(), 3)); - const T nn = local_dot( - std::span(normal.data(), 3), - std::span(normal.data(), 3)); - if (nn <= tol * tol) - return out; - const T c = local_dot( - std::span(out.data(), out.size()), - std::span(normal.data(), 3)) / nn; - for (int d = 0; d < 3; ++d) - out[static_cast(d)] -= c * normal[static_cast(d)]; - } - - return out; -} - -template -std::vector local_host_face_normal_reference(const AdaptCell& ac, - int local_zero_entity_id, - T tol) -{ - const auto host = local_host_domain_for_zero_entity( - ac, local_zero_entity_id); - if (ac.tdim != 3 || !host.valid(ac.tdim) || host.dimension != 2) - return {}; - - const int nverts = static_cast( - host.vertices.size() / static_cast(ac.tdim)); - if (nverts < 3) - return {}; - - const auto p0 = std::span(host.vertices.data(), 3); - const auto p1 = std::span(host.vertices.data() + 3, 3); - const auto p2 = std::span(host.vertices.data() + 6, 3); - std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - auto normal = local_cross( - std::span(e0.data(), 3), - std::span(e1.data(), 3)); - const T nn = local_norm(std::span(normal.data(), 3)); - if (nn <= tol) - return {}; - std::vector out(3, T(0)); - for (int d = 0; d < 3; ++d) - out[static_cast(d)] = normal[static_cast(d)] / nn; - return out; -} - -template -struct BoundaryEdgeState -{ - std::array vertices = {-1, -1}; - const CurvedZeroEntityState* state = nullptr; - bool use_curved_state = true; -}; - -struct BoundaryEdgeRef -{ - int local_zero_entity_id = -1; - int host_face_id = -1; - bool use_curved_state = true; -}; - -template -struct ProjectionStats -{ - int iterations = 0; - CurvingStatus status = CurvingStatus::failed; - CurvingFailureCode failure_code = CurvingFailureCode::projection_failed; - T residual = std::numeric_limits::infinity(); - std::uint32_t active_face_mask = 0; - int closest_face_id = -1; - int safe_subspace_dim = -1; - CurvingProjectionMode projection_mode = CurvingProjectionMode::none; - int retry_count = 0; - std::vector seed; - std::vector direction; - T clip_lo = std::numeric_limits::quiet_NaN(); - T clip_hi = std::numeric_limits::quiet_NaN(); - T root_t = std::numeric_limits::quiet_NaN(); -}; - -template -void append_node_stats(CurvedZeroEntityState& state, - const ProjectionStats& stats, - int tdim) -{ - state.node_iterations.push_back(static_cast(stats.iterations)); - state.node_status.push_back(static_cast(stats.status)); - state.node_failure_code.push_back(static_cast(stats.failure_code)); - state.node_residual.push_back(stats.residual); - state.node_active_face_mask.push_back(stats.active_face_mask); - state.node_closest_face_id.push_back(static_cast(stats.closest_face_id)); - state.node_safe_subspace_dim.push_back(static_cast(stats.safe_subspace_dim)); - state.node_projection_mode.push_back(static_cast(stats.projection_mode)); - state.node_retry_count.push_back(static_cast(stats.retry_count)); - - const T nan = std::numeric_limits::quiet_NaN(); - auto append_vector = [&](std::vector& dst, const std::vector& src) - { - for (int d = 0; d < tdim; ++d) - { - dst.push_back( - d < static_cast(src.size()) - ? src[static_cast(d)] - : nan); - } - }; - append_vector(state.node_seed, stats.seed); - append_vector(state.node_direction, stats.direction); - state.node_clip_lo.push_back(stats.clip_lo); - state.node_clip_hi.push_back(stats.clip_hi); - state.node_root_t.push_back(stats.root_t); -} - -template -ProjectionStats accepted_node_stats(CurvingFailureCode code, T residual = T(0)) -{ - ProjectionStats stats; - stats.iterations = 0; - stats.status = CurvingStatus::curved; - stats.failure_code = code; - stats.residual = residual; - return stats; -} - -inline bool same_unordered_vertices(std::span a, std::span b) -{ - if (a.size() != b.size()) - return false; - for (const int av : a) - { - bool found = false; - for (const int bv : b) - found = found || (av == bv); - if (!found) - return false; - } - return true; -} - -template -std::vector zero_entity_vertices(const AdaptCell& ac, int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - std::vector out; - - if (zdim == 0) - { - out.resize(static_cast(ac.tdim)); - for (int d = 0; d < ac.tdim; ++d) - out[static_cast(d)] = - ac.vertex_coords[static_cast(zid * ac.tdim + d)]; - return out; - } - - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - out.resize(static_cast(verts.size() * ac.tdim)); - for (std::size_t i = 0; i < verts.size(); ++i) - { - const int v = static_cast(verts[i]); - for (int d = 0; d < ac.tdim; ++d) - out[i * static_cast(ac.tdim) + static_cast(d)] = - ac.vertex_coords[static_cast(v * ac.tdim + d)]; - } - return out; -} - -template -std::vector zero_entity_vertex_ids(const AdaptCell& ac, int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim == 0) - return {zid}; - - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - std::vector out; - out.reserve(verts.size()); - for (const auto v : verts) - out.push_back(static_cast(v)); - return out; -} - -template -T distance_between_vertices(const AdaptCell& ac, int v0, int v1) -{ - T length2 = T(0); - for (int d = 0; d < ac.tdim; ++d) - { - const T delta = - ac.vertex_coords[static_cast(v1 * ac.tdim + d)] - - ac.vertex_coords[static_cast(v0 * ac.tdim + d)]; - length2 += delta * delta; - } - return std::sqrt(length2); -} - -template -T zero_entity_edge_length(const AdaptCell& ac, int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 1) - return std::numeric_limits::infinity(); - - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[1][static_cast(zid)]; - if (verts.size() != 2) - return std::numeric_limits::infinity(); - return distance_between_vertices( - ac, static_cast(verts[0]), static_cast(verts[1])); -} - -template -T zero_entity_face_max_edge_length(const AdaptCell& ac, int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 2) - return std::numeric_limits::infinity(); - - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[2][static_cast(zid)]; - if (verts.size() < 2) - return T(0); - - T max_length = T(0); - for (std::size_t i = 0; i < verts.size(); ++i) - { - const int v0 = static_cast(verts[i]); - const int v1 = static_cast(verts[(i + 1) % verts.size()]); - max_length = std::max( - max_length, - distance_between_vertices( - ac, - v0, - v1)); - } - return max_length; -} - -template -bool zero_entity_below_curving_threshold(const AdaptCell& ac, - int local_zero_entity_id, - const CurvingOptions& options) -{ - if (!(options.small_entity_tol > T(0))) - return false; - - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim == 1) - return zero_entity_edge_length(ac, local_zero_entity_id) - <= options.small_entity_tol; - if (zdim == 2) - return zero_entity_face_max_edge_length(ac, local_zero_entity_id) - <= options.small_entity_tol; - return false; -} - -template -void append_straight_seed_node_stats(CurvedZeroEntityState& state, - const AdaptCell& ac, - std::span seeds) -{ - const int n_nodes = static_cast( - seeds.size() / static_cast(std::max(ac.tdim, 1))); - for (int i = 0; i < n_nodes; ++i) - append_node_stats( - state, - accepted_node_stats( - CurvingFailureCode::small_entity_kept_straight), - ac.tdim); -} - -template -std::pair legendre_value_and_derivative(int order, T x) -{ - if (order == 0) - return {T(1), T(0)}; - - T pm2 = T(1); - T pm1 = x; - for (int n = 2; n <= order; ++n) - { - const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); - pm2 = pm1; - pm1 = p; - } - - const T denom = T(1) - x * x; - if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) - return {pm1, T(0)}; - const T derivative = T(order) * (pm2 - x * pm1) / denom; - return {pm1, derivative}; -} - -template -std::vector gll_parameters(int order) -{ - order = std::max(order, 1); - std::vector params(static_cast(order + 1), T(0)); - params.front() = T(0); - params.back() = T(1); - if (order == 1) - return params; - - const T pi = std::acos(T(-1)); - const T eps = T(128) * std::numeric_limits::epsilon(); - for (int i = 1; i < order; ++i) - { - T x = -std::cos(pi * T(i) / T(order)); - for (int iter = 0; iter < 32; ++iter) - { - const auto [p, dp] = legendre_value_and_derivative(order, x); - const T denom = T(1) - x * x; - if (std::abs(denom) <= eps) - break; - const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; - if (std::abs(d2p) <= eps) - break; - const T step = dp / d2p; - x -= step; - x = std::clamp(x, -T(1) + eps, T(1) - eps); - if (std::abs(step) <= eps) - break; - } - params[static_cast(i)] = T(0.5) * (x + T(1)); - } - return params; -} - -template -std::vector interpolation_parameters(int order, NodeFamily family) -{ - order = std::max(order, 1); - std::vector params(static_cast(order + 1), T(0)); - - if (family == NodeFamily::gll) - return gll_parameters(order); - - // The current reference Lagrange point generator is equispaced. Keep - // lagrange distinct at the API/cache level so a later Basix-backed node - // family can be added without changing cache semantics. - for (int i = 0; i <= order; ++i) - params[static_cast(i)] = T(i) / T(order); - return params; -} - -template -std::vector> triangle_interpolation_barycentric_nodes( - int order, - NodeFamily family); - -template -void append_edge_seed_nodes(const AdaptCell& ac, - int local_zero_entity_id, - const CurvingOptions& options, - std::vector& seeds) -{ - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[1][static_cast(zid)]; - if (verts.size() != 2) - throw std::runtime_error("curving: zero edge does not have two vertices"); - - const auto params = interpolation_parameters(options.geometry_order, options.node_family); - seeds.clear(); - seeds.reserve(params.size() * static_cast(ac.tdim)); - for (const T s : params) - { - for (int d = 0; d < ac.tdim; ++d) - { - const T x0 = ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; - const T x1 = ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)]; - seeds.push_back((T(1) - s) * x0 + s * x1); - } - } -} - -template -void append_face_seed_nodes(const AdaptCell& ac, - int local_zero_entity_id, - const CurvingOptions& options, - std::vector& seeds) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim != 2) - throw std::runtime_error("curving: expected a zero face"); - - auto verts = ac.entity_to_vertex[2][static_cast(zid)]; - const auto entity_type = ac.entity_types[2][static_cast(zid)]; - const int order = std::max(options.geometry_order, 1); - seeds.clear(); - - auto append_combo = [&](std::span weights) - { - for (int d = 0; d < ac.tdim; ++d) - { - T x = T(0); - for (std::size_t v = 0; v < weights.size(); ++v) - { - x += weights[v] * ac.vertex_coords[ - static_cast(verts[v] * ac.tdim + d)]; - } - seeds.push_back(x); - } - }; - - if (entity_type == cell::type::triangle) - { - const auto nodes = - triangle_interpolation_barycentric_nodes(order, options.node_family); - for (const auto& w : nodes) - append_combo(std::span(w.data(), 3)); - return; - } - - if (entity_type == cell::type::quadrilateral) - { - const auto params = interpolation_parameters(order, options.node_family); - std::array w = {}; - for (const T v : params) - { - for (const T u : params) - { - w = {(T(1) - u) * (T(1) - v), - u * (T(1) - v), - (T(1) - u) * v, - u * v}; - append_combo(std::span(w.data(), 4)); - } - } - return; - } - - throw std::runtime_error("curving: unsupported zero-face type"); -} - -template -T lagrange_basis_1d(int i, std::span params, T x) -{ - T value = T(1); - const T xi = params[static_cast(i)]; - for (int j = 0; j < static_cast(params.size()); ++j) - { - if (j == i) - continue; - value *= (x - params[static_cast(j)]) - / (xi - params[static_cast(j)]); - } - return value; -} - -template -T warp_factor(int order, T r) -{ - if (order <= 1) - return T(0); - - std::vector equispaced(static_cast(order + 1), T(0)); - std::vector gll = gll_parameters(order); - for (int i = 0; i <= order; ++i) - { - equispaced[static_cast(i)] = -T(1) + T(2 * i) / T(order); - gll[static_cast(i)] = T(2) * gll[static_cast(i)] - T(1); - } - - T warp = T(0); - for (int i = 0; i <= order; ++i) - { - const T Li = lagrange_basis_1d( - i, std::span(equispaced.data(), equispaced.size()), r); - warp += Li * (gll[static_cast(i)] - - equispaced[static_cast(i)]); - } - - const T edge_factor = T(1) - r * r; - if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) - warp /= edge_factor; - return warp; -} - -template -std::array equilateral_to_reference_barycentric(T x, T y) -{ - const T sqrt3 = std::sqrt(T(3)); - std::array w = {}; - w[2] = (sqrt3 * y + T(1)) / T(3); - w[1] = (x + T(1) - w[2]) / T(2); - w[0] = T(1) - w[1] - w[2]; - - T sum = T(0); - for (T& wi : w) - { - if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) - wi = T(0); - if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) - wi = T(1); - wi = std::clamp(wi, T(0), T(1)); - sum += wi; - } - if (sum > T(0)) - for (T& wi : w) - wi /= sum; - return w; -} - -template -std::vector> triangle_interpolation_barycentric_nodes( - int order, - NodeFamily family) -{ - order = std::max(order, 1); - std::vector> nodes; - nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); - - if (family != NodeFamily::gll) - { - for (int j = 0; j <= order; ++j) - { - for (int i = 0; i <= order - j; ++i) - { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - nodes.push_back({T(1) - u - v, u, v}); - } - } - return nodes; - } - - constexpr std::array alpha_opt = { - T(0.0), T(0.0), T(1.4152), T(0.1001), - T(0.2751), T(0.9800), T(1.0999), T(1.2832), - T(1.3648), T(1.4773), T(1.4959), T(1.5743), - T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; - const T alpha = (order < static_cast(alpha_opt.size())) - ? alpha_opt[static_cast(order)] - : T(5) / T(3); - const T sqrt3 = std::sqrt(T(3)); - const T cos120 = -T(0.5); - const T sin120 = sqrt3 / T(2); - const T cos240 = -T(0.5); - const T sin240 = -sqrt3 / T(2); - - for (int j = 0; j <= order; ++j) - { - for (int i = 0; i <= order - j; ++i) - { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - const T lambda0 = T(1) - u - v; - const T lambda1 = u; - const T lambda2 = v; - - T x = -lambda0 + lambda1; - T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; - - const T L1 = lambda2; - const T L2 = lambda0; - const T L3 = lambda1; - const T warp1 = T(4) * L2 * L3 * warp_factor(order, L3 - L2) - * (T(1) + (alpha * L1) * (alpha * L1)); - const T warp2 = T(4) * L1 * L3 * warp_factor(order, L1 - L3) - * (T(1) + (alpha * L2) * (alpha * L2)); - const T warp3 = T(4) * L1 * L2 * warp_factor(order, L2 - L1) - * (T(1) + (alpha * L3) * (alpha * L3)); - - x += warp1 + cos120 * warp2 + cos240 * warp3; - y += sin120 * warp2 + sin240 * warp3; - nodes.push_back(equilateral_to_reference_barycentric(x, y)); - } - } - return nodes; -} - -template -const BoundaryEdgeState* find_boundary_edge_state( - std::span> boundary_edges, - int v0, - int v1) -{ - std::array query = {v0, v1}; - for (const auto& edge : boundary_edges) - { - if (same_unordered_vertices( - std::span(query.data(), query.size()), - std::span(edge.vertices.data(), edge.vertices.size()))) - { - return &edge; - } - } - return nullptr; -} - -template -std::vector eval_edge_state_at(const BoundaryEdgeState& edge, - const CurvingOptions& options, - int from_vertex, - int to_vertex, - T t_from_to) -{ - if (edge.state == nullptr || edge.state->status != CurvingStatus::curved) - throw std::runtime_error("curving: missing accepted curved boundary edge"); - - const int order = std::max(options.geometry_order, 1); - const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); - const bool same_orientation = - edge.vertices[0] == from_vertex && edge.vertices[1] == to_vertex; - const bool reverse_orientation = - edge.vertices[0] == to_vertex && edge.vertices[1] == from_vertex; - if (!same_orientation && !reverse_orientation) - throw std::runtime_error("curving: boundary edge orientation mismatch"); - - const T t = same_orientation ? t_from_to : T(1) - t_from_to; - const auto params = interpolation_parameters(order, options.node_family); - std::vector out(static_cast(tdim), T(0)); - for (int i = 0; i <= order; ++i) - { - const T Li = lagrange_basis_1d( - i, std::span(params.data(), params.size()), t); - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += Li * edge.state->ref_nodes[ - static_cast(i * tdim + d)]; - } - return out; -} - -template -void append_domain_inequality(std::vector>& ineq, - std::array a, - T b) -{ - ineq.push_back({a[0], a[1], a[2], b}); -} - -template -std::vector> reference_domain_inequalities(cell::type cell_type) -{ - std::vector> ineq; - switch (cell_type) - { - case cell::type::interval: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, { T(1), T(0), T(0)}, T(1)); - break; - case cell::type::triangle: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); - append_domain_inequality(ineq, {T(1), T(1), T(0)}, T(1)); - break; - case cell::type::tetrahedron: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); - append_domain_inequality(ineq, {T(1), T(1), T(1)}, T(1)); - break; - case cell::type::quadrilateral: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, { T(1), T(0), T(0)}, T(1)); - append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), T(1), T(0)}, T(1)); - break; - case cell::type::hexahedron: - for (int d = 0; d < 3; ++d) - { - std::array lo = {T(0), T(0), T(0)}; - std::array hi = {T(0), T(0), T(0)}; - lo[static_cast(d)] = -T(1); - hi[static_cast(d)] = T(1); - append_domain_inequality(ineq, lo, T(0)); - append_domain_inequality(ineq, hi, T(1)); - } - break; - case cell::type::prism: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); - append_domain_inequality(ineq, {T(1), T(1), T(0)}, T(1)); - append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); - append_domain_inequality(ineq, {T(0), T(0), T(1)}, T(1)); - break; - case cell::type::pyramid: - append_domain_inequality(ineq, {-T(1), T(0), T(0)}, T(0)); - append_domain_inequality(ineq, {T(0), -T(1), T(0)}, T(0)); - append_domain_inequality(ineq, {T(1), T(0), T(1)}, T(1)); - append_domain_inequality(ineq, {T(0), T(1), T(1)}, T(1)); - append_domain_inequality(ineq, {T(0), T(0), -T(1)}, T(0)); - append_domain_inequality(ineq, {T(0), T(0), T(1)}, T(1)); - break; - default: - break; - } - return ineq; -} - -template -geom::ParentEntity zero_entity_parent_entity(const AdaptCell& ac, - int local_zero_entity_id) -{ - if (local_zero_entity_id < 0 - || local_zero_entity_id >= ac.n_zero_entities()) - { - return {-1, -1}; - } - - const int parent_dim = - ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; - int parent_id = - ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; - if (parent_dim == ac.tdim) - parent_id = -1; - return {parent_dim, parent_id}; -} - -template -bool host_parameter_interval(const AdaptCell& ac, - int local_zero_entity_id, - std::span x0, - std::span d, - T tol, - T& lo, - T& hi) -{ - const auto local_host = - local_host_domain_for_zero_entity(ac, local_zero_entity_id); - const bool has_explicit_host = - zero_entity_has_explicit_host_cell(ac, local_zero_entity_id); - if (local_host.valid(ac.tdim)) - { - T local_lo = -std::numeric_limits::infinity(); - T local_hi = std::numeric_limits::infinity(); - if (clip_line_interval_in_convex_hull( - local_host, ac.tdim, x0, d, tol, local_lo, local_hi) - && local_lo <= local_hi) - { - lo = local_lo; - hi = local_hi; - return true; - } - if (has_explicit_host) - return false; - } - else if (has_explicit_host) - { - return false; - } - - const auto host = zero_entity_parent_entity(ac, local_zero_entity_id); - const auto interval = geom::clip_line_interval_in_parent_entity( - ac.parent_cell_type, - host, - x0, - d, - -std::numeric_limits::infinity(), - std::numeric_limits::infinity(), - tol); - lo = interval.t0; - hi = interval.t1; - return interval.valid && lo <= hi; -} - -template -T vec_dot(std::span a, std::span b) -{ - T out = T(0); - for (std::size_t i = 0; i < a.size(); ++i) - out += a[i] * b[i]; - return out; -} - -template -T vec_norm(std::span a) -{ - return std::sqrt(vec_dot(a, a)); -} - -template -std::vector normalized_delta_direction(std::span from, - std::span to, - T tol) -{ - std::vector direction(from.size(), T(0)); - for (std::size_t i = 0; i < from.size(); ++i) - direction[i] = to[i] - from[i]; - - const T norm = vec_norm( - std::span(direction.data(), direction.size())); - if (norm <= tol) - return {}; - - for (T& value : direction) - value /= norm; - return direction; -} - -template -T face_normal_norm(const std::array& row, int tdim) -{ - T n2 = T(0); - for (int d = 0; d < tdim; ++d) - n2 += row[static_cast(d)] * row[static_cast(d)]; - return std::sqrt(n2); -} - -template -T face_value(const std::array& row, std::span x, int tdim) -{ - T ax = T(0); - for (int d = 0; d < tdim; ++d) - ax += row[static_cast(d)] * x[static_cast(d)]; - return ax; -} - -template -T normalized_face_slack(const std::array& row, - std::span x, - int tdim) -{ - const T n = face_normal_norm(row, tdim); - if (n <= T(0)) - return std::numeric_limits::infinity(); - return (row[3] - face_value(row, x, tdim)) / n; -} - -template -bool vertex_satisfies_face(const std::array& row, - std::span ref_vertices, - int vertex, - int tdim) -{ - std::array x = {T(0), T(0), T(0)}; - for (int d = 0; d < tdim; ++d) - x[static_cast(d)] = - ref_vertices[static_cast(vertex * tdim + d)]; - const T n = face_normal_norm(row, tdim); - const T tol = T(64) * std::numeric_limits::epsilon() * std::max(T(1), n); - return std::fabs(face_value(row, std::span(x.data(), static_cast(tdim)), tdim) - row[3]) <= tol; -} - -template -int face_inequality_index_from_vertices(cell::type cell_type, - std::span vertices, - int tdim) -{ - const auto ineq = reference_domain_inequalities(cell_type); - const auto ref_vertices = cell::reference_vertices(cell_type); - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - bool all_on_face = true; - for (const int v : vertices) - { - all_on_face = all_on_face - && vertex_satisfies_face( - ineq[static_cast(i)], - std::span(ref_vertices.data(), ref_vertices.size()), - v, - tdim); - } - if (all_on_face) - return i; - } - return -1; -} - -template -int parent_face_inequality_index(cell::type cell_type, int parent_face_id, int tdim) -{ - if (parent_face_id < 0 || parent_face_id >= cell::num_faces(cell_type)) - return -1; - const auto fv = cell::face_vertices(cell_type, parent_face_id); - return face_inequality_index_from_vertices(cell_type, fv, tdim); -} - -template -std::uint32_t structural_active_face_mask(const AdaptCell& ac, - int local_zero_entity_id) -{ - if (ac.tdim != 3) - return 0; - - const int parent_dim = - ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; - const int parent_id = - ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; - - std::uint32_t mask = 0; - if (parent_dim == 2) - { - const int row = parent_face_inequality_index( - ac.parent_cell_type, parent_id, ac.tdim); - if (row >= 0) - mask |= std::uint32_t(1) << row; - return mask; - } - - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - const auto ref_vertices = cell::reference_vertices(ac.parent_cell_type); - if (parent_dim == 1) - { - const auto edges = cell::edges(ac.parent_cell_type); - if (parent_id < 0 || parent_id >= static_cast(edges.size())) - return mask; - const auto edge = edges[static_cast(parent_id)]; - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - if (vertex_satisfies_face( - ineq[static_cast(i)], - std::span(ref_vertices.data(), ref_vertices.size()), - edge[0], - ac.tdim) - && vertex_satisfies_face( - ineq[static_cast(i)], - std::span(ref_vertices.data(), ref_vertices.size()), - edge[1], - ac.tdim)) - { - mask |= std::uint32_t(1) << i; - } - } - } - else if (parent_dim == 0) - { - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - if (vertex_satisfies_face( - ineq[static_cast(i)], - std::span(ref_vertices.data(), ref_vertices.size()), - parent_id, - ac.tdim)) - { - mask |= std::uint32_t(1) << i; - } - } - } - return mask; -} - -template -std::uint32_t add_near_active_faces(const AdaptCell& ac, - std::span x, - std::uint32_t mask, - T tol) -{ - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - const T slack = normalized_face_slack( - ineq[static_cast(i)], x, ac.tdim); - if (slack <= tol) - mask |= std::uint32_t(1) << i; - } - return mask; -} - -template -int closest_inactive_face(const AdaptCell& ac, - std::span x, - std::uint32_t active_mask) -{ - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - int best = -1; - T best_slack = std::numeric_limits::infinity(); - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - if ((active_mask & (std::uint32_t(1) << i)) != 0) - continue; - const T slack = normalized_face_slack( - ineq[static_cast(i)], x, ac.tdim); - if (slack < best_slack) - { - best_slack = slack; - best = i; - } - } - return best; -} - -template -std::uint32_t add_closest_faces(const AdaptCell& ac, - std::span x, - std::uint32_t active_mask, - int& closest_face_id) -{ - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - closest_face_id = closest_inactive_face(ac, x, active_mask); - if (closest_face_id < 0) - return active_mask; - - const T best_slack = normalized_face_slack( - ineq[static_cast(closest_face_id)], x, ac.tdim); - const T tie_tol = std::max(T(128) * std::numeric_limits::epsilon(), - T(10) * ac.tdim * std::numeric_limits::epsilon()); - std::uint32_t out = active_mask; - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - if ((active_mask & (std::uint32_t(1) << i)) != 0) - continue; - const T slack = normalized_face_slack( - ineq[static_cast(i)], x, ac.tdim); - if (std::fabs(slack - best_slack) <= tie_tol * std::max(T(1), std::fabs(best_slack))) - out |= std::uint32_t(1) << i; - } - return out; -} - -template -std::vector> orthonormal_active_normals(const AdaptCell& ac, - std::uint32_t active_mask) -{ - const auto ineq = reference_domain_inequalities(ac.parent_cell_type); - std::vector> normals; - for (int i = 0; i < static_cast(ineq.size()); ++i) - { - if ((active_mask & (std::uint32_t(1) << i)) == 0) - continue; - std::vector q(static_cast(ac.tdim), T(0)); - for (int d = 0; d < ac.tdim; ++d) - q[static_cast(d)] = - ineq[static_cast(i)][static_cast(d)]; - for (const auto& prev : normals) - { - const T c = vec_dot( - std::span(q.data(), q.size()), - std::span(prev.data(), prev.size())); - for (int d = 0; d < ac.tdim; ++d) - q[static_cast(d)] -= c * prev[static_cast(d)]; - } - const T n = vec_norm(std::span(q.data(), q.size())); - if (n <= T(64) * std::numeric_limits::epsilon()) - continue; - for (T& x : q) - x /= n; - normals.push_back(q); - } - return normals; -} - -template -std::vector project_to_active_face_space(const AdaptCell& ac, - std::span direction, - std::uint32_t active_mask) -{ - std::vector out(direction.begin(), direction.end()); - const auto normals = orthonormal_active_normals(ac, active_mask); - for (const auto& n : normals) - { - const T c = vec_dot( - std::span(out.data(), out.size()), - std::span(n.data(), n.size())); - for (int d = 0; d < ac.tdim; ++d) - out[static_cast(d)] -= c * n[static_cast(d)]; - } - return out; -} - -template -std::vector> active_nullspace_basis(const AdaptCell& ac, - std::uint32_t active_mask) -{ - std::vector> basis; - const auto normals = orthonormal_active_normals(ac, active_mask); - for (int axis = 0; axis < ac.tdim; ++axis) - { - std::vector q(static_cast(ac.tdim), T(0)); - q[static_cast(axis)] = T(1); - for (const auto& n : normals) - { - const T c = vec_dot( - std::span(q.data(), q.size()), - std::span(n.data(), n.size())); - for (int d = 0; d < ac.tdim; ++d) - q[static_cast(d)] -= c * n[static_cast(d)]; - } - for (const auto& prev : basis) - { - const T c = vec_dot( - std::span(q.data(), q.size()), - std::span(prev.data(), prev.size())); - for (int d = 0; d < ac.tdim; ++d) - q[static_cast(d)] -= c * prev[static_cast(d)]; - } - const T n = vec_norm(std::span(q.data(), q.size())); - if (n <= T(64) * std::numeric_limits::epsilon()) - continue; - for (T& x : q) - x /= n; - basis.push_back(q); - } - return basis; -} - -template -int active_subspace_dim(const AdaptCell& ac, std::uint32_t active_mask) -{ - return static_cast(active_nullspace_basis(ac, active_mask).size()); -} - -template -void append_unique_direction(std::vector>& directions, - std::vector dir, - T tol) -{ - const T n = vec_norm(std::span(dir.data(), dir.size())); - if (n <= tol) - return; - for (T& x : dir) - x /= n; - - for (const auto& existing : directions) - { - const T c = std::fabs(vec_dot( - std::span(dir.data(), dir.size()), - std::span(existing.data(), existing.size()))); - if (c >= T(1) - T(64) * std::numeric_limits::epsilon()) - return; - } - directions.push_back(std::move(dir)); -} - -template -void append_admissible_unique_direction(std::vector>& directions, - const AdaptCell& ac, - int local_zero_entity_id, - std::span raw_direction, - T tol) -{ - const auto projected = geom::admissible_direction_in_parent_frame( - ac.parent_cell_type, - zero_entity_parent_entity(ac, local_zero_entity_id), - raw_direction, - tol); - if (projected.degenerate()) - return; - auto local_projected = project_to_local_host_space( - ac, - local_zero_entity_id, - std::span(projected.value.data(), projected.value.size()), - tol); - append_unique_direction(directions, std::move(local_projected), tol); -} - -template -std::vector zero_entity_edge_tangent(const AdaptCell& ac, - int local_zero_entity_id) -{ - std::vector tangent(static_cast(ac.tdim), T(0)); - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 1) - return tangent; - - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[1][static_cast(zid)]; - if (verts.size() != 2) - return tangent; - for (int d = 0; d < ac.tdim; ++d) - tangent[static_cast(d)] = - ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)] - - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; - return tangent; -} - -template -std::vector zero_entity_face_normal(const AdaptCell& ac, - int local_zero_entity_id) -{ - std::vector normal(static_cast(ac.tdim), T(0)); - if (ac.tdim != 3) - return normal; - - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 2) - return normal; - - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[2][static_cast(zid)]; - if (verts.size() < 3) - return normal; - - std::array e0 = {T(0), T(0), T(0)}; - std::array e1 = {T(0), T(0), T(0)}; - for (int d = 0; d < 3; ++d) - { - e0[static_cast(d)] = - ac.vertex_coords[static_cast(verts[1] * ac.tdim + d)] - - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; - e1[static_cast(d)] = - ac.vertex_coords[static_cast(verts[2] * ac.tdim + d)] - - ac.vertex_coords[static_cast(verts[0] * ac.tdim + d)]; - } - normal[0] = e0[1] * e1[2] - e0[2] * e1[1]; - normal[1] = e0[2] * e1[0] - e0[0] * e1[2]; - normal[2] = e0[0] * e1[1] - e0[1] * e1[0]; - return normal; -} - -template -bool contains_vertex(std::span vertices, std::int32_t query) -{ - for (const auto vertex : vertices) - if (vertex == query) - return true; - return false; -} - -template -std::vector adjacent_zero_face_normal_for_edge(const AdaptCell& ac, - int local_zero_entity_id, - T tol) -{ - std::vector best(static_cast(ac.tdim), T(0)); - if (ac.tdim != 3) - return best; - - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 1) - return best; - - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto edge_vertices = ac.entity_to_vertex[1][static_cast(zid)]; - if (edge_vertices.size() != 2) - return best; - - const std::uint64_t edge_mask = - ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; - T best_norm = T(0); - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (ac.zero_entity_dim[static_cast(z)] != 2) - continue; - - const std::uint64_t face_mask = - ac.zero_entity_zero_mask[static_cast(z)]; - if ((face_mask & edge_mask) != face_mask) - continue; - - const int face_id = ac.zero_entity_id[static_cast(z)]; - auto face_vertices = ac.entity_to_vertex[2][static_cast(face_id)]; - if (!contains_vertex(face_vertices, edge_vertices[0]) - || !contains_vertex(face_vertices, edge_vertices[1])) - { - continue; - } - - auto normal = zero_entity_face_normal(ac, z); - const T n = vec_norm(std::span(normal.data(), normal.size())); - if (n > best_norm) - { - best_norm = n; - best = std::move(normal); - } - } - - if (best_norm <= tol) - std::fill(best.begin(), best.end(), T(0)); - return best; -} - -template -geom::VectorQuantity straight_zero_entity_normal_direction( - const AdaptCell& ac, - int local_zero_entity_id, - T tol) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int tdim = ac.tdim; - const auto host = zero_entity_parent_entity(ac, local_zero_entity_id); - - if (zdim == 1) - { - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - auto verts = ac.entity_to_vertex[1][static_cast(zid)]; - if (verts.size() != 2) - return {{}, T(0), geom::Degeneracy::invalid_parent_entity}; - - std::vector a(static_cast(tdim), T(0)); - std::vector b(static_cast(tdim), T(0)); - for (int d = 0; d < tdim; ++d) - { - a[static_cast(d)] = - ac.vertex_coords[static_cast(verts[0] * tdim + d)]; - b[static_cast(d)] = - ac.vertex_coords[static_cast(verts[1] * tdim + d)]; - } - - geom::VectorQuantity raw; - if (tdim == 2) - { - raw = geom::segment_normal( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - true, - tol); - } - else if (tdim == 3) - { - auto explicit_host_normal = - local_host_face_normal_reference( - ac, local_zero_entity_id, tol); - if (!explicit_host_normal.empty()) - { - raw = geom::in_face_segment_normal( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - std::span( - explicit_host_normal.data(), explicit_host_normal.size()), - true, - tol); - } - else if (host.dim == 2) - { - const auto face_normal = geom::parent_face_normal( - ac.parent_cell_type, host.id, true, tol); - if (face_normal.degenerate()) - return {{}, T(0), face_normal.degeneracy}; - raw = geom::in_face_segment_normal( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - std::span(face_normal.value.data(), face_normal.value.size()), - true, - tol); - } - else - { - auto face_normal = adjacent_zero_face_normal_for_edge( - ac, local_zero_entity_id, tol); - auto face_normal_quantity = geom::make_vector_quantity( - std::move(face_normal), - tol, - geom::Degeneracy::zero_projection); - if (!face_normal_quantity.degenerate()) - { - raw = geom::in_face_segment_normal( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - std::span( - face_normal_quantity.value.data(), - face_normal_quantity.value.size()), - true, - tol); - } - else - { - raw = std::move(face_normal_quantity); - } - } - } - else - { - return {std::vector(static_cast(tdim), T(0)), - T(0), - geom::Degeneracy::zero_projection}; - } - - if (raw.degenerate()) - return raw; - return geom::admissible_direction_in_parent_frame( - ac.parent_cell_type, - host, - std::span(raw.value.data(), raw.value.size()), - tol); - } - - if (zdim == 2 && tdim == 3) - { - auto normal = zero_entity_face_normal(ac, local_zero_entity_id); - return geom::admissible_direction_in_parent_frame( - ac.parent_cell_type, - host, - std::span(normal.data(), normal.size()), - tol); - } - - return {std::vector(static_cast(tdim), T(0)), - T(0), - geom::Degeneracy::zero_projection}; -} - -template -std::vector remove_component(std::vector direction, - std::span tangent) -{ - const T tt = vec_dot(tangent, tangent); - if (tt <= T(0)) - return direction; - const T c = vec_dot( - std::span(direction.data(), direction.size()), tangent) / tt; - for (std::size_t i = 0; i < direction.size(); ++i) - direction[i] -= c * tangent[i]; - return direction; -} - -template -void orient_with_level_set_gradient(std::vector& direction, - std::span grad) -{ - if (direction.size() != grad.size()) - return; - if (vec_dot( - std::span(direction.data(), direction.size()), - grad) < T(0)) - { - for (T& value : direction) - value = -value; - } -} - -template -std::vector physical_normal_reference_direction( - const LevelSetCell& ls_cell, - std::span grad_ref) -{ - std::vector out(grad_ref.begin(), grad_ref.end()); - const int tdim = ls_cell.tdim; - const int gdim = ls_cell.gdim; - if (tdim <= 0 || tdim > 3 || gdim <= 0 - || static_cast(grad_ref.size()) != tdim - || ls_cell.parent_vertex_coords.empty()) - { - return out; - } - - const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); - std::vector gram(static_cast(tdim * tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - const int va = cols[static_cast(a)]; - if (va < 0) - return out; - for (int b = 0; b < tdim; ++b) - { - const int vb = cols[static_cast(b)]; - if (vb < 0) - return out; - T value = T(0); - for (int r = 0; r < gdim; ++r) - { - const T ja = ls_cell.parent_vertex_coords[ - static_cast(va * gdim + r)] - - ls_cell.parent_vertex_coords[ - static_cast(r)]; - const T jb = ls_cell.parent_vertex_coords[ - static_cast(vb * gdim + r)] - - ls_cell.parent_vertex_coords[ - static_cast(r)]; - value += ja * jb; - } - gram[static_cast(a * tdim + b)] = value; - } - } - - std::vector rhs(grad_ref.begin(), grad_ref.end()); - std::vector metric_direction; - if (solve_dense_small(std::move(gram), std::move(rhs), tdim, metric_direction)) - return metric_direction; - return out; -} - -template -bool affine_jacobian(const LevelSetCell& ls_cell, - std::vector& jacobian) -{ - const int tdim = ls_cell.tdim; - const int gdim = ls_cell.gdim; - if (tdim <= 0 || tdim > 3 || gdim <= 0 - || ls_cell.parent_vertex_coords.empty()) - { - return false; - } - - const auto cols = cell::jacobian_col_indices(ls_cell.cell_type); - jacobian.assign(static_cast(gdim * tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - const int va = cols[static_cast(a)]; - if (va < 0) - return false; - for (int r = 0; r < gdim; ++r) - { - jacobian[static_cast(r * tdim + a)] = - ls_cell.parent_vertex_coords[ - static_cast(va * gdim + r)] - - ls_cell.parent_vertex_coords[static_cast(r)]; - } - } - return true; -} - -template -std::vector pull_back_physical_direction(std::span jacobian, - int gdim, - int tdim, - std::span direction_phys, - std::span fallback_ref) -{ - std::vector gram(static_cast(tdim * tdim), T(0)); - std::vector rhs(static_cast(tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - for (int r = 0; r < gdim; ++r) - { - rhs[static_cast(a)] += - jacobian[static_cast(r * tdim + a)] - * direction_phys[static_cast(r)]; - } - for (int b = 0; b < tdim; ++b) - { - T value = T(0); - for (int r = 0; r < gdim; ++r) - { - value += jacobian[static_cast(r * tdim + a)] - * jacobian[static_cast(r * tdim + b)]; - } - gram[static_cast(a * tdim + b)] = value; - } - } - - std::vector out; - if (solve_dense_small(std::move(gram), std::move(rhs), tdim, out)) - return out; - return std::vector(fallback_ref.begin(), fallback_ref.end()); -} - -template -std::vector solve_physical_gradient(std::span jacobian, - int gdim, - int tdim, - std::span grad_ref, - std::span fallback_ref) -{ - if (gdim == tdim) - { - std::vector jt(static_cast(tdim * tdim), T(0)); - for (int a = 0; a < tdim; ++a) - { - for (int r = 0; r < gdim; ++r) - { - jt[static_cast(a * tdim + r)] = - jacobian[static_cast(r * tdim + a)]; - } - } - std::vector rhs(grad_ref.begin(), grad_ref.end()); - std::vector grad_phys; - if (solve_dense_small(std::move(jt), std::move(rhs), tdim, grad_phys)) - return grad_phys; - } - - std::vector out(static_cast(gdim), T(0)); - for (int r = 0; r < gdim; ++r) - { - for (int a = 0; a < tdim; ++a) - { - out[static_cast(r)] += - jacobian[static_cast(r * tdim + a)] - * fallback_ref[static_cast(a)]; - } - } - return out; -} - -template -std::vector level_set_physical_point(const LevelSetCell& ls_cell, - std::span ref) -{ - return cell::push_forward_affine_map( - ls_cell.cell_type, - ls_cell.parent_vertex_coords, - ls_cell.gdim, - ref); -} - -template -std::vector local_host_face_normal_physical( - const LevelSetCell& ls_cell, - const AdaptCell& ac, - int local_zero_entity_id, - T tol) -{ - const auto host = local_host_domain_for_zero_entity( - ac, local_zero_entity_id); - if (ac.tdim != 3 || ls_cell.gdim != 3 - || !host.valid(ac.tdim) || host.dimension != 2) - { - return {}; - } - - const int nverts = static_cast( - host.vertices.size() / static_cast(ac.tdim)); - if (nverts < 3) - return {}; - - const auto p0_ref = std::span(host.vertices.data(), 3); - const auto p1_ref = std::span(host.vertices.data() + 3, 3); - const auto p2_ref = std::span(host.vertices.data() + 6, 3); - const auto p0 = level_set_physical_point(ls_cell, p0_ref); - const auto p1 = level_set_physical_point(ls_cell, p1_ref); - const auto p2 = level_set_physical_point(ls_cell, p2_ref); - - std::array e0 = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; - std::array e1 = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; - auto normal = local_cross( - std::span(e0.data(), 3), - std::span(e1.data(), 3)); - const T nn = local_norm(std::span(normal.data(), 3)); - if (nn <= tol) - return {}; - - std::vector out(3, T(0)); - for (int d = 0; d < 3; ++d) - out[static_cast(d)] = normal[static_cast(d)] / nn; - return out; -} - -template -T curving_level_set_value(const LevelSetCell& ls_cell, - std::span ref) -{ - return ls_cell.value(ref); -} - -template -void curving_level_set_grad(const LevelSetCell& ls_cell, - std::span ref, - std::span grad_ref) -{ - ls_cell.grad(ref, grad_ref); -} - -template -std::vector physical_host_gradient_reference_direction( - const LevelSetCell& ls_cell, - const AdaptCell& ac, - int local_zero_entity_id, - std::span grad_ref, - std::span fallback_ref) -{ - std::vector jacobian; - if (!affine_jacobian(ls_cell, jacobian)) - return std::vector(fallback_ref.begin(), fallback_ref.end()); - - const int tdim = ls_cell.tdim; - const int gdim = ls_cell.gdim; - auto direction_phys = solve_physical_gradient( - std::span(jacobian.data(), jacobian.size()), - gdim, - tdim, - grad_ref, - fallback_ref); - - bool restricted_to_explicit_host_face = false; - const auto explicit_host_normal = - local_host_face_normal_physical( - ls_cell, - ac, - local_zero_entity_id, - std::numeric_limits::epsilon() * T(128)); - if (!explicit_host_normal.empty() - && direction_phys.size() == explicit_host_normal.size()) - { - const T nn = vec_dot( - std::span( - explicit_host_normal.data(), explicit_host_normal.size()), - std::span( - explicit_host_normal.data(), explicit_host_normal.size())); - if (nn > T(0)) - { - const T c = vec_dot( - std::span(direction_phys.data(), direction_phys.size()), - std::span( - explicit_host_normal.data(), explicit_host_normal.size())) / nn; - for (std::size_t d = 0; d < direction_phys.size(); ++d) - direction_phys[d] -= c * explicit_host_normal[d]; - restricted_to_explicit_host_face = true; - } - } - - const int parent_dim = - ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; - const int parent_id = - ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; - if (!restricted_to_explicit_host_face && parent_dim == 2 && gdim == 3) - { - auto face = cell::face_vertices(ac.parent_cell_type, parent_id); - if (face.size() >= 3) - { - std::array e0 = {T(0), T(0), T(0)}; - std::array e1 = {T(0), T(0), T(0)}; - for (int d = 0; d < 3; ++d) - { - e0[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(face[1] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(face[0] * gdim + d)]; - e1[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(face[2] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(face[0] * gdim + d)]; - } - std::array normal = { - e0[1] * e1[2] - e0[2] * e1[1], - e0[2] * e1[0] - e0[0] * e1[2], - e0[0] * e1[1] - e0[1] * e1[0]}; - const T nn = normal[0] * normal[0] - + normal[1] * normal[1] - + normal[2] * normal[2]; - if (nn > T(0)) - { - const T c = (direction_phys[0] * normal[0] - + direction_phys[1] * normal[1] - + direction_phys[2] * normal[2]) / nn; - for (int d = 0; d < 3; ++d) - direction_phys[static_cast(d)] -= c * normal[d]; - } - } - } - else if (!restricted_to_explicit_host_face && parent_dim == 1) - { - auto edges = cell::edges(ac.parent_cell_type); - if (parent_id >= 0 && parent_id < static_cast(edges.size())) - { - const auto edge = edges[static_cast(parent_id)]; - std::vector tangent(static_cast(gdim), T(0)); - for (int d = 0; d < gdim; ++d) - { - tangent[static_cast(d)] = - ls_cell.parent_vertex_coords[ - static_cast(edge[1] * gdim + d)] - - ls_cell.parent_vertex_coords[ - static_cast(edge[0] * gdim + d)]; - } - const T tt = vec_dot( - std::span(tangent.data(), tangent.size()), - std::span(tangent.data(), tangent.size())); - if (tt > T(0)) - { - const T c = vec_dot( - std::span(direction_phys.data(), direction_phys.size()), - std::span(tangent.data(), tangent.size())) / tt; - for (int d = 0; d < gdim; ++d) - direction_phys[static_cast(d)] = - c * tangent[static_cast(d)]; - } - } - } - else if (!restricted_to_explicit_host_face && parent_dim == 0) - { - return std::vector(static_cast(tdim), T(0)); - } - - auto pulled = pull_back_physical_direction( - std::span(jacobian.data(), jacobian.size()), - gdim, - tdim, - std::span(direction_phys.data(), direction_phys.size()), - fallback_ref); - return project_to_local_host_space( - ac, - local_zero_entity_id, - std::span(pulled.data(), pulled.size()), - std::numeric_limits::epsilon() * T(128)); -} - -template -bool accept_seed_if_level_sets_within_tolerance( - const AdaptCell& ac, - int local_zero_entity_id, - std::span* const> active_cells, - std::span seed, - const CurvingOptions& options, - std::vector& out, - ProjectionStats& stats) -{ - if (active_cells.empty()) - return false; - - stats.seed.assign(seed.begin(), seed.end()); - T max_abs_value = T(0); - for (const LevelSetCell* ls_cell : active_cells) - { - if (ls_cell == nullptr) - return false; - const T value = curving_level_set_value(*ls_cell, seed); - max_abs_value = std::max(max_abs_value, std::fabs(value)); - if (max_abs_value > options.ftol) - return false; - } - - out.assign(seed.begin(), seed.end()); - stats = {}; - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = max_abs_value; - stats.active_face_mask = add_near_active_faces( - ac, - seed, - structural_active_face_mask(ac, local_zero_entity_id), - options.active_face_tol); - stats.safe_subspace_dim = active_subspace_dim(ac, stats.active_face_mask); - stats.projection_mode = CurvingProjectionMode::none; - return true; -} - -template -std::vector> scalar_candidate_directions(const AdaptCell& ac, - int local_zero_entity_id, - std::span seed, - std::span grad, - std::span metric_grad, - std::span host_metric_grad, - std::uint32_t active_mask, - const CurvingOptions& options) -{ - std::vector> directions; - const T tol = std::max(options.ftol, T(64) * std::numeric_limits::epsilon()); - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const std::uint32_t host_mask = - structural_active_face_mask(ac, local_zero_entity_id); - - auto append_host_gradient = [&]() - { - // The primary implicit-geometry direction is restricted only by the - // structural parent host entity. Extra near-face constraints can - // overrestrict seeds on warped cut quads and create large tangential - // motion, so those are kept for fallback directions below. - append_admissible_unique_direction( - directions, ac, local_zero_entity_id, host_metric_grad, tol); - }; - - auto append_straight_normal = [&]() -> bool - { - const auto normal = straight_zero_entity_normal_direction( - ac, local_zero_entity_id, tol); - if (!normal.degenerate()) - { - auto direction = normal.value; - orient_with_level_set_gradient(direction, grad); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(direction.data(), direction.size()), - tol); - return true; - } - return false; - }; - - if (options.direction_mode == CurvingDirectionMode::straight_zero_entity_normal) - { - append_straight_normal(); - append_host_gradient(); - } - else - { - append_host_gradient(); - } - - if (active_mask != host_mask) - { - auto d = project_to_active_face_space(ac, metric_grad, active_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - tol); - } - - if (zdim == 1) - { - const auto tangent = zero_entity_edge_tangent(ac, local_zero_entity_id); - auto d = project_to_active_face_space(ac, metric_grad, active_mask); - d = remove_component( - std::move(d), std::span(tangent.data(), tangent.size())); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - tol); - } - else if (zdim == 2) - { - const auto normal = straight_zero_entity_normal_direction( - ac, local_zero_entity_id, tol); - if (!normal.degenerate()) - { - auto d = project_to_active_face_space( - ac, - std::span(normal.value.data(), normal.value.size()), - active_mask); - orient_with_level_set_gradient(d, grad); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - tol); - } - } - - { - auto d = project_to_active_face_space(ac, grad, active_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - tol); - } - - const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); - const int nv = cell::get_num_vertices(ac.parent_cell_type); - for (int v = 0; v < nv; ++v) - { - std::vector ray(static_cast(ac.tdim), T(0)); - for (int d = 0; d < ac.tdim; ++d) - ray[static_cast(d)] = - seed[static_cast(d)] - - parent_vertices[static_cast(v * ac.tdim + d)]; - auto d = project_to_active_face_space( - ac, std::span(ray.data(), ray.size()), active_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - tol); - } - - const auto basis = active_nullspace_basis(ac, active_mask); - for (const auto& b : basis) - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(b.data(), b.size()), - tol); - - if (options.direction_mode == CurvingDirectionMode::level_set_gradient) - append_straight_normal(); - - return directions; -} - -template -bool try_scalar_line_search(const AdaptCell& ac, - int local_zero_entity_id, - std::span seed, - std::span> directions, - std::span gradient_at_seed, - const CurvingOptions& options, - Eval&& eval_on_point, - std::vector& out, - ProjectionStats& stats) -{ - const int tdim = ac.tdim; - for (const auto& unit : directions) - { - std::vector direction = unit; - if (direction.size() != static_cast(tdim)) - { - stats.failure_code = CurvingFailureCode::singular_gradient_system; - continue; - } - if (gradient_at_seed.size() == direction.size() - && vec_dot(gradient_at_seed, std::span( - direction.data(), direction.size())) < T(0)) - { - for (T& value : direction) - value = -value; - } - stats.seed.assign(seed.begin(), seed.end()); - stats.direction = direction; - stats.clip_lo = std::numeric_limits::quiet_NaN(); - stats.clip_hi = std::numeric_limits::quiet_NaN(); - stats.root_t = std::numeric_limits::quiet_NaN(); - - T lo = T(0), hi = T(0); - if (!host_parameter_interval( - ac, - local_zero_entity_id, - seed, - std::span(direction.data(), direction.size()), - options.domain_tol, lo, hi)) - { - stats.failure_code = CurvingFailureCode::no_host_interval; - continue; - } - stats.clip_lo = lo; - stats.clip_hi = hi; - if (!(lo <= T(0) && T(0) <= hi)) - { - stats.failure_code = CurvingFailureCode::no_host_interval; - continue; - } - - auto eval_line = [&](T t) -> T - { - std::array x = {T(0), T(0), T(0)}; - for (int d = 0; d < tdim; ++d) - x[static_cast(d)] = - seed[static_cast(d)] - + t * direction[static_cast(d)]; - return eval_on_point(std::span(x.data(), static_cast(tdim))); - }; - - const T f_seed = eval_line(T(0)); - if (std::fabs(f_seed) <= options.ftol) - { - out.assign(seed.begin(), seed.end()); - stats.iterations = 0; - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = std::fabs(f_seed); - stats.root_t = T(0); - return true; - } - - T a = T(0); - T b = T(0); - if (f_seed > T(0)) - { - a = lo; - b = T(0); - } - else - { - a = T(0); - b = hi; - } - if (!(a < b)) - { - stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; - continue; - } - - const T fa = eval_line(a); - const T fb = (b == T(0)) ? f_seed : eval_line(b); - if (!(std::isfinite(fa) && std::isfinite(fb)) || fa * fb > T(0)) - { - stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; - continue; - } - - int iterations = 0; - bool converged = false; - const T linear_guess = cell::edge_root::linear_root_parameter(fa, fb); - const T initial_guess = a + (b - a) * std::clamp(linear_guess, T(0), T(1)); - const T root_t = cell::edge_root::newton_parameter( - eval_line, a, b, fa, fb, - initial_guess, - options.max_iter, options.xtol, options.ftol, - &iterations, &converged); - T accepted_root_t = root_t; - stats.iterations = iterations; - T residual = std::fabs(eval_line(root_t)); - stats.residual = residual; - if (!converged && residual > options.ftol) - { - int fallback_iterations = 0; - bool fallback_converged = false; - try - { - const T fallback_root_t = cell::edge_root::itp_parameter( - eval_line, - a, - b, - fa, - fb, - initial_guess, - options.max_iter, - options.xtol, - options.ftol, - &fallback_iterations, - &fallback_converged); - const T fallback_residual = std::fabs(eval_line(fallback_root_t)); - if (fallback_converged || fallback_residual <= options.ftol) - { - accepted_root_t = fallback_root_t; - iterations += fallback_iterations; - residual = fallback_residual; - stats.iterations = iterations; - stats.residual = residual; - converged = true; - } - } - catch (const std::exception&) - { - } - if (!converged && residual > options.ftol) - { - stats.failure_code = CurvingFailureCode::brent_failed; - stats.root_t = accepted_root_t; - continue; - } - } - - out.assign(seed.begin(), seed.end()); - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] += accepted_root_t * direction[static_cast(d)]; - const bool inside = cell::edge_root::is_inside_reference_domain( - std::span(out.data(), out.size()), - ac.parent_cell_type, - options.domain_tol); - if (!inside) - { - stats.status = CurvingStatus::failed; - stats.failure_code = CurvingFailureCode::outside_host_domain; - stats.root_t = accepted_root_t; - return false; - } - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.direction = direction; - stats.root_t = accepted_root_t; - stats.residual = residual; - return true; - } - - stats.status = CurvingStatus::failed; - return false; -} - -template -bool constrained_scalar_newton(const AdaptCell& ac, - int local_zero_entity_id, - std::span seed, - std::uint32_t active_mask, - const CurvingOptions& options, - Eval&& eval_on_point, - Grad&& grad_on_point, - std::vector& out, - ProjectionStats& stats) -{ - const int tdim = ac.tdim; - out.assign(seed.begin(), seed.end()); - stats.projection_mode = CurvingProjectionMode::constrained_newton; - stats.active_face_mask = active_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - - if (stats.safe_subspace_dim <= 0) - { - stats.failure_code = CurvingFailureCode::constrained_newton_failed; - return false; - } - - std::vector grad(static_cast(tdim), T(0)); - for (int iter = 0; iter < options.max_iter; ++iter) - { - const T f = eval_on_point(std::span(out.data(), out.size())); - if (std::fabs(f) <= options.ftol) - { - stats.iterations = iter; - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = std::fabs(f); - stats.direction = normalized_delta_direction( - seed, - std::span(out.data(), out.size()), - options.xtol); - return true; - } - - grad_on_point(std::span(out.data(), out.size()), - std::span(grad.data(), grad.size())); - auto pg = project_to_active_face_space( - ac, std::span(grad.data(), grad.size()), active_mask); - const T gg = vec_dot( - std::span(pg.data(), pg.size()), - std::span(pg.data(), pg.size())); - if (gg <= options.ftol * options.ftol) - { - stats.iterations = iter; - stats.failure_code = CurvingFailureCode::singular_gradient_system; - stats.residual = std::fabs(f); - return false; - } - - std::vector delta(static_cast(tdim), T(0)); - for (int d = 0; d < tdim; ++d) - delta[static_cast(d)] = - -f * pg[static_cast(d)] / gg; - - T lo = T(0), hi = T(0); - if (!host_parameter_interval( - ac, - local_zero_entity_id, - std::span(out.data(), out.size()), - std::span(delta.data(), delta.size()), - options.domain_tol, - lo, - hi)) - { - stats.failure_code = CurvingFailureCode::no_host_interval; - stats.residual = std::fabs(f); - return false; - } - - T step = std::min(T(1), hi); - if (!(step > T(0))) - { - stats.failure_code = CurvingFailureCode::line_search_failed; - stats.residual = std::fabs(f); - return false; - } - - const T merit = T(0.5) * f * f; - bool accepted = false; - std::vector candidate(static_cast(tdim), T(0)); - for (int ls = 0; ls < 16; ++ls) - { - for (int d = 0; d < tdim; ++d) - candidate[static_cast(d)] = - out[static_cast(d)] - + step * delta[static_cast(d)]; - if (!cell::edge_root::is_inside_reference_domain( - std::span(candidate.data(), candidate.size()), - ac.parent_cell_type, - options.domain_tol)) - { - step *= T(0.5); - continue; - } - const T cand_f = eval_on_point( - std::span(candidate.data(), candidate.size())); - const T cand_merit = T(0.5) * cand_f * cand_f; - if (cand_merit < merit) - { - out = candidate; - accepted = true; - break; - } - step *= T(0.5); - } - if (!accepted) - { - stats.iterations = iter + 1; - stats.failure_code = CurvingFailureCode::line_search_failed; - stats.residual = std::fabs(f); - return false; - } - } - - const T f = eval_on_point(std::span(out.data(), out.size())); - stats.iterations = options.max_iter; - stats.residual = std::fabs(f); - if (stats.residual <= options.ftol) - { - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.direction = normalized_delta_direction( - seed, - std::span(out.data(), out.size()), - options.xtol); - return true; - } - stats.failure_code = CurvingFailureCode::constrained_newton_failed; - return false; -} - -template -bool solve_dense_small(std::vector A, std::vector b, int n, std::vector& x) -{ - x.assign(static_cast(n), T(0)); - if (n <= 0 || n > 3) - return false; - - for (int k = 0; k < n; ++k) - { - int pivot = k; - T best = std::fabs(A[static_cast(k * n + k)]); - for (int r = k + 1; r < n; ++r) - { - const T val = std::fabs(A[static_cast(r * n + k)]); - if (val > best) - { - best = val; - pivot = r; - } - } - if (best <= T(64) * std::numeric_limits::epsilon()) - return false; - if (pivot != k) - { - for (int c = k; c < n; ++c) - std::swap(A[static_cast(k * n + c)], - A[static_cast(pivot * n + c)]); - std::swap(b[static_cast(k)], b[static_cast(pivot)]); - } - - const T diag = A[static_cast(k * n + k)]; - for (int c = k; c < n; ++c) - A[static_cast(k * n + c)] /= diag; - b[static_cast(k)] /= diag; - - for (int r = 0; r < n; ++r) - { - if (r == k) - continue; - const T factor = A[static_cast(r * n + k)]; - if (factor == T(0)) - continue; - for (int c = k; c < n; ++c) - A[static_cast(r * n + c)] -= factor * A[static_cast(k * n + c)]; - b[static_cast(r)] -= factor * b[static_cast(k)]; - } - } - - x = std::move(b); - return true; -} - -template -std::vector projected_direction_to_host(const AdaptCell& ac, - int local_zero_entity_id, - std::span direction) -{ - const int tdim = ac.tdim; - std::vector out(direction.begin(), direction.end()); - const int parent_dim = ac.zero_entity_parent_dim[static_cast(local_zero_entity_id)]; - const int parent_id = ac.zero_entity_parent_id[static_cast(local_zero_entity_id)]; - if (parent_dim < 0 || parent_dim >= tdim) - return out; - - std::vector ref_vertices = cell::reference_vertices(ac.parent_cell_type); - std::vector> basis; - - auto add_basis_from_vertices = [&](int v0, int v1) - { - std::array b = {T(0), T(0), T(0)}; - for (int d = 0; d < tdim; ++d) - b[static_cast(d)] = - ref_vertices[static_cast(v1 * tdim + d)] - - ref_vertices[static_cast(v0 * tdim + d)]; - basis.push_back(b); - }; - - if (parent_dim == 1) - { - auto edges = cell::edges(ac.parent_cell_type); - if (parent_id >= 0 && parent_id < static_cast(edges.size())) - add_basis_from_vertices(edges[static_cast(parent_id)][0], - edges[static_cast(parent_id)][1]); - } - else if (parent_dim == 2 && ac.tdim == 3) - { - auto fv = cell::face_vertices(ac.parent_cell_type, parent_id); - add_basis_from_vertices(fv[0], fv[1]); - add_basis_from_vertices(fv[0], fv[2]); - } - - if (basis.empty()) - return out; - - std::fill(out.begin(), out.end(), T(0)); - if (basis.size() == 1) - { - T bd = T(0); - T bb = T(0); - for (int d = 0; d < tdim; ++d) - { - bd += basis[0][static_cast(d)] * direction[static_cast(d)]; - bb += basis[0][static_cast(d)] * basis[0][static_cast(d)]; - } - if (bb > T(0)) - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] = (bd / bb) * basis[0][static_cast(d)]; - return out; - } - - T g00 = T(0), g01 = T(0), g11 = T(0), r0 = T(0), r1 = T(0); - for (int d = 0; d < tdim; ++d) - { - g00 += basis[0][static_cast(d)] * basis[0][static_cast(d)]; - g01 += basis[0][static_cast(d)] * basis[1][static_cast(d)]; - g11 += basis[1][static_cast(d)] * basis[1][static_cast(d)]; - r0 += basis[0][static_cast(d)] * direction[static_cast(d)]; - r1 += basis[1][static_cast(d)] * direction[static_cast(d)]; - } - const T det = g00 * g11 - g01 * g01; - if (std::fabs(det) <= T(64) * std::numeric_limits::epsilon()) - return out; - const T a = ( r0 * g11 - r1 * g01) / det; - const T b = (-r0 * g01 + r1 * g00) / det; - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] = - a * basis[0][static_cast(d)] - + b * basis[1][static_cast(d)]; - return out; -} - -template -std::vector active_level_sets(std::uint64_t mask) -{ - std::vector ids; - for (int i = 0; i < 64; ++i) - if ((mask & (std::uint64_t(1) << i)) != 0) - ids.push_back(i); - return ids; -} - -template -const LevelSetCell& find_level_set_cell(std::span> cells, - std::span offsets, - int cut_cell_id, - int level_set_id) -{ - for (int i = offsets[static_cast(cut_cell_id)]; - i < offsets[static_cast(cut_cell_id + 1)]; ++i) - { - if (cells[static_cast(i)].level_set_id == level_set_id) - return cells[static_cast(i)]; - } - throw std::runtime_error("curving: missing LevelSetCell for zero entity"); -} - -template -bool scalar_project(const AdaptCell& ac, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span seed, - const CurvingOptions& options, - std::vector& out, - ProjectionStats& stats) -{ - const int tdim = ac.tdim; - stats = {}; - stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; - const T seed_value = curving_level_set_value(ls_cell, seed); - if (std::fabs(seed_value) <= options.ftol) - { - out.assign(seed.begin(), seed.end()); - stats.seed.assign(seed.begin(), seed.end()); - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = std::fabs(seed_value); - stats.active_face_mask = add_near_active_faces( - ac, - seed, - structural_active_face_mask(ac, local_zero_entity_id), - options.active_face_tol); - stats.safe_subspace_dim = active_subspace_dim(ac, stats.active_face_mask); - stats.projection_mode = CurvingProjectionMode::none; - return true; - } - std::vector grad(static_cast(tdim), T(0)); - curving_level_set_grad( - ls_cell, seed, std::span(grad.data(), grad.size())); - const auto metric_grad = physical_normal_reference_direction( - ls_cell, std::span(grad.data(), grad.size())); - const auto host_metric_grad = physical_host_gradient_reference_direction( - ls_cell, - ac, - local_zero_entity_id, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size())); - - auto eval_on_point = [&](std::span x) -> T - { - return curving_level_set_value(ls_cell, x); - }; - auto grad_on_point = [&](std::span x, std::span out_grad) - { - curving_level_set_grad(ls_cell, x, out_grad); - }; - - std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); - active_mask = add_near_active_faces(ac, seed, active_mask, options.active_face_tol); - - stats.active_face_mask = active_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; - - auto directions = scalar_candidate_directions( - ac, - local_zero_entity_id, - seed, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size()), - std::span(host_metric_grad.data(), host_metric_grad.size()), - active_mask, - options); - if (try_scalar_line_search( - ac, - local_zero_entity_id, - seed, - std::span>(directions.data(), directions.size()), - std::span(grad.data(), grad.size()), - options, - eval_on_point, - out, - stats)) - { - stats.active_face_mask = active_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; - return true; - } - - int closest_face_id = -1; - const std::uint32_t retry_mask = - add_closest_faces(ac, seed, active_mask, closest_face_id); - if (retry_mask != active_mask) - { - ProjectionStats retry_stats = stats; - retry_stats.active_face_mask = retry_mask; - retry_stats.closest_face_id = closest_face_id; - retry_stats.safe_subspace_dim = active_subspace_dim(ac, retry_mask); - retry_stats.projection_mode = CurvingProjectionMode::closest_face_retry; - retry_stats.retry_count = 1; - - directions = scalar_candidate_directions( - ac, - local_zero_entity_id, - seed, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size()), - std::span(host_metric_grad.data(), host_metric_grad.size()), - retry_mask, - options); - if (try_scalar_line_search( - ac, - local_zero_entity_id, - seed, - std::span>(directions.data(), directions.size()), - std::span(grad.data(), grad.size()), - options, - eval_on_point, - out, - retry_stats)) - { - retry_stats.active_face_mask = retry_mask; - retry_stats.closest_face_id = closest_face_id; - retry_stats.safe_subspace_dim = active_subspace_dim(ac, retry_mask); - retry_stats.projection_mode = CurvingProjectionMode::closest_face_retry; - retry_stats.retry_count = 1; - stats = retry_stats; - return true; - } - stats = retry_stats; - stats.failure_code = CurvingFailureCode::closest_face_retry_failed; - } - - std::array newton_masks = { - (retry_mask != active_mask) ? retry_mask : active_mask, - active_mask - }; - const int newton_attempts = (newton_masks[0] == newton_masks[1]) ? 1 : 2; - ProjectionStats best_newton_stats = stats; - for (int attempt = 0; attempt < newton_attempts; ++attempt) - { - ProjectionStats newton_stats = stats; - const std::uint32_t newton_mask = newton_masks[static_cast(attempt)]; - newton_stats.active_face_mask = newton_mask; - newton_stats.closest_face_id = (newton_mask == retry_mask) ? closest_face_id : -1; - newton_stats.retry_count = (retry_mask != active_mask) ? 2 + attempt : 1 + attempt; - if (constrained_scalar_newton( - ac, - local_zero_entity_id, - seed, - newton_mask, - options, - eval_on_point, - grad_on_point, - out, - newton_stats)) - { - stats = newton_stats; - return true; - } - best_newton_stats = newton_stats; - } - - stats = best_newton_stats; - stats.status = CurvingStatus::failed; - if (stats.failure_code == CurvingFailureCode::none) - stats.failure_code = CurvingFailureCode::constrained_newton_failed; - return false; -} - -template -bool fixed_ray_scalar_project(const AdaptCell& ac, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span seed, - const CurvingOptions& options, - std::vector& out, - ProjectionStats& stats) -{ - const int tdim = ac.tdim; - stats = {}; - const std::uint32_t host_mask = - structural_active_face_mask(ac, local_zero_entity_id); - stats.active_face_mask = host_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; - stats.failure_code = CurvingFailureCode::no_sign_changing_bracket; - - const T seed_value = curving_level_set_value(ls_cell, seed); - if (std::fabs(seed_value) <= options.ftol) - { - out.assign(seed.begin(), seed.end()); - stats.seed.assign(seed.begin(), seed.end()); - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = std::fabs(seed_value); - return true; - } - - std::vector grad(static_cast(tdim), T(0)); - curving_level_set_grad( - ls_cell, seed, std::span(grad.data(), grad.size())); - const auto metric_grad = physical_normal_reference_direction( - ls_cell, std::span(grad.data(), grad.size())); - auto direction = physical_host_gradient_reference_direction( - ls_cell, - ac, - local_zero_entity_id, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size())); - direction = project_to_active_face_space( - ac, std::span(direction.data(), direction.size()), host_mask); - - std::vector> directions; - const T direction_tol = - std::max(options.ftol, T(64) * std::numeric_limits::epsilon()); - - auto append_gradient_direction = [&]() - { - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(direction.data(), direction.size()), - direction_tol); - }; - auto append_normal_direction = [&]() -> bool - { - const auto normal = straight_zero_entity_normal_direction( - ac, local_zero_entity_id, direction_tol); - if (!normal.degenerate()) - { - auto oriented = normal.value; - orient_with_level_set_gradient(oriented, std::span(grad.data(), grad.size())); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(oriented.data(), oriented.size()), - direction_tol); - return true; - } - return false; - }; - auto append_gradient_fallback_directions = [&]() - { - { - auto d = project_to_active_face_space( - ac, std::span(grad.data(), grad.size()), host_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - direction_tol); - } - { - auto d = project_to_active_face_space( - ac, std::span(metric_grad.data(), metric_grad.size()), host_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - direction_tol); - } - - const auto parent_vertices = cell::reference_vertices(ac.parent_cell_type); - const int nv = cell::get_num_vertices(ac.parent_cell_type); - for (int v = 0; v < nv; ++v) - { - std::vector ray(static_cast(tdim), T(0)); - for (int d = 0; d < tdim; ++d) - ray[static_cast(d)] = - seed[static_cast(d)] - - parent_vertices[static_cast(v * tdim + d)]; - auto d = project_to_active_face_space( - ac, std::span(ray.data(), ray.size()), host_mask); - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(d.data(), d.size()), - direction_tol); - } - - const auto basis = active_nullspace_basis(ac, host_mask); - for (const auto& b : basis) - append_admissible_unique_direction( - directions, - ac, - local_zero_entity_id, - std::span(b.data(), b.size()), - direction_tol); - }; - - if (options.direction_mode == CurvingDirectionMode::straight_zero_entity_normal) - { - append_normal_direction(); - append_gradient_direction(); - append_gradient_fallback_directions(); - } - else - { - append_gradient_direction(); - append_gradient_fallback_directions(); - append_normal_direction(); - } - - if (directions.empty()) - { - stats.status = CurvingStatus::failed; - stats.failure_code = CurvingFailureCode::singular_gradient_system; - return false; - } - - auto eval_on_point = [&](std::span x) -> T - { - return curving_level_set_value(ls_cell, x); - }; - - ProjectionStats best_stats = stats; - for (const auto& unit : directions) - { - std::vector candidate; - ProjectionStats candidate_stats = stats; - if (!try_scalar_line_search( - ac, - local_zero_entity_id, - seed, - std::span>(&unit, 1), - std::span(grad.data(), grad.size()), - options, - eval_on_point, - candidate, - candidate_stats)) - { - best_stats = candidate_stats; - continue; - } - - out = std::move(candidate); - stats = candidate_stats; - stats.active_face_mask = host_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; - return true; - } - - stats = best_stats; - stats.active_face_mask = host_mask; - stats.safe_subspace_dim = active_subspace_dim(ac, host_mask); - stats.projection_mode = CurvingProjectionMode::safe_line; - stats.status = CurvingStatus::failed; - if (stats.failure_code == CurvingFailureCode::none) - stats.failure_code = CurvingFailureCode::projection_failed; - return false; -} - -template -bool vector_project(const AdaptCell& ac, - int local_zero_entity_id, - std::span* const> ls_cells, - std::span seed, - const CurvingOptions& options, - std::vector& out, - ProjectionStats& stats) -{ - const int tdim = ac.tdim; - const int m = static_cast(ls_cells.size()); - stats = {}; - stats.failure_code = CurvingFailureCode::projection_failed; - out.assign(seed.begin(), seed.end()); - if (m <= 0 || m > tdim) - { - stats.failure_code = CurvingFailureCode::invalid_constraint_count; - return false; - } - - if (accept_seed_if_level_sets_within_tolerance( - ac, local_zero_entity_id, ls_cells, seed, options, out, stats)) - return true; - - std::vector values(static_cast(m), T(0)); - std::vector grads(static_cast(m * tdim), T(0)); - std::uint32_t active_mask = structural_active_face_mask(ac, local_zero_entity_id); - active_mask = add_near_active_faces(ac, seed, active_mask, options.active_face_tol); - int closest_face_id = -1; - int retry_count = 0; - - for (int iter = 0; iter < options.max_iter; ++iter) - { - T norm_f = T(0); - T max_f = T(0); - for (int i = 0; i < m; ++i) - { - values[static_cast(i)] = - ls_cells[static_cast(i)]->value( - std::span(out.data(), out.size())); - norm_f += values[static_cast(i)] * values[static_cast(i)]; - max_f = std::max(max_f, std::fabs(values[static_cast(i)])); - ls_cells[static_cast(i)]->grad( - std::span(out.data(), out.size()), - std::span(grads.data() + static_cast(i * tdim), - static_cast(tdim))); - } - norm_f = std::sqrt(norm_f); - if (max_f <= options.ftol) - { - stats.iterations = iter; - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.residual = max_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - stats.projection_mode = - (iter == 0) ? CurvingProjectionMode::none : CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - stats.direction = normalized_delta_direction( - seed, - std::span(out.data(), out.size()), - options.xtol); - return true; - } - - const auto basis = active_nullspace_basis(ac, active_mask); - const int q = static_cast(basis.size()); - if (q < m) - { - const std::uint32_t retry_mask = - add_closest_faces( - ac, - std::span(out.data(), out.size()), - active_mask, - closest_face_id); - if (retry_mask != active_mask) - { - active_mask = retry_mask; - ++retry_count; - continue; - } - stats.iterations = iter; - stats.failure_code = CurvingFailureCode::singular_gradient_system; - stats.residual = norm_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = q; - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - return false; - } - - std::vector jq(static_cast(m * q), T(0)); - for (int i = 0; i < m; ++i) - { - std::span gi( - grads.data() + static_cast(i * tdim), - static_cast(tdim)); - for (int k = 0; k < q; ++k) - jq[static_cast(i * q + k)] = - vec_dot(gi, std::span(basis[static_cast(k)].data(), - basis[static_cast(k)].size())); - } - - std::vector normal_matrix(static_cast(m * m), T(0)); - for (int i = 0; i < m; ++i) - { - for (int j = 0; j < m; ++j) - { - T a = T(0); - for (int k = 0; k < q; ++k) - a += jq[static_cast(i * q + k)] - * jq[static_cast(j * q + k)]; - normal_matrix[static_cast(i * m + j)] = a; - } - } - std::vector rhs(static_cast(m), T(0)); - for (int i = 0; i < m; ++i) - rhs[static_cast(i)] = -values[static_cast(i)]; - - std::vector lambda; - if (!solve_dense_small(normal_matrix, rhs, m, lambda)) - { - const std::uint32_t retry_mask = - add_closest_faces( - ac, - std::span(out.data(), out.size()), - active_mask, - closest_face_id); - if (retry_mask != active_mask) - { - active_mask = retry_mask; - ++retry_count; - continue; - } - stats.iterations = iter; - stats.failure_code = CurvingFailureCode::singular_gradient_system; - stats.residual = norm_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = q; - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - return false; - } - - std::vector delta(static_cast(tdim), T(0)); - for (int k = 0; k < q; ++k) - { - T dy = T(0); - for (int i = 0; i < m; ++i) - dy += jq[static_cast(i * q + k)] - * lambda[static_cast(i)]; - for (int d = 0; d < tdim; ++d) - delta[static_cast(d)] += - dy * basis[static_cast(k)][static_cast(d)]; - } - - if (vec_norm(std::span(delta.data(), delta.size())) <= options.xtol) - { - stats.iterations = iter; - stats.failure_code = CurvingFailureCode::singular_gradient_system; - stats.residual = norm_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = q; - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - return false; - } - - T lo = T(0), hi = T(0); - if (!host_parameter_interval( - ac, - local_zero_entity_id, - std::span(out.data(), out.size()), - std::span(delta.data(), delta.size()), - options.domain_tol, - lo, - hi)) - { - stats.iterations = iter; - stats.failure_code = CurvingFailureCode::no_host_interval; - stats.residual = norm_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = q; - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - return false; - } - - T step = T(1); - if (hi > T(0) && hi < step) - step = hi; - bool accepted = false; - std::vector candidate(static_cast(tdim), T(0)); - const T merit = T(0.5) * norm_f * norm_f; - for (int ls = 0; ls < 12; ++ls) - { - for (int d = 0; d < tdim; ++d) - candidate[static_cast(d)] = - out[static_cast(d)] + step * delta[static_cast(d)]; - if (!cell::edge_root::is_inside_reference_domain( - std::span(candidate.data(), candidate.size()), - ac.parent_cell_type, - options.domain_tol)) - { - step *= T(0.5); - continue; - } - T cand_norm = T(0); - for (int i = 0; i < m; ++i) - { - const T f = ls_cells[static_cast(i)]->value( - std::span(candidate.data(), candidate.size())); - cand_norm += f * f; - } - const T cand_merit = T(0.5) * cand_norm; - if (cand_merit < merit) - { - out = candidate; - accepted = true; - break; - } - step *= T(0.5); - } - if (!accepted) - { - const std::uint32_t retry_mask = - add_closest_faces( - ac, - std::span(out.data(), out.size()), - active_mask, - closest_face_id); - if (retry_mask != active_mask) - { - active_mask = retry_mask; - ++retry_count; - continue; - } - stats.iterations = iter + 1; - stats.failure_code = CurvingFailureCode::line_search_failed; - stats.residual = norm_f; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = q; - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - return false; - } - } - - T final_max = T(0); - for (int i = 0; i < m; ++i) - { - const T f = ls_cells[static_cast(i)]->value( - std::span(out.data(), out.size())); - final_max = std::max(final_max, std::fabs(f)); - } - stats.iterations = options.max_iter; - stats.residual = final_max; - stats.active_face_mask = active_mask; - stats.closest_face_id = closest_face_id; - stats.safe_subspace_dim = active_subspace_dim(ac, active_mask); - stats.projection_mode = CurvingProjectionMode::vector_newton; - stats.retry_count = retry_count; - if (final_max <= options.ftol) - { - stats.status = CurvingStatus::curved; - stats.failure_code = CurvingFailureCode::none; - stats.direction = normalized_delta_direction( - seed, - std::span(out.data(), out.size()), - options.xtol); - return true; - } - stats.status = CurvingStatus::failed; - stats.failure_code = CurvingFailureCode::max_iterations; - return false; -} - -template -bool project_seed_to_zero_entity(const AdaptCell& ac, - int local_zero_entity_id, - std::span* const> active_cells, - std::span seed, - const CurvingOptions& options, - std::vector& projected, - ProjectionStats& stats) -{ - if (active_cells.size() == 1) - { - return scalar_project( - ac, local_zero_entity_id, *active_cells[0], seed, options, projected, stats); - } - return vector_project( - ac, local_zero_entity_id, active_cells, seed, options, projected, stats); -} - -template -std::vector preferred_scalar_projection_direction( - const AdaptCell& ac, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span seed, - const CurvingOptions& options) -{ - if (local_zero_entity_id < 0 - || local_zero_entity_id >= ac.n_zero_entities()) - { - return {}; - } - - std::vector grad(static_cast(ac.tdim), T(0)); - curving_level_set_grad( - ls_cell, seed, std::span(grad.data(), grad.size())); - const auto metric_grad = physical_normal_reference_direction( - ls_cell, std::span(grad.data(), grad.size())); - const auto host_metric_grad = physical_host_gradient_reference_direction( - ls_cell, - ac, - local_zero_entity_id, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size())); - - std::uint32_t active_mask = structural_active_face_mask( - ac, local_zero_entity_id); - active_mask = add_near_active_faces( - ac, seed, active_mask, options.active_face_tol); - - auto directions = scalar_candidate_directions( - ac, - local_zero_entity_id, - seed, - std::span(grad.data(), grad.size()), - std::span(metric_grad.data(), metric_grad.size()), - std::span(host_metric_grad.data(), host_metric_grad.size()), - active_mask, - options); - if (directions.empty()) - return {}; - return directions.front(); -} - -template -bool build_hierarchical_face_nodes( - CurvedZeroEntityState& state, - const AdaptCell& ac, - int local_zero_entity_id, - std::span* const> active_cells, - std::span> boundary_edges, - const CurvingOptions& options) -{ - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - const auto entity_type = ac.entity_types[2][static_cast(zid)]; - auto verts_span = ac.entity_to_vertex[2][static_cast(zid)]; - std::vector verts; - verts.reserve(verts_span.size()); - for (const auto v : verts_span) - verts.push_back(static_cast(v)); - - const int order = std::max(options.geometry_order, 1); - const T eps = T(64) * std::numeric_limits::epsilon(); - std::vector projected; - state.ref_nodes.clear(); - - auto append_vertex = [&](int vertex_id) - { - for (int d = 0; d < ac.tdim; ++d) - state.ref_nodes.push_back( - ac.vertex_coords[static_cast(vertex_id * ac.tdim + d)]); - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::exact_vertex), ac.tdim); - }; - - auto straight_seed = [&](std::span weights) - { - std::array seed = {T(0), T(0), T(0)}; - for (std::size_t v = 0; v < weights.size(); ++v) - { - for (int d = 0; d < ac.tdim; ++d) - { - seed[static_cast(d)] += weights[v] * ac.vertex_coords[ - static_cast(verts[v] * ac.tdim + d)]; - } - } - return seed; - }; - - auto project_face_interior_seed = [&](std::span seed, - std::vector& candidate, - ProjectionStats& stats) - { - candidate.clear(); - stats = {}; - if (active_cells.size() != 1) - { - stats.failure_code = CurvingFailureCode::invalid_constraint_count; - stats.projection_mode = CurvingProjectionMode::safe_line; - stats.active_face_mask = - structural_active_face_mask(ac, local_zero_entity_id); - stats.safe_subspace_dim = active_subspace_dim( - ac, stats.active_face_mask); - return false; - } - return fixed_ray_scalar_project( - ac, - local_zero_entity_id, - *active_cells[0], - seed, - options, - candidate, - stats); - }; - - auto append_straight_face_interior_seed = [&](std::span weights) - { - const auto seed = straight_seed(weights); - ProjectionStats stats; - if (!project_face_interior_seed( - std::span(seed.data(), static_cast(ac.tdim)), - projected, - stats)) - { - append_node_stats(state, stats, ac.tdim); - return false; - } - state.ref_nodes.insert(state.ref_nodes.end(), projected.begin(), projected.end()); - append_node_stats(state, stats, ac.tdim); - return true; - }; - - auto append_straight_edge_node = [&](int v0, int v1, T t) - { - for (int d = 0; d < ac.tdim; ++d) - { - const T x0 = ac.vertex_coords[static_cast(v0 * ac.tdim + d)]; - const T x1 = ac.vertex_coords[static_cast(v1 * ac.tdim + d)]; - state.ref_nodes.push_back((T(1) - t) * x0 + t * x1); - } - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); - }; - - auto eval_curved_edge = [&](int v0, int v1, T t, std::vector& x) -> bool - { - const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); - if (edge == nullptr || !edge->use_curved_state) - return false; - x = eval_edge_state_at(*edge, options, v0, v1, t); - return true; - }; - - auto add_scaled = [&](std::array& out, - T scale, - std::span x) - { - for (int d = 0; d < ac.tdim; ++d) - out[static_cast(d)] += scale * x[static_cast(d)]; - }; - - auto sub_scaled_vertex = [&](std::array& out, T scale, int vertex_id) - { - for (int d = 0; d < ac.tdim; ++d) - out[static_cast(d)] -= scale * ac.vertex_coords[ - static_cast(vertex_id * ac.tdim + d)]; - }; - - auto append_transfinite_or_straight_seed = - [&](std::array transfinite_seed, - std::span straight_weights) -> bool - { - // Strict face-interior nodes are lifted by a scalar solve along one - // frozen ray through the transfinite seed. This preserves the face - // parameter location; the generic scalar projector can otherwise - // select fallback directions that satisfy phi=0 but drift tangentially. - ProjectionStats transfinite_stats; - std::vector transfinite_projected; - if (project_face_interior_seed( - std::span( - transfinite_seed.data(), static_cast(ac.tdim)), - transfinite_projected, - transfinite_stats)) - { - state.ref_nodes.insert( - state.ref_nodes.end(), - transfinite_projected.begin(), - transfinite_projected.end()); - append_node_stats(state, transfinite_stats, ac.tdim); - return true; - } - - const auto fallback_seed = straight_seed(straight_weights); - ProjectionStats fallback_stats; - std::vector fallback_projected; - if (!project_face_interior_seed( - std::span( - fallback_seed.data(), static_cast(ac.tdim)), - fallback_projected, - fallback_stats)) - { - append_node_stats(state, fallback_stats, ac.tdim); - return false; - } - state.ref_nodes.insert( - state.ref_nodes.end(), - fallback_projected.begin(), - fallback_projected.end()); - append_node_stats(state, fallback_stats, ac.tdim); - return true; - }; - - auto append_triangle_interior_node = [&](std::array w) -> bool - { - const int v0 = verts[0]; - const int v1 = verts[1]; - const int v2 = verts[2]; - std::vector c01; - std::vector c12; - std::vector c02; - - const T s01 = w[0] + w[1]; - const T s12 = w[1] + w[2]; - const T s02 = w[0] + w[2]; - if (s01 <= eps || s12 <= eps || s02 <= eps - || !eval_curved_edge(v0, v1, w[1] / s01, c01) - || !eval_curved_edge(v1, v2, w[2] / s12, c12) - || !eval_curved_edge(v0, v2, w[2] / s02, c02)) - { - return append_straight_face_interior_seed( - std::span(w.data(), w.size())); - } - - std::array seed = {T(0), T(0), T(0)}; - add_scaled(seed, s01, std::span(c01.data(), c01.size())); - add_scaled(seed, s12, std::span(c12.data(), c12.size())); - add_scaled(seed, s02, std::span(c02.data(), c02.size())); - sub_scaled_vertex(seed, w[0], v0); - sub_scaled_vertex(seed, w[1], v1); - sub_scaled_vertex(seed, w[2], v2); - return append_transfinite_or_straight_seed( - seed, std::span(w.data(), w.size())); - }; - - auto append_quad_interior_node = [&](T u, T v) -> bool - { - std::array w = {(T(1) - u) * (T(1) - v), - u * (T(1) - v), - (T(1) - u) * v, - u * v}; - std::vector bottom; - std::vector top; - std::vector left; - std::vector right; - if (!eval_curved_edge(verts[0], verts[1], u, bottom) - || !eval_curved_edge(verts[2], verts[3], u, top) - || !eval_curved_edge(verts[0], verts[2], v, left) - || !eval_curved_edge(verts[1], verts[3], v, right)) - { - return append_straight_face_interior_seed( - std::span(w.data(), w.size())); - } - - std::array seed = {T(0), T(0), T(0)}; - add_scaled(seed, T(1) - v, std::span(bottom.data(), bottom.size())); - add_scaled(seed, v, std::span(top.data(), top.size())); - add_scaled(seed, T(1) - u, std::span(left.data(), left.size())); - add_scaled(seed, u, std::span(right.data(), right.size())); - sub_scaled_vertex(seed, w[0], verts[0]); - sub_scaled_vertex(seed, w[1], verts[1]); - sub_scaled_vertex(seed, w[2], verts[2]); - sub_scaled_vertex(seed, w[3], verts[3]); - return append_transfinite_or_straight_seed( - seed, std::span(w.data(), w.size())); - }; - - auto append_triangle_node = [&](std::array w) -> bool - { - int zero_count = 0; - int one_index = -1; - int edge_a = -1; - int edge_b = -1; - for (int k = 0; k < 3; ++k) - { - if (std::abs(w[static_cast(k)]) <= eps) - ++zero_count; - if (std::abs(w[static_cast(k)] - T(1)) <= eps) - one_index = k; - else if (w[static_cast(k)] > eps) - { - if (edge_a < 0) - edge_a = k; - else - edge_b = k; - } - } - - if (one_index >= 0) - { - append_vertex(verts[static_cast(one_index)]); - return true; - } - if (zero_count == 1 && edge_a >= 0 && edge_b >= 0) - { - const int v0 = verts[static_cast(edge_a)]; - const int v1 = verts[static_cast(edge_b)]; - const T denom = w[static_cast(edge_a)] - + w[static_cast(edge_b)]; - const T t = w[static_cast(edge_b)] / denom; - const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); - if (edge == nullptr) - { - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats, ac.tdim); - return false; - } - if (!edge->use_curved_state) - { - append_straight_edge_node(v0, v1, t); - return true; - } - const auto x = eval_edge_state_at(*edge, options, v0, v1, t); - state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); - return true; - } - return append_triangle_interior_node(w); - }; - - if (entity_type == cell::type::triangle) - { - const auto nodes = - triangle_interpolation_barycentric_nodes(order, options.node_family); - for (const auto& w : nodes) - { - if (!append_triangle_node(w)) - { - state.failure_reason = "missing or failed hierarchical curved face boundary edge"; - return false; - } - } - return true; - } - - if (entity_type == cell::type::quadrilateral) - { - const auto params = interpolation_parameters(order, options.node_family); - for (const T v : params) - { - for (const T u : params) - { - const bool on_u0 = std::abs(u) <= eps; - const bool on_u1 = std::abs(u - T(1)) <= eps; - const bool on_v0 = std::abs(v) <= eps; - const bool on_v1 = std::abs(v - T(1)) <= eps; - - if ((on_u0 || on_u1) && (on_v0 || on_v1)) - { - int vertex = -1; - if (on_u0 && on_v0) - { - vertex = verts[0]; - } - else if (on_u1 && on_v0) - { - vertex = verts[1]; - } - else if (on_u0 && on_v1) - { - vertex = verts[2]; - } - else - { - vertex = verts[3]; - } - append_vertex(vertex); - continue; - } - - if (on_v0 || on_v1 || on_u0 || on_u1) - { - int v0 = -1; - int v1 = -1; - T t = T(0); - if (on_v0) - { - v0 = verts[0]; v1 = verts[1]; t = u; - } - else if (on_v1) - { - v0 = verts[2]; v1 = verts[3]; t = u; - } - else if (on_u0) - { - v0 = verts[0]; v1 = verts[2]; t = v; - } - else - { - v0 = verts[1]; v1 = verts[3]; t = v; - } - const auto* edge = find_boundary_edge_state(boundary_edges, v0, v1); - if (edge == nullptr) - { - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats, ac.tdim); - state.failure_reason = "missing or failed hierarchical curved face boundary edge"; - return false; - } - if (!edge->use_curved_state) - { - append_straight_edge_node(v0, v1, t); - continue; - } - const auto x = eval_edge_state_at(*edge, options, v0, v1, t); - state.ref_nodes.insert(state.ref_nodes.end(), x.begin(), x.end()); - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::boundary_from_edge), ac.tdim); - continue; - } - - if (!append_quad_interior_node(u, v)) - { - state.failure_reason = "projection failed inside zero-entity host domain"; - return false; - } - } - } - return true; - } - - state.failure_reason = "curving: unsupported zero-face type"; - return false; -} - -template -void build_curved_state(CurvedZeroEntityState& state, - const AdaptCell& ac, - std::span> level_set_cells, - std::span ls_offsets, - int cut_cell_id, - int local_zero_entity_id, - std::span> boundary_edges, - const CurvingOptions& options) -{ - state.status = CurvingStatus::in_progress; - state.failure_reason.clear(); - state.geometry_order = options.geometry_order; - state.node_family = options.node_family; - state.direction_mode = options.direction_mode; - state.small_entity_tol = options.small_entity_tol; - state.zero_entity_version = ac.zero_entity_version; - state.zero_mask = ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; - state.ref_nodes.clear(); - state.node_iterations.clear(); - state.node_status.clear(); - state.node_failure_code.clear(); - state.node_residual.clear(); - state.node_active_face_mask.clear(); - state.node_closest_face_id.clear(); - state.node_safe_subspace_dim.clear(); - state.node_projection_mode.clear(); - state.node_retry_count.clear(); - state.node_seed.clear(); - state.node_direction.clear(); - state.node_clip_lo.clear(); - state.node_clip_hi.clear(); - state.node_root_t.clear(); - - std::vector seeds; - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim == 0) - { - state.ref_nodes = zero_entity_vertices(ac, local_zero_entity_id); - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::exact_vertex), ac.tdim); - state.status = CurvingStatus::curved; - return; - } - if (zdim == 1) - append_edge_seed_nodes(ac, local_zero_entity_id, options, seeds); - else if (zdim == 2) - { - // Face states are built hierarchically below after active constraints - // are available: vertices are kept exact, boundary nodes come from - // already curved zero edges, and only strict face-interior nodes are - // projected. - } - else - { - state.status = CurvingStatus::failed; - state.failure_reason = "unsupported zero-entity dimension"; - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::unsupported_entity; - append_node_stats(state, stats, ac.tdim); - return; - } - - if (zero_entity_below_curving_threshold(ac, local_zero_entity_id, options)) - { - if (zdim == 2) - append_face_seed_nodes(ac, local_zero_entity_id, options, seeds); - state.ref_nodes = seeds; - append_straight_seed_node_stats( - state, - ac, - std::span(state.ref_nodes.data(), state.ref_nodes.size())); - state.status = CurvingStatus::curved; - return; - } - - const std::uint64_t effective_zero_mask = state.zero_mask & ac.active_level_set_mask; - const auto ls_ids = active_level_sets(effective_zero_mask); - if (ls_ids.empty()) - { - if (zdim == 2) - append_face_seed_nodes(ac, local_zero_entity_id, options, seeds); - state.ref_nodes = seeds; - const int n_nodes = static_cast( - seeds.size() / static_cast(std::max(ac.tdim, 1))); - for (int i = 0; i < n_nodes; ++i) - append_node_stats( - state, accepted_node_stats(CurvingFailureCode::none), ac.tdim); - state.status = CurvingStatus::curved; - return; - } - - std::vector*> active_cells; - active_cells.reserve(ls_ids.size()); - try - { - for (const int ls_id : ls_ids) - active_cells.push_back(&find_level_set_cell( - level_set_cells, ls_offsets, cut_cell_id, ls_id)); - } - catch (const std::exception& e) - { - state.status = CurvingStatus::failed; - state.failure_reason = e.what(); - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::missing_level_set_cell; - append_node_stats(state, stats, ac.tdim); - return; - } - - if (zdim == 2) - { - if (!build_hierarchical_face_nodes( - state, - ac, - local_zero_entity_id, - std::span* const>( - active_cells.data(), active_cells.size()), - boundary_edges, - options)) - { - state.status = CurvingStatus::failed; - if (state.failure_reason.empty()) - state.failure_reason = "hierarchical zero-face curving failed"; - state.ref_nodes.clear(); - return; - } - state.status = CurvingStatus::curved; - return; - } - - const int nseeds = static_cast(seeds.size() / static_cast(ac.tdim)); - state.ref_nodes.reserve(seeds.size()); - std::vector projected; - for (int i = 0; i < nseeds; ++i) - { - std::span seed( - seeds.data() + static_cast(i * ac.tdim), - static_cast(ac.tdim)); - - if (zdim == 1 && (i == 0 || i == nseeds - 1)) - { - state.ref_nodes.insert(state.ref_nodes.end(), seed.begin(), seed.end()); - append_node_stats( - state, - accepted_node_stats(CurvingFailureCode::exact_vertex), - ac.tdim); - continue; - } - - ProjectionStats stats; - const bool ok = project_seed_to_zero_entity( - ac, - local_zero_entity_id, - std::span* const>( - active_cells.data(), active_cells.size()), - seed, - options, - projected, - stats); - append_node_stats(state, stats, ac.tdim); - - if (!ok) - { - state.status = CurvingStatus::failed; - state.failure_reason = "projection failed inside zero-entity host domain"; - state.ref_nodes.clear(); - return; - } - state.ref_nodes.insert(state.ref_nodes.end(), projected.begin(), projected.end()); - } - - state.status = CurvingStatus::curved; -} - -} // namespace - -NodeFamily node_family_from_string(std::string_view name) -{ - if (name == "gll" || name == "lobatto" - || name == "fekete" || name == "warp_blend" || name == "warp-blend") - return NodeFamily::gll; - if (name == "equispaced") - return NodeFamily::equispaced; - if (name == "lagrange") - return NodeFamily::lagrange; - throw std::invalid_argument("unknown curving node family"); -} - -std::string_view node_family_name(NodeFamily family) -{ - switch (family) - { - case NodeFamily::gll: return "gll"; - case NodeFamily::equispaced: return "equispaced"; - case NodeFamily::lagrange: return "lagrange"; - } - return "unknown"; -} - -CurvingDirectionMode direction_mode_from_string(std::string_view name) -{ - if (name == "straight_zero_entity_normal" - || name == "straight-zero-entity-normal" - || name == "zero_entity_normal" - || name == "zero-entity-normal" - || name == "straight_normal" - || name == "straight-normal" - || name == "normal") - { - return CurvingDirectionMode::straight_zero_entity_normal; - } - if (name == "level_set_gradient" - || name == "level-set-gradient" - || name == "gradient") - { - return CurvingDirectionMode::level_set_gradient; - } - throw std::invalid_argument("unknown curving direction mode"); -} - -std::string_view direction_mode_name(CurvingDirectionMode mode) -{ - switch (mode) - { - case CurvingDirectionMode::straight_zero_entity_normal: - return "straight_zero_entity_normal"; - case CurvingDirectionMode::level_set_gradient: - return "level_set_gradient"; - } - return "unknown"; -} - -template -T reference_level_set_value(const LevelSetCell& ls_cell, - std::span ref) -{ - return curving_level_set_value(ls_cell, ref); -} - -template -void reference_level_set_gradient(const LevelSetCell& ls_cell, - std::span ref, - std::span grad_ref) -{ - curving_level_set_grad(ls_cell, ref, grad_ref); -} - -template -ProjectionDiagnostic make_projection_diagnostic( - bool accepted, - const std::vector& projected, - const ProjectionStats& stats) -{ - ProjectionDiagnostic diagnostic; - diagnostic.accepted = accepted; - diagnostic.status = stats.status; - diagnostic.failure_code = stats.failure_code; - diagnostic.projection_mode = stats.projection_mode; - diagnostic.iterations = stats.iterations; - diagnostic.residual = stats.residual; - diagnostic.active_face_mask = stats.active_face_mask; - diagnostic.closest_face_id = stats.closest_face_id; - diagnostic.safe_subspace_dim = stats.safe_subspace_dim; - diagnostic.retry_count = stats.retry_count; - diagnostic.projected = projected; - diagnostic.seed = stats.seed; - diagnostic.direction = stats.direction; - diagnostic.clip_lo = stats.clip_lo; - diagnostic.clip_hi = stats.clip_hi; - diagnostic.root_t = stats.root_t; - return diagnostic; -} - -template -ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell& ac, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span seed, - const CurvingOptions& options) -{ - std::array*, 1> active_cells = {&ls_cell}; - std::vector projected; - ProjectionStats stats; - bool accepted = false; - try - { - accepted = project_seed_to_zero_entity( - ac, - local_zero_entity_id, - std::span* const>( - active_cells.data(), active_cells.size()), - seed, - options, - projected, - stats); - } - catch (const std::exception&) - { - accepted = false; - if (stats.failure_code == CurvingFailureCode::none) - stats.failure_code = CurvingFailureCode::projection_failed; - stats.status = CurvingStatus::failed; - } - - if (accepted && stats.direction.empty()) - { - stats.direction = normalized_delta_direction( - seed, - std::span(projected.data(), projected.size()), - options.xtol); - } - if (stats.direction.empty()) - { - stats.direction = preferred_scalar_projection_direction( - ac, local_zero_entity_id, ls_cell, seed, options); - } - - return make_projection_diagnostic(accepted, projected, stats); -} - -template -void rebuild_identity(CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells) -{ - curving.clear(); - curving.num_cut_cells = static_cast(adapt_cells.size()); - curving.local_to_canonical_offsets.reserve(adapt_cells.size() + 1); - curving.local_to_canonical_offsets.push_back(0); - - for (int c = 0; c < static_cast(adapt_cells.size()); ++c) - { - const auto& ac = adapt_cells[static_cast(c)]; - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - CurvingIdentity id; - id.cut_cell_id = c; - id.local_zero_entity_id = z; - id.dim = ac.zero_entity_dim[static_cast(z)]; - id.parent_dim = ac.zero_entity_parent_dim[static_cast(z)]; - id.parent_id = ac.zero_entity_parent_id[static_cast(z)]; - if (id.parent_dim == ac.tdim && id.parent_id < 0) - id.parent_id = static_cast(parent_cell_ids[static_cast(c)]); - id.zero_mask = ac.zero_entity_zero_mask[static_cast(z)]; - curving.identities.push_back(id); - curving.states.emplace_back(); - curving.local_to_canonical.push_back( - static_cast(curving.identities.size()) - 1); - } - curving.local_to_canonical_offsets.push_back( - static_cast(curving.local_to_canonical.size())); - } - - curving.identity_valid = true; -} - -template -int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, - int zero_face_id, - int zero_edge_id, - T tol); - -template -std::vector face_boundary_zero_edges(const AdaptCell& ac, - int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - if (zdim != 2) - return {}; - - const std::uint64_t face_mask = - ac.zero_entity_zero_mask[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - const auto face_type = ac.entity_types[2][static_cast(zid)]; - auto face_verts_span = ac.entity_to_vertex[2][static_cast(zid)]; - std::vector face_verts; - face_verts.reserve(face_verts_span.size()); - for (const auto v : face_verts_span) - face_verts.push_back(static_cast(v)); - - std::vector edge_ids; - for (const auto& edge : cell::edges(face_type)) - { - std::array target = { - face_verts[static_cast(edge[0])], - face_verts[static_cast(edge[1])] - }; - int match = -1; - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (ac.zero_entity_dim[static_cast(z)] != 1) - continue; - const auto edge_mask = ac.zero_entity_zero_mask[static_cast(z)]; - if ((edge_mask & face_mask) != face_mask) - continue; - const auto edge_verts = zero_entity_vertex_ids(ac, z); - if (same_unordered_vertices( - std::span(target.data(), target.size()), - std::span(edge_verts.data(), edge_verts.size()))) - { - match = z; - break; - } - } - if (match < 0) - return {}; - const int host_face_id = - host_face_for_zero_face_boundary_edge( - ac, - local_zero_entity_id, - match, - T(256) * std::numeric_limits::epsilon()); - // Triangulated zero quadrilaterals introduce an artificial diagonal. - // It is a zero subedge of the output triangle, but it is not on a - // host-cell boundary face. Keep it as a curved zero edge; a negative - // host_face_id makes its contextual host the uncut subcell volume. - edge_ids.push_back({match, host_face_id, true}); - } - return edge_ids; -} - -template -bool point_in_triangle_ref(const AdaptCell& ac, - std::span tri_vertices, - std::span point, - T tol) -{ - if (ac.tdim != 3 || tri_vertices.size() != 3 || point.size() != 3) - return false; - const auto a = vertex_ref_point(ac, tri_vertices[0]); - const auto b = vertex_ref_point(ac, tri_vertices[1]); - const auto c = vertex_ref_point(ac, tri_vertices[2]); - std::array v0 = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}; - std::array v1 = {c[0] - a[0], c[1] - a[1], c[2] - a[2]}; - std::array v2 = {point[0] - a[0], point[1] - a[1], point[2] - a[2]}; - const auto n = local_cross( - std::span(v0.data(), 3), - std::span(v1.data(), 3)); - const T area2 = local_norm(std::span(n.data(), 3)); - if (area2 <= tol) - return false; - const T plane = - std::fabs(local_dot( - std::span(v2.data(), 3), - std::span(n.data(), 3))) / area2; - if (plane > tol) - return false; - - const T d00 = local_dot( - std::span(v0.data(), 3), - std::span(v0.data(), 3)); - const T d01 = local_dot( - std::span(v0.data(), 3), - std::span(v1.data(), 3)); - const T d11 = local_dot( - std::span(v1.data(), 3), - std::span(v1.data(), 3)); - const T d20 = local_dot( - std::span(v2.data(), 3), - std::span(v0.data(), 3)); - const T d21 = local_dot( - std::span(v2.data(), 3), - std::span(v1.data(), 3)); - const T denom = d00 * d11 - d01 * d01; - if (std::fabs(denom) <= tol * tol) - return false; - const T v = (d11 * d20 - d01 * d21) / denom; - const T w = (d00 * d21 - d01 * d20) / denom; - const T u = T(1) - v - w; - return u >= -tol && v >= -tol && w >= -tol - && u <= T(1) + tol && v <= T(1) + tol && w <= T(1) + tol; -} - -template -bool point_in_face_ref(const AdaptCell& ac, - std::span face_vertices, - std::span point, - T tol) -{ - if (face_vertices.size() == 3) - return point_in_triangle_ref(ac, face_vertices, point, tol); - if (face_vertices.size() != 4) - return false; - - const std::array tri0 = { - face_vertices[0], face_vertices[1], face_vertices[3]}; - const std::array tri1 = { - face_vertices[0], face_vertices[3], face_vertices[2]}; - return point_in_triangle_ref( - ac, std::span(tri0.data(), tri0.size()), point, tol) - || point_in_triangle_ref( - ac, std::span(tri1.data(), tri1.size()), point, tol); -} - -template -int host_face_for_zero_face_boundary_edge(const AdaptCell& ac, - int zero_face_id, - int zero_edge_id, - T tol) -{ - if (zero_face_id < 0 || zero_edge_id < 0 - || zero_face_id >= ac.n_zero_entities() - || zero_edge_id >= ac.n_zero_entities()) - { - return -1; - } - if (zero_face_id >= static_cast(ac.zero_entity_host_cell_type.size())) - return -1; - const cell::type host_type = - ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; - const auto host_cell_vertices = - zero_entity_host_cell_vertex_ids(ac, zero_face_id); - const auto edge_vertices = - local_zero_entity_vertex_ids(ac, zero_edge_id); - if (host_cell_vertices.empty() || edge_vertices.size() != 2 - || host_type == cell::type::point) - { - return -1; - } - - const auto p0 = vertex_ref_point(ac, edge_vertices[0]); - const auto p1 = vertex_ref_point(ac, edge_vertices[1]); - for (int f = 0; f < cell::num_faces(host_type); ++f) - { - auto local_face = cell::face_vertices(host_type, f); - if (local_face.size() != 3 && local_face.size() != 4) - continue; - std::array face_vertices = {-1, -1, -1, -1}; - for (std::size_t i = 0; i < local_face.size(); ++i) - face_vertices[i] = - host_cell_vertices[static_cast(local_face[i])]; - - const std::span face_span( - face_vertices.data(), local_face.size()); - if (point_in_face_ref(ac, face_span, p0, tol) - && point_in_face_ref(ac, face_span, p1, tol)) - { - return f; - } - } - return -1; -} - -template -void override_zero_edge_host_from_face(AdaptCell& ac, - int zero_edge_id, - int zero_face_id, - int host_face_id) -{ - if (zero_edge_id < 0 || zero_face_id < 0 - || zero_edge_id >= ac.n_zero_entities() - || zero_face_id >= ac.n_zero_entities()) - { - return; - } - if (zero_face_id >= static_cast(ac.zero_entity_host_cell_id.size()) - || zero_edge_id >= static_cast(ac.zero_entity_host_cell_id.size())) - { - return; - } - const auto face_host_vertices = - zero_entity_host_cell_vertex_ids(ac, zero_face_id); - if (face_host_vertices.empty()) - return; - - ac.zero_entity_host_cell_id[static_cast(zero_edge_id)] = - ac.zero_entity_host_cell_id[static_cast(zero_face_id)]; - ac.zero_entity_host_cell_type[static_cast(zero_edge_id)] = - ac.zero_entity_host_cell_type[static_cast(zero_face_id)]; - ac.zero_entity_host_face_id[static_cast(zero_edge_id)] = - static_cast(host_face_id); - ac.zero_entity_source_level_set[static_cast(zero_edge_id)] = - ac.zero_entity_source_level_set[static_cast(zero_face_id)]; - - EntityAdjacency updated; - updated.offsets.push_back(std::int32_t(0)); - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (z == zero_edge_id) - { - for (const int v : face_host_vertices) - updated.indices.push_back(static_cast(v)); - } - else if (z < ac.zero_entity_host_cell_vertices.size()) - { - const auto old = ac.zero_entity_host_cell_vertices[static_cast(z)]; - for (const auto v : old) - updated.indices.push_back(v); - } - updated.offsets.push_back( - static_cast(updated.indices.size())); - } - ac.zero_entity_host_cell_vertices = std::move(updated); -} - -template -const CurvedZeroEntityState& ensure_curved( - CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells, - std::span> level_set_cells, - std::span ls_offsets, - int cut_cell_id, - int local_zero_entity_id, - const CurvingOptions& options) -{ - if (!curving.identity_valid - || curving.num_cut_cells != static_cast(adapt_cells.size())) - { - rebuild_identity(curving, parent_cell_ids, adapt_cells); - } - if (cut_cell_id < 0 || cut_cell_id >= static_cast(adapt_cells.size())) - throw std::out_of_range("curving: cut_cell_id out of range"); - - const int begin = curving.local_to_canonical_offsets[static_cast(cut_cell_id)]; - const int end = curving.local_to_canonical_offsets[static_cast(cut_cell_id + 1)]; - if (local_zero_entity_id < 0 || begin + local_zero_entity_id >= end) - throw std::out_of_range("curving: local_zero_entity_id out of range"); - - const int canonical = curving.local_to_canonical[static_cast(begin + local_zero_entity_id)]; - auto& state = curving.states[static_cast(canonical)]; - const auto& ac = adapt_cells[static_cast(cut_cell_id)]; - const bool valid = - state.status == CurvingStatus::curved - && state.geometry_order == options.geometry_order - && state.node_family == options.node_family - && state.direction_mode == options.direction_mode - && state.small_entity_tol == options.small_entity_tol - && state.zero_entity_version == ac.zero_entity_version; - const bool failed = - state.status == CurvingStatus::failed - && state.geometry_order == options.geometry_order - && state.node_family == options.node_family - && state.direction_mode == options.direction_mode - && state.small_entity_tol == options.small_entity_tol - && state.zero_entity_version == ac.zero_entity_version; - if (!valid && !failed) - { - std::vector> boundary_edges; - std::vector> contextual_boundary_edge_states; - if (ac.zero_entity_dim[static_cast(local_zero_entity_id)] == 2 - && !zero_entity_below_curving_threshold(ac, local_zero_entity_id, options)) - { - const auto boundary_edge_refs = - face_boundary_zero_edges(ac, local_zero_entity_id); - if (boundary_edge_refs.empty()) - { - state.status = CurvingStatus::failed; - state.failure_reason = "hierarchical zero-face curving requires boundary zero edges"; - state.geometry_order = options.geometry_order; - state.node_family = options.node_family; - state.direction_mode = options.direction_mode; - state.small_entity_tol = options.small_entity_tol; - state.zero_entity_version = ac.zero_entity_version; - state.zero_mask = ac.zero_entity_zero_mask[ - static_cast(local_zero_entity_id)]; - state.ref_nodes.clear(); - state.node_iterations.clear(); - state.node_status.clear(); - state.node_failure_code.clear(); - state.node_residual.clear(); - state.node_active_face_mask.clear(); - state.node_closest_face_id.clear(); - state.node_safe_subspace_dim.clear(); - state.node_projection_mode.clear(); - state.node_retry_count.clear(); - state.node_seed.clear(); - state.node_direction.clear(); - state.node_clip_lo.clear(); - state.node_clip_hi.clear(); - state.node_root_t.clear(); - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::missing_boundary_edge; - append_node_stats(state, stats, ac.tdim); - return state; - } - - boundary_edges.reserve(boundary_edge_refs.size()); - contextual_boundary_edge_states.reserve(boundary_edge_refs.size()); - for (const auto& edge_ref : boundary_edge_refs) - { - const CurvedZeroEntityState* edge_state = nullptr; - if (edge_ref.use_curved_state) - { - AdaptCell edge_context_ac = ac; - override_zero_edge_host_from_face( - edge_context_ac, - edge_ref.local_zero_entity_id, - local_zero_entity_id, - edge_ref.host_face_id); - contextual_boundary_edge_states.emplace_back(); - auto& mutable_edge_state = - contextual_boundary_edge_states.back(); - build_curved_state( - mutable_edge_state, - edge_context_ac, - level_set_cells, - ls_offsets, - cut_cell_id, - edge_ref.local_zero_entity_id, - std::span>(), - options); - edge_state = &mutable_edge_state; - if (edge_state->status != CurvingStatus::curved) - { - state.status = CurvingStatus::failed; - state.failure_reason = "hierarchical zero-face boundary edge curving failed"; - state.geometry_order = options.geometry_order; - state.node_family = options.node_family; - state.direction_mode = options.direction_mode; - state.small_entity_tol = options.small_entity_tol; - state.zero_entity_version = ac.zero_entity_version; - state.zero_mask = ac.zero_entity_zero_mask[ - static_cast(local_zero_entity_id)]; - state.ref_nodes.clear(); - state.node_iterations.clear(); - state.node_status.clear(); - state.node_failure_code.clear(); - state.node_residual.clear(); - state.node_active_face_mask.clear(); - state.node_closest_face_id.clear(); - state.node_safe_subspace_dim.clear(); - state.node_projection_mode.clear(); - state.node_retry_count.clear(); - state.node_seed.clear(); - state.node_direction.clear(); - state.node_clip_lo.clear(); - state.node_clip_hi.clear(); - state.node_root_t.clear(); - ProjectionStats stats; - stats.failure_code = CurvingFailureCode::boundary_edge_failed; - append_node_stats(state, stats, ac.tdim); - return state; - } - } - const auto edge_verts = - zero_entity_vertex_ids(ac, edge_ref.local_zero_entity_id); - BoundaryEdgeState boundary; - boundary.vertices = {edge_verts[0], edge_verts[1]}; - boundary.state = edge_state; - boundary.use_curved_state = edge_ref.use_curved_state; - boundary_edges.push_back(boundary); - } - } - - build_curved_state( - state, ac, level_set_cells, ls_offsets, - cut_cell_id, local_zero_entity_id, - std::span>( - boundary_edges.data(), boundary_edges.size()), - options); - } - return state; -} - -template -void ensure_all_curved(CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells, - std::span> level_set_cells, - std::span ls_offsets, - const CurvingOptions& options) -{ - if (!curving.identity_valid - || curving.num_cut_cells != static_cast(adapt_cells.size())) - { - rebuild_identity(curving, parent_cell_ids, adapt_cells); - } - for (int c = 0; c < static_cast(adapt_cells.size()); ++c) - { - for (int z = 0; z < adapt_cells[static_cast(c)].n_zero_entities(); ++z) - (void)ensure_curved( - curving, parent_cell_ids, adapt_cells, level_set_cells, ls_offsets, - c, z, options); - } -} - -template double reference_level_set_value( - const LevelSetCell&, std::span); -template float reference_level_set_value( - const LevelSetCell&, std::span); -template double reference_level_set_value( - const LevelSetCell&, std::span); -template float reference_level_set_value( - const LevelSetCell&, std::span); - -template void reference_level_set_gradient( - const LevelSetCell&, std::span, std::span); -template void reference_level_set_gradient( - const LevelSetCell&, std::span, std::span); -template void reference_level_set_gradient( - const LevelSetCell&, std::span, std::span); -template void reference_level_set_gradient( - const LevelSetCell&, std::span, std::span); - -template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell&, int, const LevelSetCell&, - std::span, const CurvingOptions&); -template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell&, int, const LevelSetCell&, - std::span, const CurvingOptions&); -template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell&, int, const LevelSetCell&, - std::span, const CurvingOptions&); -template ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell&, int, const LevelSetCell&, - std::span, const CurvingOptions&); - -template void rebuild_identity(CurvingData&, std::span, std::span>); -template void rebuild_identity(CurvingData&, std::span, std::span>); -template void rebuild_identity(CurvingData&, std::span, std::span>); -template void rebuild_identity(CurvingData&, std::span, std::span>); - -template const CurvedZeroEntityState& ensure_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, int, int, - const CurvingOptions&); -template const CurvedZeroEntityState& ensure_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, int, int, - const CurvingOptions&); -template const CurvedZeroEntityState& ensure_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, int, int, - const CurvingOptions&); -template const CurvedZeroEntityState& ensure_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, int, int, - const CurvingOptions&); - -template void ensure_all_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, - const CurvingOptions&); -template void ensure_all_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, - const CurvingOptions&); -template void ensure_all_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, - const CurvingOptions&); -template void ensure_all_curved( - CurvingData&, std::span, std::span>, - std::span>, std::span, - const CurvingOptions&); - -} // namespace cutcells::curving diff --git a/cpp/src/curving.h b/cpp/src/curving.h deleted file mode 100644 index ddae978..0000000 --- a/cpp/src/curving.h +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) 2026 ONERA -// Authors: Susanne Claus -// This file is part of CutCells -// SPDX-License-Identifier: MIT - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "adapt_cell.h" -#include "level_set_cell.h" - -namespace cutcells::curving -{ - -enum class NodeFamily : std::uint8_t -{ - gll = 0, - equispaced = 1, - lagrange = 2 -}; - -enum class CurvingStatus : std::uint8_t -{ - not_built = 0, - in_progress = 1, - curved = 2, - failed = 3 -}; - -enum class CurvingFailureCode : std::uint8_t -{ - none = 0, - exact_vertex = 1, - boundary_from_edge = 2, - invalid_constraint_count = 3, - missing_level_set_cell = 4, - empty_zero_mask = 5, - unsupported_entity = 6, - no_host_interval = 7, - no_sign_changing_bracket = 8, - brent_failed = 9, - outside_host_domain = 10, - singular_gradient_system = 11, - line_search_failed = 12, - max_iterations = 13, - missing_boundary_edge = 14, - boundary_edge_failed = 15, - projection_failed = 16, - closest_face_retry_failed = 17, - constrained_newton_failed = 18, - small_entity_kept_straight = 19 -}; - -enum class CurvingProjectionMode : std::uint8_t -{ - none = 0, - safe_line = 1, - closest_face_retry = 2, - constrained_newton = 3, - vector_newton = 4 -}; - -enum class CurvingDirectionMode : std::uint8_t -{ - straight_zero_entity_normal = 0, - level_set_gradient = 1 -}; - -template -struct CurvingOptions -{ - int geometry_order = 2; - NodeFamily node_family = NodeFamily::gll; - CurvingDirectionMode direction_mode = - CurvingDirectionMode::level_set_gradient; - int max_iter = 32; - T xtol = T(1e-12); - T ftol = T(64) * std::numeric_limits::epsilon(); - T domain_tol = T(1e-10); - T active_face_tol = T(1e-9); - // Length-scale threshold in parent reference coordinates. Zero edges - // shorter than this and zero faces whose boundary edges are all shorter - // than this are kept on their straight interpolation seeds instead of - // being projected. - T small_entity_tol = std::sqrt(std::numeric_limits::epsilon()); - int max_subdivision_depth = 3; -}; - -template -struct CurvedZeroEntityState -{ - CurvingStatus status = CurvingStatus::not_built; - int geometry_order = -1; - NodeFamily node_family = NodeFamily::gll; - CurvingDirectionMode direction_mode = - CurvingDirectionMode::level_set_gradient; - T small_entity_tol = T(0); - std::uint32_t zero_entity_version = 0; - std::uint64_t zero_mask = 0; - std::string failure_reason; - - // Full curved interpolation nodes, including endpoints/boundary nodes. - // Coordinates are in the parent background cell reference frame. - std::vector ref_nodes; - - // Per intended interpolation node, including nodes that failed before the - // entity as a whole was accepted. Successful vertices/boundary nodes have - // zero iterations and a non-error failure code documenting their origin. - std::vector node_iterations; - std::vector node_status; - std::vector node_failure_code; - std::vector node_residual; - std::vector node_active_face_mask; - std::vector node_closest_face_id; - std::vector node_safe_subspace_dim; - std::vector node_projection_mode; - std::vector node_retry_count; - std::vector node_seed; - std::vector node_direction; - std::vector node_clip_lo; - std::vector node_clip_hi; - std::vector node_root_t; -}; - -struct CurvingIdentity -{ - int cut_cell_id = -1; - int local_zero_entity_id = -1; - int dim = -1; - std::int8_t parent_dim = -1; - std::int32_t parent_id = -1; - std::uint64_t zero_mask = 0; -}; - -template -struct ProjectionDiagnostic -{ - bool accepted = false; - CurvingStatus status = CurvingStatus::failed; - CurvingFailureCode failure_code = CurvingFailureCode::projection_failed; - CurvingProjectionMode projection_mode = CurvingProjectionMode::none; - int iterations = 0; - T residual = std::numeric_limits::infinity(); - std::uint32_t active_face_mask = 0; - int closest_face_id = -1; - int safe_subspace_dim = -1; - int retry_count = 0; - std::vector projected; - std::vector seed; - std::vector direction; - T clip_lo = std::numeric_limits::quiet_NaN(); - T clip_hi = std::numeric_limits::quiet_NaN(); - T root_t = std::numeric_limits::quiet_NaN(); -}; - -template -struct CurvingData -{ - std::vector identities; - std::vector> states; - - // CSR-like local lookup: local_to_canonical_offsets[k]..[k+1] belongs to - // cut cell k and stores canonical ids for local zero entity ids. - std::vector local_to_canonical_offsets; - std::vector local_to_canonical; - - int num_cut_cells = 0; - bool identity_valid = false; - - void clear() - { - identities.clear(); - states.clear(); - local_to_canonical_offsets.clear(); - local_to_canonical.clear(); - num_cut_cells = 0; - identity_valid = false; - } -}; - -NodeFamily node_family_from_string(std::string_view name); -std::string_view node_family_name(NodeFamily family); -CurvingDirectionMode direction_mode_from_string(std::string_view name); -std::string_view direction_mode_name(CurvingDirectionMode mode); - -template -T reference_level_set_value(const LevelSetCell& ls_cell, - std::span ref); - -template -void reference_level_set_gradient(const LevelSetCell& ls_cell, - std::span ref, - std::span grad_ref); - -template -ProjectionDiagnostic project_seed_to_zero_entity_diagnostic( - const AdaptCell& ac, - int local_zero_entity_id, - const LevelSetCell& ls_cell, - std::span seed, - const CurvingOptions& options); - -template -void rebuild_identity(CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells); - -template -const CurvedZeroEntityState& ensure_curved( - CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells, - std::span> level_set_cells, - std::span ls_offsets, - int cut_cell_id, - int local_zero_entity_id, - const CurvingOptions& options); - -template -void ensure_all_curved(CurvingData& curving, - std::span parent_cell_ids, - std::span> adapt_cells, - std::span> level_set_cells, - std::span ls_offsets, - const CurvingOptions& options); - -} // namespace cutcells::curving diff --git a/cpp/src/edge_certification.cpp b/cpp/src/edge_certification.cpp index 03486f5..0389539 100644 --- a/cpp/src/edge_certification.cpp +++ b/cpp/src/edge_certification.cpp @@ -329,6 +329,41 @@ void merge_root_intervals(std::vector>& intervals, intervals = std::move(merged); } +template +bool linear_one_root_parameter_from_endpoint_values(T phi0, + T phi1, + T zero_tol, + T& root_t) +{ + const bool left_zero = std::fabs(phi0) <= zero_tol; + const bool right_zero = std::fabs(phi1) <= zero_tol; + + root_t = T(0); + if (left_zero && right_zero) + return false; + if (left_zero) + return true; + if (right_zero) + { + root_t = T(1); + return true; + } + + const bool brackets_zero = (phi0 < T(0) && phi1 > T(0)) + || (phi0 > T(0) && phi1 < T(0)); + if (!brackets_zero) + return false; + + const T denom = phi0 - phi1; + const T denom_tol = + std::max(zero_tol, T(64) * std::numeric_limits::epsilon()); + if (std::fabs(denom) <= denom_tol) + return false; + + root_t = std::clamp(phi0 / denom, T(0), T(1)); + return true; +} + } // anonymous namespace // ===================================================================== @@ -843,117 +878,6 @@ EdgeRootTag classify_edge_roots(std::span edge_coeffs, return EdgeRootTag::multiple_roots; } -template -bool locate_one_root_parameter(std::span edge_coeffs, - T zero_tol, T sign_tol, - int max_depth, - T& root_t) -{ - root_t = T(0); - - if (edge_coeffs.empty()) - return false; - - if (bernstein_all_zero(edge_coeffs, zero_tol)) - return false; - - const bool left_zero = std::fabs(edge_coeffs.front()) <= zero_tol; - const bool right_zero = std::fabs(edge_coeffs.back()) <= zero_tol; - - if (left_zero && !right_zero) - { - root_t = T(0); - return true; - } - if (right_zero && !left_zero) - { - root_t = T(1); - return true; - } - - std::vector> intervals; - bool has_zero_segment = false; - find_root_intervals_1d(edge_coeffs, T(0), T(1), - zero_tol, sign_tol, 0, max_depth, - intervals, has_zero_segment); - if (has_zero_segment) - return false; - - T merge_tol = T(4) * std::numeric_limits::epsilon(); - if (max_depth > 0) - { - T width = T(1); - for (int i = 0; i < max_depth; ++i) - width *= T(0.5); - merge_tol = std::max(merge_tol, T(2) * width); - } - merge_root_intervals(intervals, merge_tol); - - std::vector> clusters = intervals; - bool left_covered = false; - bool right_covered = false; - for (const auto& interval : clusters) - { - if (interval.t0 <= merge_tol) - left_covered = true; - if (interval.t1 >= T(1) - merge_tol) - right_covered = true; - } - - if (left_zero && !left_covered) - clusters.push_back({T(0), T(0)}); - if (right_zero && !right_covered) - clusters.push_back({T(1), T(1)}); - - if (clusters.empty()) - return false; - - std::sort(clusters.begin(), clusters.end(), - [](const EdgeRootInterval& a, const EdgeRootInterval& b) - { return a.t0 < b.t0; }); - merge_root_intervals(clusters, merge_tol); - - if (clusters.size() != 1) - return false; - - const T a = clusters[0].t0; - const T b = clusters[0].t1; - root_t = (a + b) * T(0.5); - - if (std::fabs(b - a) <= zero_tol) - return true; - - const int degree = static_cast(edge_coeffs.size()) - 1; - std::array xi = {root_t}; - std::array grad = {T(0)}; - - const T eval_tol = std::max(zero_tol, T(32) * std::numeric_limits::epsilon()); - for (int iter = 0; iter < 12; ++iter) - { - xi[0] = root_t; - const T value = bernstein::evaluate( - cell::type::interval, degree, - edge_coeffs, std::span(xi.data(), 1)); - if (std::fabs(value) <= eval_tol) - break; - - bernstein::gradient( - cell::type::interval, degree, - edge_coeffs, std::span(xi.data(), 1), - std::span(grad.data(), 1)); - if (std::fabs(grad[0]) <= eval_tol) - break; - - const T next = root_t - value / grad[0]; - if (next <= a || next >= b) - break; - root_t = next; - } - - root_t = std::clamp(root_t, a, b); - return true; -} - // ===================================================================== // classify_new_edges // ===================================================================== @@ -989,8 +913,9 @@ void classify_new_edges(AdaptCell& adapt_cell, // Extract edge Bernstein coefficients on the actual adaptive edge segment. // // Even if an adaptive edge lies on a parent edge, using the parent-edge - // fast-path can be incorrect for subsegments (root localization would be - // in the wrong parameterization). Restrict by endpoints for robustness. + // fast-path can be incorrect for subsegments. Restrict by endpoints so + // the endpoint coefficients and green split parameter use this edge's + // local parameterization. auto verts = adapt_cell.entity_to_vertex[1][static_cast(e)]; const int tdim = adapt_cell.tdim; std::span xi_a( @@ -1015,12 +940,27 @@ void classify_new_edges(AdaptCell& adapt_cell, zero_tol, sign_tol, max_depth, green_split_t, has_green); + T linear_root_t = T(0); + const bool has_linear_root = + tag == EdgeRootTag::one_root + && edge_coeffs.size() >= 2 + && linear_one_root_parameter_from_endpoint_values( + edge_coeffs.front(), edge_coeffs.back(), zero_tol, linear_root_t); + if (tag == EdgeRootTag::one_root && !has_linear_root) + tag = EdgeRootTag::no_root; + adapt_cell.set_edge_root_tag(level_set_id, e, tag); const auto idx = static_cast(level_set_id * n_edges + e); adapt_cell.edge_green_split_param[idx] = green_split_t; adapt_cell.edge_green_split_has_value[idx] = has_green ? 1 : 0; - if (tag != EdgeRootTag::one_root) + if (tag == EdgeRootTag::one_root) + { + adapt_cell.edge_one_root_param[idx] = linear_root_t; + adapt_cell.edge_one_root_vertex_id[idx] = -1; + adapt_cell.edge_one_root_has_value[idx] = 1; + } + else { adapt_cell.edge_one_root_param[idx] = T(0); adapt_cell.edge_one_root_vertex_id[idx] = -1; @@ -1077,10 +1017,6 @@ template EdgeRootTag classify_edge_roots(std::span, double, double int, double&, bool&); template EdgeRootTag classify_edge_roots(std::span, float, float, int, float&, bool&); -template bool locate_one_root_parameter(std::span, double, double, - int, double&); -template bool locate_one_root_parameter(std::span, float, float, - int, float&); template void classify_new_edges(AdaptCell&, const LevelSetCell&, int, double, double, int); diff --git a/cpp/src/edge_certification.h b/cpp/src/edge_certification.h index 8994084..68a4ed3 100644 --- a/cpp/src/edge_certification.h +++ b/cpp/src/edge_certification.h @@ -74,6 +74,10 @@ bool bernstein_all_zero(std::span coeffs, T tol); /// Bernstein polynomial, using convex-hull exclusion and de Casteljau /// subdivision. /// +/// This is used only for edge topology classification, not for placing the +/// cut vertex. The committed cut point is placed by linear interpolation of +/// the endpoint level-set values. +/// /// @param coeffs Bernstein coefficients on the sub-interval [t0, t1]. /// @param t0, t1 Current parameter bounds (start with 0, 1). /// @param zero_tol Tolerance for the all-zero test. @@ -150,10 +154,15 @@ bool edge_is_on_single_parent_edge(const AdaptCell& adapt_cell, /// Classify a 1D Bernstein polynomial for root structure. /// +/// This classification may use Bernstein sign-hull subdivision to detect +/// edge topology, but it does not determine the final cut-vertex location. +/// Cut vertices on one-root edges are placed later by linear interpolation +/// of the two endpoint level-set values. +/// /// @param edge_coeffs Bernstein coefficients (degree+1 entries). /// @param zero_tol Tolerance for all-zero. /// @param sign_tol Tolerance for all-positive / all-negative. -/// @param max_depth Maximum subdivision depth for root search. +/// @param max_depth Maximum subdivision depth for topology classification. /// @param[out] green_split_t Parameter between two distinct root intervals /// (valid only if tag == multiple_roots). /// @param[out] has_green_split_t True if green_split_t was computed. @@ -165,21 +174,11 @@ EdgeRootTag classify_edge_roots(std::span edge_coeffs, T& green_split_t, bool& has_green_split_t); -/// Localize the unique root on a 1D Bernstein polynomial already known to -/// have exactly one isolated root. -/// -/// Returns false if the polynomial does not represent a unique isolated root. -template -bool locate_one_root_parameter(std::span edge_coeffs, - T zero_tol, T sign_tol, - int max_depth, - T& root_t); - /// Classify all not-yet-classified edges of an AdaptCell for one level set. /// /// For each edge with tag == not_classified: /// 1. extract exact edge Bernstein from the LevelSetCell -/// 2. classify root structure +/// 2. classify root topology using Bernstein sign information /// 3. store tag and optional green-split parameter /// /// @param adapt_cell The AdaptCell (modified in place). @@ -187,7 +186,7 @@ bool locate_one_root_parameter(std::span edge_coeffs, /// @param level_set_id Which level set (bit position / name index). /// @param zero_tol Tolerance for all-zero. /// @param sign_tol Tolerance for all-positive / all-negative. -/// @param max_depth Maximum subdivision depth for root search. +/// @param max_depth Maximum subdivision depth for topology classification. template void classify_new_edges(AdaptCell& adapt_cell, const LevelSetCell& ls_cell, diff --git a/cpp/src/geometric_quantity.h b/cpp/src/geometric_quantity.h deleted file mode 100644 index 076dbe6..0000000 --- a/cpp/src/geometric_quantity.h +++ /dev/null @@ -1,947 +0,0 @@ -// Copyright (c) 2026 ONERA -// Authors: Susanne Claus -// This file is part of CutCells -// -// SPDX-License-Identifier: MIT -#pragma once - -#include "cell_topology.h" -#include "reference_cell.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cutcells::geom -{ - -/// Local parent entity in the reference parent cell. -/// dim = 0, 1, 2, tdim denotes vertex, edge, face, cell interior. -/// id is local to that entity dimension, except for cell interiors where it is -/// conventionally -1. -struct ParentEntity -{ - int dim = -1; - int id = -1; - - bool valid() const noexcept { return dim >= 0; } -}; - -enum class Degeneracy -{ - none = 0, - zero_input, - zero_frame, - zero_projection, - invalid_parent_entity -}; - -template -struct VectorQuantity -{ - std::vector value; - T norm = T(0); - Degeneracy degeneracy = Degeneracy::none; - - bool degenerate() const noexcept - { - return degeneracy != Degeneracy::none; - } -}; - -template -struct DisplacementComponents -{ - std::vector normal; - std::vector tangential; - T signed_normal_magnitude = T(0); - bool degenerate_normal = false; -}; - -template -struct Alignment -{ - T cosine = T(0); - T angle = T(0); - bool degenerate = false; -}; - -template -struct LineInterval -{ - bool valid = false; - T t0 = T(0); - T t1 = T(0); -}; - -template -inline T default_tolerance() -{ - return T(128) * std::numeric_limits::epsilon(); -} - -template -inline void require_same_size(std::span a, std::span b, - const char* name) -{ - if (a.size() != b.size()) - throw std::invalid_argument(std::string(name) + ": dimension mismatch"); -} - -template -inline void require_size(std::span a, std::size_t n, const char* name) -{ - if (a.size() != n) - throw std::invalid_argument(std::string(name) + ": unexpected dimension"); -} - -template -inline T dot(std::span a, std::span b) -{ - require_same_size(a, b, "dot"); - T out = T(0); - for (std::size_t i = 0; i < a.size(); ++i) - out += a[i] * b[i]; - return out; -} - -template -inline T squared_norm(std::span a) -{ - return dot(a, a); -} - -template -inline T norm(std::span a) -{ - return std::sqrt(squared_norm(a)); -} - -template -inline std::vector subtract(std::span a, std::span b) -{ - require_same_size(a, b, "subtract"); - std::vector out(a.size(), T(0)); - for (std::size_t i = 0; i < a.size(); ++i) - out[i] = a[i] - b[i]; - return out; -} - -template -inline std::array cross(std::span a, std::span b) -{ - require_size(a, 3, "cross"); - require_size(b, 3, "cross"); - return {a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0]}; -} - -template -inline VectorQuantity make_vector_quantity(std::vector value, - T tol, - Degeneracy zero_reason) -{ - const T n = norm(std::span(value.data(), value.size())); - return {std::move(value), n, n <= tol ? zero_reason : Degeneracy::none}; -} - -template -inline std::vector reference_vertex(cell::type parent_cell_type, - int vertex_id) -{ - const int tdim = cell::get_tdim(parent_cell_type); - const int nverts = cell::get_num_vertices(parent_cell_type); - if (vertex_id < 0 || vertex_id >= nverts) - throw std::invalid_argument("reference_vertex: vertex id out of bounds"); - if (parent_cell_type == cell::type::point) - return {}; - - const auto vertices = cell::reference_vertices(parent_cell_type); - std::vector out(static_cast(tdim), T(0)); - for (int d = 0; d < tdim; ++d) - out[static_cast(d)] = - vertices[static_cast(vertex_id * tdim + d)]; - return out; -} - -template -inline VectorQuantity segment_tangent(std::span a, - std::span b, - bool unit = true, - T tol = default_tolerance()) -{ - auto out = subtract(b, a); - const T n = norm(std::span(out.data(), out.size())); - if (n <= tol) - return {std::move(out), n, Degeneracy::zero_frame}; - - if (unit) - { - for (T& x : out) - x /= n; - return {std::move(out), T(1), Degeneracy::none}; - } - - return {std::move(out), n, Degeneracy::none}; -} - -template -inline VectorQuantity parent_edge_tangent(cell::type parent_cell_type, - int parent_edge_id, - bool unit = true, - T tol = default_tolerance()) -{ - const auto edges = cell::edges(parent_cell_type); - if (parent_edge_id < 0 || parent_edge_id >= static_cast(edges.size())) - return {{}, T(0), Degeneracy::invalid_parent_entity}; - - const auto edge = edges[static_cast(parent_edge_id)]; - const auto a = reference_vertex(parent_cell_type, edge[0]); - const auto b = reference_vertex(parent_cell_type, edge[1]); - return segment_tangent( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - unit, - tol); -} - -template -inline VectorQuantity face_normal(std::span a, - std::span b, - std::span c, - bool unit = true, - T tol = default_tolerance()) -{ - require_size(a, 3, "face_normal"); - require_size(b, 3, "face_normal"); - require_size(c, 3, "face_normal"); - - const auto ab = subtract(b, a); - const auto ac = subtract(c, a); - const auto n_array = cross( - std::span(ab.data(), ab.size()), - std::span(ac.data(), ac.size())); - - std::vector out(n_array.begin(), n_array.end()); - const T n = norm(std::span(out.data(), out.size())); - if (n <= tol) - return {std::move(out), n, Degeneracy::zero_frame}; - - if (unit) - { - for (T& x : out) - x /= n; - return {std::move(out), T(1), Degeneracy::none}; - } - - return {std::move(out), n, Degeneracy::none}; -} - -template -inline VectorQuantity parent_face_normal(cell::type parent_cell_type, - int parent_face_id, - bool unit = true, - T tol = default_tolerance()) -{ - if (cell::get_tdim(parent_cell_type) != 3) - return {{}, T(0), Degeneracy::invalid_parent_entity}; - if (parent_face_id < 0 || parent_face_id >= cell::num_faces(parent_cell_type)) - return {{}, T(0), Degeneracy::invalid_parent_entity}; - - const auto face_vertices = cell::face_vertices(parent_cell_type, parent_face_id); - if (face_vertices.size() < 3) - return {{}, T(0), Degeneracy::invalid_parent_entity}; - - const auto a = reference_vertex(parent_cell_type, face_vertices[0]); - const auto b = reference_vertex(parent_cell_type, face_vertices[1]); - const auto c = reference_vertex(parent_cell_type, face_vertices[2]); - return face_normal( - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - std::span(c.data(), c.size()), - unit, - tol); -} - -template -inline VectorQuantity segment_normal_2d(std::span a, - std::span b, - bool unit = true, - T tol = default_tolerance()) -{ - require_size(a, 2, "segment_normal_2d"); - require_size(b, 2, "segment_normal_2d"); - - std::vector out = {-(b[1] - a[1]), b[0] - a[0]}; - const T n = norm(std::span(out.data(), out.size())); - if (n <= tol) - return {std::move(out), n, Degeneracy::zero_frame}; - - if (unit) - { - out[0] /= n; - out[1] /= n; - return {std::move(out), T(1), Degeneracy::none}; - } - - return {std::move(out), n, Degeneracy::none}; -} - -template -inline VectorQuantity segment_normal(std::span a, - std::span b, - bool unit = true, - T tol = default_tolerance()) -{ - return segment_normal_2d(a, b, unit, tol); -} - -/// Unit normal to a segment, constrained to a 3D face tangent plane. -/// The orientation is face_normal x segment_tangent. -template -inline VectorQuantity in_face_segment_normal(std::span a, - std::span b, - std::span face_normal_vector, - bool unit = true, - T tol = default_tolerance()) -{ - require_size(a, 3, "in_face_segment_normal"); - require_size(b, 3, "in_face_segment_normal"); - require_size(face_normal_vector, 3, "in_face_segment_normal"); - - auto tangent = segment_tangent(a, b, false, tol); - if (tangent.degenerate()) - return {std::move(tangent.value), tangent.norm, tangent.degeneracy}; - - const auto n_array = cross( - face_normal_vector, - std::span(tangent.value.data(), tangent.value.size())); - std::vector out(n_array.begin(), n_array.end()); - const T n = norm(std::span(out.data(), out.size())); - if (n <= tol) - return {std::move(out), n, Degeneracy::zero_frame}; - - if (unit) - { - for (T& x : out) - x /= n; - return {std::move(out), T(1), Degeneracy::none}; - } - - return {std::move(out), n, Degeneracy::none}; -} - -template -inline VectorQuantity project_onto_line(std::span vector, - std::span line_direction, - T tol = default_tolerance()) -{ - require_same_size(vector, line_direction, "project_onto_line"); - const T dd = squared_norm(line_direction); - std::vector out(vector.size(), T(0)); - if (dd <= tol * tol) - return {std::move(out), T(0), Degeneracy::zero_frame}; - - const T scale = dot(vector, line_direction) / dd; - for (std::size_t i = 0; i < vector.size(); ++i) - out[i] = scale * line_direction[i]; - return make_vector_quantity(std::move(out), tol, Degeneracy::zero_projection); -} - -template -inline VectorQuantity project_into_plane(std::span vector, - std::span plane_normal, - T tol = default_tolerance()) -{ - require_same_size(vector, plane_normal, "project_into_plane"); - const T nn = squared_norm(plane_normal); - std::vector out(vector.begin(), vector.end()); - if (nn <= tol * tol) - { - std::fill(out.begin(), out.end(), T(0)); - return {std::move(out), T(0), Degeneracy::zero_frame}; - } - - const T scale = dot(vector, plane_normal) / nn; - for (std::size_t i = 0; i < vector.size(); ++i) - out[i] -= scale * plane_normal[i]; - return make_vector_quantity(std::move(out), tol, Degeneracy::zero_projection); -} - -template -inline VectorQuantity project_into_parent_face_tangent(cell::type parent_cell_type, - int parent_face_id, - std::span vector, - T tol = default_tolerance()) -{ - const auto normal = parent_face_normal(parent_cell_type, parent_face_id, false, tol); - if (normal.degenerate()) - return {{}, T(0), normal.degeneracy}; - return project_into_plane( - vector, - std::span(normal.value.data(), normal.value.size()), - tol); -} - -template -inline VectorQuantity project_onto_parent_edge(cell::type parent_cell_type, - int parent_edge_id, - std::span vector, - T tol = default_tolerance()) -{ - const auto tangent = parent_edge_tangent(parent_cell_type, parent_edge_id, false, tol); - if (tangent.degenerate()) - return {{}, T(0), tangent.degeneracy}; - return project_onto_line( - vector, - std::span(tangent.value.data(), tangent.value.size()), - tol); -} - -/// Project a raw direction into the tangent frame admitted by the smallest -/// parent entity that hosts the point. The caller supplies that host entity; -/// use smallest_parent_entity_containing_point when it must be inferred from -/// reference coordinates. -template -inline VectorQuantity admissible_direction_in_parent_frame( - cell::type parent_cell_type, - ParentEntity host, - std::span raw_direction, - T tol = default_tolerance()) -{ - const int tdim = cell::get_tdim(parent_cell_type); - if (static_cast(raw_direction.size()) != tdim) - throw std::invalid_argument( - "admissible_direction_in_parent_frame: direction dimension mismatch"); - - if (!host.valid() || host.dim > tdim) - return {{}, T(0), Degeneracy::invalid_parent_entity}; - - if (host.dim == tdim) - { - std::vector out(raw_direction.begin(), raw_direction.end()); - return make_vector_quantity(std::move(out), tol, Degeneracy::zero_input); - } - - if (host.dim == 2 && tdim == 3) - return project_into_parent_face_tangent( - parent_cell_type, host.id, raw_direction, tol); - - if (host.dim == 1) - return project_onto_parent_edge( - parent_cell_type, host.id, raw_direction, tol); - - if (host.dim == 0) - return {std::vector(raw_direction.size(), T(0)), - T(0), - Degeneracy::zero_projection}; - - return {{}, T(0), Degeneracy::invalid_parent_entity}; -} - -template -inline VectorQuantity restricted_level_set_gradient_in_parent_frame( - cell::type parent_cell_type, - ParentEntity host, - std::span raw_gradient, - T tol = default_tolerance()) -{ - return admissible_direction_in_parent_frame( - parent_cell_type, host, raw_gradient, tol); -} - -template -inline DisplacementComponents decompose_displacement( - std::span displacement, - std::span normal_vector, - T tol = default_tolerance()) -{ - require_same_size(displacement, normal_vector, "decompose_displacement"); - DisplacementComponents out; - out.normal.assign(displacement.size(), T(0)); - out.tangential.assign(displacement.begin(), displacement.end()); - - const T nn = squared_norm(normal_vector); - if (nn <= tol * tol) - { - out.degenerate_normal = true; - return out; - } - - const T scale = dot(displacement, normal_vector) / nn; - const T n = std::sqrt(nn); - out.signed_normal_magnitude = dot(displacement, normal_vector) / n; - for (std::size_t i = 0; i < displacement.size(); ++i) - { - out.normal[i] = scale * normal_vector[i]; - out.tangential[i] = displacement[i] - out.normal[i]; - } - return out; -} - -template -inline Alignment alignment(std::span a, - std::span b, - T tol = default_tolerance()) -{ - require_same_size(a, b, "alignment"); - const T na = norm(a); - const T nb = norm(b); - if (na <= tol || nb <= tol) - return {T(0), T(0), true}; - - const T c = std::clamp(dot(a, b) / (na * nb), T(-1), T(1)); - return {c, std::acos(c), false}; -} - -template -inline T cosine_alignment(std::span a, - std::span b, - T tol = default_tolerance()) -{ - return alignment(a, b, tol).cosine; -} - -template -inline T absolute_alignment(std::span a, - std::span b, - T tol = default_tolerance()) -{ - return std::fabs(cosine_alignment(a, b, tol)); -} - -template -inline T angle_between(std::span a, - std::span b, - T tol = default_tolerance()) -{ - return alignment(a, b, tol).angle; -} - -template -inline bool point_in_parent_cell(cell::type parent_cell_type, - std::span x, - T tol = default_tolerance()) -{ - const int tdim = cell::get_tdim(parent_cell_type); - if (static_cast(x.size()) != tdim) - throw std::invalid_argument("point_in_parent_cell: point dimension mismatch"); - - switch (parent_cell_type) - { - case cell::type::point: - return x.empty(); - case cell::type::interval: - return x[0] >= -tol && x[0] <= T(1) + tol; - case cell::type::triangle: - return x[0] >= -tol && x[1] >= -tol && x[0] + x[1] <= T(1) + tol; - case cell::type::quadrilateral: - return x[0] >= -tol && x[0] <= T(1) + tol - && x[1] >= -tol && x[1] <= T(1) + tol; - case cell::type::tetrahedron: - return x[0] >= -tol && x[1] >= -tol && x[2] >= -tol - && x[0] + x[1] + x[2] <= T(1) + tol; - case cell::type::hexahedron: - return x[0] >= -tol && x[0] <= T(1) + tol - && x[1] >= -tol && x[1] <= T(1) + tol - && x[2] >= -tol && x[2] <= T(1) + tol; - case cell::type::prism: - return x[0] >= -tol && x[1] >= -tol && x[0] + x[1] <= T(1) + tol - && x[2] >= -tol && x[2] <= T(1) + tol; - case cell::type::pyramid: - return x[0] >= -tol && x[1] >= -tol && x[2] >= -tol - && x[0] + x[2] <= T(1) + tol - && x[1] + x[2] <= T(1) + tol; - default: - return false; - } -} - -template -inline bool point_on_segment(std::span x, - std::span a, - std::span b, - T tol = default_tolerance(), - T* parameter = nullptr) -{ - require_same_size(x, a, "point_on_segment"); - require_same_size(a, b, "point_on_segment"); - - const auto ab = subtract(b, a); - const T ab2 = squared_norm(std::span(ab.data(), ab.size())); - if (ab2 <= tol * tol) - return false; - - T ax_ab = T(0); - for (std::size_t i = 0; i < x.size(); ++i) - ax_ab += (x[i] - a[i]) * ab[i]; - const T t = ax_ab / ab2; - if (parameter != nullptr) - *parameter = t; - - if (t < -tol || t > T(1) + tol) - return false; - - T distance2 = T(0); - for (std::size_t i = 0; i < x.size(); ++i) - { - const T closest = a[i] + t * ab[i]; - const T r = x[i] - closest; - distance2 += r * r; - } - return distance2 <= tol * tol; -} - -template -inline bool point_on_parent_edge(cell::type parent_cell_type, - int parent_edge_id, - std::span x, - T tol = default_tolerance(), - T* edge_parameter = nullptr) -{ - const auto edges = cell::edges(parent_cell_type); - if (parent_edge_id < 0 || parent_edge_id >= static_cast(edges.size())) - return false; - const auto edge = edges[static_cast(parent_edge_id)]; - const auto a = reference_vertex(parent_cell_type, edge[0]); - const auto b = reference_vertex(parent_cell_type, edge[1]); - return point_on_segment( - x, - std::span(a.data(), a.size()), - std::span(b.data(), b.size()), - tol, - edge_parameter); -} - -template -inline bool point_on_parent_face(cell::type parent_cell_type, - int parent_face_id, - std::span x, - T tol = default_tolerance()) -{ - if (cell::get_tdim(parent_cell_type) != 3) - return false; - if (parent_face_id < 0 || parent_face_id >= cell::num_faces(parent_cell_type)) - return false; - - const auto fv = cell::face_vertices(parent_cell_type, parent_face_id); - if (fv.size() < 3) - return false; - - const auto a = reference_vertex(parent_cell_type, fv[0]); - const auto normal = parent_face_normal(parent_cell_type, parent_face_id, false, tol); - if (normal.degenerate()) - return false; - - T signed_distance_num = T(0); - for (std::size_t i = 0; i < x.size(); ++i) - signed_distance_num += (x[i] - a[i]) * normal.value[i]; - if (std::fabs(signed_distance_num) > tol * normal.norm) - return false; - - return point_in_parent_cell(parent_cell_type, x, tol); -} - -template -inline bool point_in_parent_entity(cell::type parent_cell_type, - ParentEntity entity, - std::span x, - T tol = default_tolerance()) -{ - const int tdim = cell::get_tdim(parent_cell_type); - if (!entity.valid() || entity.dim > tdim) - return false; - - if (entity.dim == tdim) - return point_in_parent_cell(parent_cell_type, x, tol); - - if (entity.dim == 2 && tdim == 3) - return point_on_parent_face(parent_cell_type, entity.id, x, tol); - - if (entity.dim == 1) - return point_on_parent_edge(parent_cell_type, entity.id, x, tol); - - if (entity.dim == 0) - { - const auto v = reference_vertex(parent_cell_type, entity.id); - const auto delta = subtract(x, std::span(v.data(), v.size())); - return norm(std::span(delta.data(), delta.size())) <= tol; - } - - return false; -} - -template -inline ParentEntity smallest_parent_entity_containing_point( - cell::type parent_cell_type, - std::span x, - T tol = default_tolerance()) -{ - const int nverts = cell::get_num_vertices(parent_cell_type); - for (int v = 0; v < nverts; ++v) - { - const ParentEntity entity{0, v}; - if (point_in_parent_entity(parent_cell_type, entity, x, tol)) - return entity; - } - - if (cell::get_tdim(parent_cell_type) >= 1) - { - const auto edges = cell::edges(parent_cell_type); - for (int e = 0; e < static_cast(edges.size()); ++e) - { - if (point_on_parent_edge(parent_cell_type, e, x, tol)) - return {1, e}; - } - } - - if (cell::get_tdim(parent_cell_type) == 3) - { - for (int f = 0; f < cell::num_faces(parent_cell_type); ++f) - { - if (point_on_parent_face(parent_cell_type, f, x, tol)) - return {2, f}; - } - } - - if (point_in_parent_cell(parent_cell_type, x, tol)) - return {cell::get_tdim(parent_cell_type), -1}; - - return {-1, -1}; -} - -template -inline bool clip_halfspace(std::span x0, - std::span direction, - std::span normal, - T rhs, - LineInterval& interval, - T tol) -{ - const T value0 = dot(normal, x0); - const T rate = dot(normal, direction); - if (std::fabs(rate) <= tol) - return value0 <= rhs + tol; - - const T hit = (rhs - value0) / rate; - if (rate > T(0)) - interval.t1 = std::min(interval.t1, hit); - else - interval.t0 = std::max(interval.t0, hit); - - return interval.t0 <= interval.t1 + tol; -} - -template -inline bool clip_coordinate_lower(int axis, - std::span x0, - std::span direction, - LineInterval& interval, - T tol) -{ - std::vector n(x0.size(), T(0)); - n[static_cast(axis)] = T(-1); - return clip_halfspace( - x0, direction, std::span(n.data(), n.size()), T(0), interval, tol); -} - -template -inline bool clip_coordinate_upper(int axis, - T upper, - std::span x0, - std::span direction, - LineInterval& interval, - T tol) -{ - std::vector n(x0.size(), T(0)); - n[static_cast(axis)] = T(1); - return clip_halfspace( - x0, direction, std::span(n.data(), n.size()), upper, interval, tol); -} - -template -inline bool clip_sum_upper(std::span axes, - T upper, - std::span x0, - std::span direction, - LineInterval& interval, - T tol) -{ - std::vector n(x0.size(), T(0)); - for (const int axis : axes) - n[static_cast(axis)] = T(1); - return clip_halfspace( - x0, direction, std::span(n.data(), n.size()), upper, interval, tol); -} - -template -inline LineInterval clip_line_interval_in_parent_cell( - cell::type parent_cell_type, - std::span x0, - std::span direction, - T t0 = -std::numeric_limits::infinity(), - T t1 = std::numeric_limits::infinity(), - T tol = default_tolerance()) -{ - const int tdim = cell::get_tdim(parent_cell_type); - if (static_cast(x0.size()) != tdim - || static_cast(direction.size()) != tdim) - { - throw std::invalid_argument( - "clip_line_interval_in_parent_cell: dimension mismatch"); - } - - LineInterval interval{true, t0, t1}; - auto lower = [&](int axis) - { - return clip_coordinate_lower(axis, x0, direction, interval, tol); - }; - auto upper = [&](int axis, T value) - { - return clip_coordinate_upper(axis, value, x0, direction, interval, tol); - }; - auto sum_upper = [&](std::span axes, T value) - { - return clip_sum_upper(axes, value, x0, direction, interval, tol); - }; - - switch (parent_cell_type) - { - case cell::type::point: - interval.valid = x0.empty() && direction.empty(); - return interval; - case cell::type::interval: - interval.valid = lower(0) && upper(0, T(1)); - return interval; - case cell::type::triangle: - { - const std::array axes = {0, 1}; - interval.valid = lower(0) && lower(1) - && sum_upper(std::span(axes), T(1)); - return interval; - } - case cell::type::quadrilateral: - interval.valid = lower(0) && upper(0, T(1)) - && lower(1) && upper(1, T(1)); - return interval; - case cell::type::tetrahedron: - { - const std::array axes = {0, 1, 2}; - interval.valid = lower(0) && lower(1) && lower(2) - && sum_upper(std::span(axes), T(1)); - return interval; - } - case cell::type::hexahedron: - interval.valid = lower(0) && upper(0, T(1)) - && lower(1) && upper(1, T(1)) - && lower(2) && upper(2, T(1)); - return interval; - case cell::type::prism: - { - const std::array axes = {0, 1}; - interval.valid = lower(0) && lower(1) - && sum_upper(std::span(axes), T(1)) - && lower(2) && upper(2, T(1)); - return interval; - } - case cell::type::pyramid: - { - const std::array xz = {0, 2}; - const std::array yz = {1, 2}; - interval.valid = lower(0) && lower(1) && lower(2) - && sum_upper(std::span(xz), T(1)) - && sum_upper(std::span(yz), T(1)); - return interval; - } - default: - interval.valid = false; - return interval; - } -} - -template -inline LineInterval clip_line_interval_in_parent_entity( - cell::type parent_cell_type, - ParentEntity entity, - std::span x0, - std::span direction, - T t0 = -std::numeric_limits::infinity(), - T t1 = std::numeric_limits::infinity(), - T tol = default_tolerance()) -{ - const int tdim = cell::get_tdim(parent_cell_type); - if (!entity.valid() || entity.dim > tdim) - return {false, T(0), T(0)}; - - if (entity.dim == tdim) - return clip_line_interval_in_parent_cell( - parent_cell_type, x0, direction, t0, t1, tol); - - if (entity.dim == 2 && tdim == 3) - { - if (!point_on_parent_face(parent_cell_type, entity.id, x0, tol)) - return {false, T(0), T(0)}; - const auto normal = parent_face_normal(parent_cell_type, entity.id, false, tol); - if (normal.degenerate()) - return {false, T(0), T(0)}; - const T normal_rate = dot( - direction, - std::span(normal.value.data(), normal.value.size())); - if (std::fabs(normal_rate) > tol * std::max(T(1), normal.norm)) - return {false, T(0), T(0)}; - return clip_line_interval_in_parent_cell( - parent_cell_type, x0, direction, t0, t1, tol); - } - - if (entity.dim == 1) - { - T edge_s = T(0); - if (!point_on_parent_edge(parent_cell_type, entity.id, x0, tol, &edge_s)) - return {false, T(0), T(0)}; - - const auto tangent = parent_edge_tangent(parent_cell_type, entity.id, false, tol); - if (tangent.degenerate()) - return {false, T(0), T(0)}; - - const T tt = squared_norm( - std::span(tangent.value.data(), tangent.value.size())); - const T ds = dot( - direction, - std::span(tangent.value.data(), tangent.value.size())) / tt; - std::vector parallel(direction.size(), T(0)); - for (std::size_t i = 0; i < direction.size(); ++i) - parallel[i] = ds * tangent.value[i]; - const auto residual = subtract( - direction, std::span(parallel.data(), parallel.size())); - if (norm(std::span(residual.data(), residual.size())) > tol) - return {false, T(0), T(0)}; - - if (std::fabs(ds) <= tol) - return {true, t0, t1}; - const T a = (T(0) - edge_s) / ds; - const T b = (T(1) - edge_s) / ds; - return {std::max(t0, std::min(a, b)) <= std::min(t1, std::max(a, b)) + tol, - std::max(t0, std::min(a, b)), - std::min(t1, std::max(a, b))}; - } - - if (entity.dim == 0) - { - if (!point_in_parent_entity(parent_cell_type, entity, x0, tol)) - return {false, T(0), T(0)}; - if (norm(direction) <= tol) - return {true, t0, t1}; - return {true, T(0), T(0)}; - } - - return {false, T(0), T(0)}; -} - -} // namespace cutcells::geom diff --git a/cpp/src/graph_criteria.h b/cpp/src/graph_criteria.h deleted file mode 100644 index d778d08..0000000 --- a/cpp/src/graph_criteria.h +++ /dev/null @@ -1,640 +0,0 @@ -// Copyright (c) 2026 ONERA -// Authors: Susanne Claus -// This file is part of CutCells -// -// SPDX-License-Identifier: MIT -#pragma once - -#include "geometric_quantity.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cutcells::graph_criteria -{ - -enum class HostDimension : std::uint8_t -{ - edge = 1, - face = 2 -}; - -enum class DirectionKind : std::uint8_t -{ - projected_straight_host_normal = 0, - projected_level_set_gradient = 1 -}; - -enum class FailureReason : std::uint8_t -{ - none = 0, - invalid_input, - invalid_parent_entity, - host_point_outside_parent_entity, - invalid_host_frame, - weak_restricted_gradient, - degenerate_direction, - direction_not_admissible, - tangent_to_zero_set, - root_not_on_search_line, - root_segment_leaves_parent_entity, - excessive_correction_distance, - excessive_tangential_shift, - excessive_drift_amplification, - direction_too_tangential_to_host, - edge_ordering_fold, - face_degenerate, - surface_jacobian_not_positive, - refinement_requested -}; - -inline std::string_view failure_reason_name(FailureReason reason) -{ - switch (reason) - { - case FailureReason::none: - return "none"; - case FailureReason::invalid_input: - return "invalid_input"; - case FailureReason::invalid_parent_entity: - return "invalid_parent_entity"; - case FailureReason::host_point_outside_parent_entity: - return "host_point_outside_parent_entity"; - case FailureReason::invalid_host_frame: - return "invalid_host_frame"; - case FailureReason::weak_restricted_gradient: - return "weak_restricted_gradient"; - case FailureReason::degenerate_direction: - return "degenerate_direction"; - case FailureReason::direction_not_admissible: - return "direction_not_admissible"; - case FailureReason::tangent_to_zero_set: - return "tangent_to_zero_set"; - case FailureReason::root_not_on_search_line: - return "root_not_on_search_line"; - case FailureReason::root_segment_leaves_parent_entity: - return "root_segment_leaves_parent_entity"; - case FailureReason::excessive_correction_distance: - return "excessive_correction_distance"; - case FailureReason::excessive_tangential_shift: - return "excessive_tangential_shift"; - case FailureReason::excessive_drift_amplification: - return "excessive_drift_amplification"; - case FailureReason::direction_too_tangential_to_host: - return "direction_too_tangential_to_host"; - case FailureReason::edge_ordering_fold: - return "edge_ordering_fold"; - case FailureReason::face_degenerate: - return "face_degenerate"; - case FailureReason::surface_jacobian_not_positive: - return "surface_jacobian_not_positive"; - case FailureReason::refinement_requested: - return "refinement_requested"; - } - return "unknown"; -} - -template -struct Options -{ - T tolerance = geom::default_tolerance(); - T min_restricted_gradient_strength = T(64) * std::numeric_limits::epsilon(); - T min_transversality = T(1.0e-6); - T min_host_normal_alignment = T(0.25); - T max_drift_amplification = T(4); - T max_relative_correction_distance = T(0.5); - T max_relative_tangential_shift = T(0.25); - T max_root_line_residual = std::sqrt(std::numeric_limits::epsilon()); - T min_edge_ordering_fraction = std::sqrt(std::numeric_limits::epsilon()); - T min_surface_jacobian_ratio = std::sqrt(std::numeric_limits::epsilon()); -}; - -template -struct HostFrame -{ - HostDimension dimension = HostDimension::edge; - std::vector normal; - std::vector tangent; - T h = T(1); -}; - -template -struct Metrics -{ - T restricted_gradient_strength = T(0); - T true_transversality = T(0); - T host_normal_alignment = T(0); - T drift_amplification = std::numeric_limits::infinity(); - T root_alpha = T(0); - T root_line_residual = T(0); - bool root_segment_contained = false; - T relative_correction_distance = T(0); - T relative_tangential_shift = T(0); -}; - -template -struct DirectionReport -{ - bool accepted = false; - DirectionKind kind = DirectionKind::projected_straight_host_normal; - FailureReason failure_reason = FailureReason::none; - Metrics metrics; -}; - -template -struct SelectionReport -{ - bool accepted = false; - bool request_refinement = false; - DirectionKind selected_kind = DirectionKind::projected_straight_host_normal; - FailureReason failure_reason = FailureReason::none; - DirectionReport straight_host_normal; - DirectionReport level_set_gradient; - std::vector selected_direction; - std::vector selected_root; -}; - -template -struct EdgeOrderingReport -{ - bool accepted = false; - FailureReason failure_reason = FailureReason::none; - T minimum_gap_ratio = std::numeric_limits::infinity(); -}; - -template -struct FaceQualityReport -{ - bool accepted = false; - FailureReason failure_reason = FailureReason::none; - T minimum_surface_jacobian_ratio = std::numeric_limits::infinity(); - int failed_triangle_index = -1; - T failed_surface_jacobian_ratio = std::numeric_limits::quiet_NaN(); -}; - -template -inline std::span vector_span(const std::vector& v) -{ - return std::span(v.data(), v.size()); -} - -template -inline bool same_dimension(std::size_t n, std::span a) -{ - return a.size() == n; -} - -template -inline bool same_dimension(std::size_t n, std::span a, Spans... rest) -{ - return a.size() == n && same_dimension(n, rest...); -} - -template -inline T safe_scale(T a, T b, T c, T d) -{ - return std::max({T(1), std::fabs(a), std::fabs(b), std::fabs(c), std::fabs(d)}); -} - -template -inline T absolute_cosine(std::span a, - std::span b, - T tol) -{ - const auto alignment = geom::alignment(a, b, tol); - if (alignment.degenerate) - return T(0); - return std::fabs(alignment.cosine); -} - -template -inline T drift_from_alignment(T abs_cosine, T tol) -{ - if (abs_cosine <= tol) - return std::numeric_limits::infinity(); - const T tangential2 = std::max(T(0), T(1) - abs_cosine * abs_cosine); - return std::sqrt(tangential2) / abs_cosine; -} - -template -inline bool valid_host_frame(const HostFrame& host, - std::size_t tdim, - T tol) -{ - if (!(host.h > tol)) - return false; - if (host.normal.size() != tdim - || geom::norm(vector_span(host.normal)) <= tol) - { - return false; - } - if (host.dimension == HostDimension::edge) - { - if (host.tangent.size() != tdim - || geom::norm(vector_span(host.tangent)) <= tol) - { - return false; - } - } - return host.dimension == HostDimension::edge - || host.dimension == HostDimension::face; -} - -template -inline bool direction_admissible_in_parent_entity( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - std::span x_h, - std::span direction, - T tol) -{ - if (!geom::point_in_parent_entity( - parent_cell_type, admissible_parent_entity, x_h, tol)) - { - return false; - } - if (admissible_parent_entity.dim == 0 - && geom::norm(direction) > tol) - { - return false; - } - - const auto interval = geom::clip_line_interval_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - x_h, - direction, - -std::numeric_limits::infinity(), - std::numeric_limits::infinity(), - tol); - - return interval.valid && interval.t0 <= tol && interval.t1 >= -tol; -} - -template -inline bool root_segment_contained_in_parent_entity( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - std::span x_h, - std::span direction, - T alpha, - T tol) -{ - const auto interval = geom::clip_line_interval_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - x_h, - direction, - -std::numeric_limits::infinity(), - std::numeric_limits::infinity(), - tol); - if (!interval.valid) - return false; - - const T lo = std::min(T(0), alpha); - const T hi = std::max(T(0), alpha); - return lo >= interval.t0 - tol && hi <= interval.t1 + tol; -} - -template -inline T relative_tangential_shift(std::span delta, - const HostFrame& host, - T tol) -{ - if (host.dimension == HostDimension::edge) - { - const T tangent_norm = geom::norm(vector_span(host.tangent)); - if (tangent_norm <= tol) - return std::numeric_limits::infinity(); - return std::fabs(geom::dot(delta, vector_span(host.tangent))) - / (tangent_norm * host.h); - } - - const auto tangential = geom::project_into_plane( - delta, vector_span(host.normal), tol); - if (tangential.degenerate() && tangential.degeneracy == geom::Degeneracy::zero_frame) - return std::numeric_limits::infinity(); - return tangential.norm / host.h; -} - -template -inline DirectionReport evaluate_direction( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - const HostFrame& host, - std::span x_h, - std::span raw_level_set_gradient, - std::span candidate_direction, - std::span x_c, - DirectionKind kind, - const Options& options = {}) -{ - DirectionReport report; - report.kind = kind; - - const int tdim = cell::get_tdim(parent_cell_type); - if (tdim <= 0 - || !admissible_parent_entity.valid() - || admissible_parent_entity.dim > tdim) - { - report.failure_reason = FailureReason::invalid_parent_entity; - return report; - } - - if (!same_dimension( - static_cast(tdim), - x_h, - raw_level_set_gradient, - candidate_direction, - x_c)) - { - report.failure_reason = FailureReason::invalid_input; - return report; - } - - const T tol = options.tolerance; - if (!valid_host_frame(host, static_cast(tdim), tol)) - { - report.failure_reason = FailureReason::invalid_host_frame; - return report; - } - - if (!geom::point_in_parent_entity( - parent_cell_type, admissible_parent_entity, x_h, tol)) - { - report.failure_reason = FailureReason::host_point_outside_parent_entity; - return report; - } - - const auto restricted_gradient = - geom::restricted_level_set_gradient_in_parent_frame( - parent_cell_type, - admissible_parent_entity, - raw_level_set_gradient, - tol); - report.metrics.restricted_gradient_strength = restricted_gradient.norm; - if (restricted_gradient.degenerate() - || restricted_gradient.norm < options.min_restricted_gradient_strength) - { - report.failure_reason = FailureReason::weak_restricted_gradient; - return report; - } - - const T direction_norm = geom::norm(candidate_direction); - if (direction_norm <= tol) - { - report.failure_reason = FailureReason::degenerate_direction; - return report; - } - - if (!direction_admissible_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - x_h, - candidate_direction, - tol)) - { - report.failure_reason = FailureReason::direction_not_admissible; - return report; - } - - report.metrics.true_transversality = - std::fabs(geom::dot( - candidate_direction, - vector_span(restricted_gradient.value))) - / (direction_norm * restricted_gradient.norm); - - report.metrics.host_normal_alignment = - absolute_cosine(candidate_direction, vector_span(host.normal), tol); - report.metrics.drift_amplification = - drift_from_alignment(report.metrics.host_normal_alignment, tol); - - const auto delta = geom::subtract(x_c, x_h); - const T delta_norm = geom::norm(vector_span(delta)); - const T dd = direction_norm * direction_norm; - report.metrics.root_alpha = - geom::dot(vector_span(delta), candidate_direction) / dd; - - std::vector residual(delta.size(), T(0)); - for (std::size_t i = 0; i < delta.size(); ++i) - { - residual[i] = delta[i] - - report.metrics.root_alpha * candidate_direction[i]; - } - const T residual_norm = geom::norm(vector_span(residual)); - const T residual_scale = safe_scale( - host.h, - delta_norm, - std::fabs(report.metrics.root_alpha) * direction_norm, - direction_norm); - report.metrics.root_line_residual = residual_norm / residual_scale; - - report.metrics.root_segment_contained = - root_segment_contained_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - x_h, - candidate_direction, - report.metrics.root_alpha, - tol) - && geom::point_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - x_c, - tol); - - report.metrics.relative_correction_distance = delta_norm / host.h; - report.metrics.relative_tangential_shift = - relative_tangential_shift(vector_span(delta), host, tol); - - if (report.metrics.true_transversality < options.min_transversality) - { - report.failure_reason = FailureReason::tangent_to_zero_set; - return report; - } - if (report.metrics.root_line_residual > options.max_root_line_residual) - { - report.failure_reason = FailureReason::root_not_on_search_line; - return report; - } - if (!report.metrics.root_segment_contained) - { - report.failure_reason = FailureReason::root_segment_leaves_parent_entity; - return report; - } - if (report.metrics.relative_correction_distance - > options.max_relative_correction_distance) - { - report.failure_reason = FailureReason::excessive_correction_distance; - return report; - } - if (report.metrics.relative_tangential_shift - > options.max_relative_tangential_shift) - { - report.failure_reason = FailureReason::excessive_tangential_shift; - return report; - } - if (report.metrics.drift_amplification > options.max_drift_amplification) - { - report.failure_reason = FailureReason::excessive_drift_amplification; - return report; - } - if (report.metrics.host_normal_alignment < options.min_host_normal_alignment) - { - report.failure_reason = FailureReason::direction_too_tangential_to_host; - return report; - } - - report.accepted = true; - report.failure_reason = FailureReason::none; - return report; -} - -template -inline SelectionReport select_preferred_direction( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - const HostFrame& host, - std::span x_h, - std::span raw_level_set_gradient, - std::span projected_level_set_gradient_direction, - std::span projected_level_set_gradient_root, - std::span projected_straight_host_normal_direction, - std::span projected_straight_host_normal_root, - const Options& options = {}) -{ - SelectionReport report; - report.straight_host_normal = evaluate_direction( - parent_cell_type, - admissible_parent_entity, - host, - x_h, - raw_level_set_gradient, - projected_straight_host_normal_direction, - projected_straight_host_normal_root, - DirectionKind::projected_straight_host_normal, - options); - report.level_set_gradient = evaluate_direction( - parent_cell_type, - admissible_parent_entity, - host, - x_h, - raw_level_set_gradient, - projected_level_set_gradient_direction, - projected_level_set_gradient_root, - DirectionKind::projected_level_set_gradient, - options); - - if (report.straight_host_normal.accepted) - { - report.accepted = true; - report.selected_kind = DirectionKind::projected_straight_host_normal; - report.failure_reason = FailureReason::none; - report.selected_direction.assign( - projected_straight_host_normal_direction.begin(), - projected_straight_host_normal_direction.end()); - report.selected_root.assign( - projected_straight_host_normal_root.begin(), - projected_straight_host_normal_root.end()); - return report; - } - - if (report.level_set_gradient.accepted - && report.level_set_gradient.metrics.drift_amplification - <= options.max_drift_amplification) - { - report.accepted = true; - report.selected_kind = DirectionKind::projected_level_set_gradient; - report.failure_reason = FailureReason::none; - report.selected_direction.assign( - projected_level_set_gradient_direction.begin(), - projected_level_set_gradient_direction.end()); - report.selected_root.assign( - projected_level_set_gradient_root.begin(), - projected_level_set_gradient_root.end()); - return report; - } - - report.accepted = false; - report.request_refinement = true; - report.failure_reason = FailureReason::refinement_requested; - return report; -} - -template -inline EdgeOrderingReport evaluate_projected_edge_ordering( - std::span host_points, - std::span corrected_points, - int point_dim, - std::span host_tangent, - T h_edge, - const Options& options = {}) -{ - EdgeOrderingReport report; - const T tol = options.tolerance; - - if (point_dim <= 0 - || host_points.size() != corrected_points.size() - || host_points.size() % static_cast(point_dim) != 0 - || host_tangent.size() != static_cast(point_dim) - || !(h_edge > tol)) - { - report.failure_reason = FailureReason::invalid_input; - return report; - } - - const std::size_t n = - host_points.size() / static_cast(point_dim); - if (n < 2 || geom::norm(host_tangent) <= tol) - { - report.failure_reason = FailureReason::invalid_host_frame; - return report; - } - - std::vector host_s(n, T(0)); - std::vector corr_s(n, T(0)); - for (std::size_t i = 0; i < n; ++i) - { - const auto hp = host_points.subspan( - i * static_cast(point_dim), - static_cast(point_dim)); - const auto cp = corrected_points.subspan( - i * static_cast(point_dim), - static_cast(point_dim)); - host_s[i] = geom::dot(hp, host_tangent); - corr_s[i] = geom::dot(cp, host_tangent); - } - - const T orientation = (host_s.back() >= host_s.front()) ? T(1) : T(-1); - report.minimum_gap_ratio = std::numeric_limits::infinity(); - for (std::size_t i = 1; i < n; ++i) - { - const T host_gap = orientation * (host_s[i] - host_s[i - 1]); - const T corr_gap = orientation * (corr_s[i] - corr_s[i - 1]); - if (host_gap <= tol * h_edge) - { - report.failure_reason = FailureReason::invalid_host_frame; - return report; - } - const T gap_ratio = corr_gap / host_gap; - report.minimum_gap_ratio = std::min(report.minimum_gap_ratio, gap_ratio); - if (gap_ratio <= options.min_edge_ordering_fraction) - { - report.failure_reason = FailureReason::edge_ordering_fold; - return report; - } - } - - report.accepted = true; - report.failure_reason = FailureReason::none; - return report; -} - -} // namespace cutcells::graph_criteria diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index e6cc19c..59090b1 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -281,54 +281,6 @@ void refresh_adapt_cell_semantics( recompute_active_level_set_masks(ac, total_num_level_sets); } -template -void merge_graph_diagnostics(ReadyCellGraphDiagnostics& dst, - const ReadyCellGraphDiagnostics& src) -{ - dst.accepted = dst.accepted && src.accepted; - dst.checked_cells += src.checked_cells; - dst.checked_edges += src.checked_edges; - dst.checked_faces += src.checked_faces; - dst.failed_checks += src.failed_checks; - dst.graph_refinements += src.graph_refinements; - if (dst.first_failed_cell < 0 && src.first_failed_cell >= 0) - { - dst.first_failed_cell = src.first_failed_cell; - dst.first_failure_reason = src.first_failure_reason; - } - if (dst.first_failed_projection_seed.empty() - && !src.first_failed_projection_seed.empty()) - { - dst.first_failed_projection_seed = src.first_failed_projection_seed; - dst.first_failed_projection_direction = - src.first_failed_projection_direction; - dst.first_failed_projection_clip_lo = - src.first_failed_projection_clip_lo; - dst.first_failed_projection_clip_hi = - src.first_failed_projection_clip_hi; - dst.first_failed_projection_root_t = - src.first_failed_projection_root_t; - } - dst.min_true_transversality = - std::min(dst.min_true_transversality, src.min_true_transversality); - dst.min_host_normal_alignment = - std::min(dst.min_host_normal_alignment, src.min_host_normal_alignment); - dst.max_drift_amplification = - std::max(dst.max_drift_amplification, src.max_drift_amplification); - dst.max_relative_correction_distance = - std::max(dst.max_relative_correction_distance, - src.max_relative_correction_distance); - dst.max_relative_tangential_shift = - std::max(dst.max_relative_tangential_shift, - src.max_relative_tangential_shift); - dst.min_edge_gap_ratio = - std::min(dst.min_edge_gap_ratio, src.min_edge_gap_ratio); - dst.min_face_area_ratio = - std::min(dst.min_face_area_ratio, src.min_face_area_ratio); - dst.zero_entities.insert( - dst.zero_entities.end(), src.zero_entities.begin(), src.zero_entities.end()); -} - } // anonymous namespace // ===================================================================== @@ -339,8 +291,7 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const LevelSetFunction& ls, - bool triangulate_cut_parts, - const ReadyCellGraphOptions& graph_options) + bool triangulate_cut_parts) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -393,11 +344,10 @@ cut(const MeshView& mesh, // AdaptCell AdaptCell ac = make_adapt_cell(mesh, ci); - ReadyCellGraphDiagnostics graph_diag = - certify_refine_graph_check_and_process_ready_cells( + certify_refine_and_process_ready_cells( ac, hc.level_set_cells.back(), /*level_set_id=*/0, /*max_iterations=*/8, T(1e-12), T(1e-12), /*edge_max_depth=*/20, - triangulate_cut_parts, graph_options); + triangulate_cut_parts); { const std::array processed_ids = {0}; const auto* processed_cell = &hc.level_set_cells.back(); @@ -414,17 +364,10 @@ cut(const MeshView& mesh, if (ac.tdim == 3) build_faces(ac); rebuild_zero_entity_inventory(ac); - populate_committed_zero_entity_graph_diagnostics( - ac, - hc.level_set_cells.back(), - /*level_set_id=*/0, - graph_options, - graph_diag); hc.adapt_cells.push_back(std::move(ac)); // Single LS: bit 0 is always set. hc.active_level_set_mask.push_back(std::uint64_t(1)); - hc.graph_diagnostics.push_back(graph_diag); hc.parent_cell_ids.push_back(ci); } @@ -440,8 +383,7 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const std::vector>& level_sets, - bool triangulate_cut_parts, - const ReadyCellGraphOptions& graph_options) + bool triangulate_cut_parts) { if (!mesh.has_cell_types()) throw std::runtime_error("cut: MeshView must have cell types"); @@ -537,16 +479,13 @@ cut(const MeshView& mesh, // Process intersecting level sets recursively (input order). // std::uint64_t cell_active_mask = 0; - ReadyCellGraphDiagnostics graph_diag; for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) { const int li = intersected_ls_indices[k]; - const ReadyCellGraphDiagnostics one_ls_graph = - certify_refine_graph_check_and_process_ready_cells( + certify_refine_and_process_ready_cells( ac, intersected_ls_cells[k], li, /*max_iterations=*/8, T(1e-12), T(1e-12), - /*edge_max_depth=*/20, triangulate_cut_parts, graph_options); - merge_graph_diagnostics(graph_diag, one_ls_graph); + /*edge_max_depth=*/20, triangulate_cut_parts); // New vertices created while processing level set li must be // reclassified for all already-processed level sets. @@ -572,31 +511,15 @@ cut(const MeshView& mesh, nls, T(1e-12)); rebuild_zero_entity_inventory(ac); - graph_diag.zero_entities.clear(); - for (std::size_t k = 0; k < intersected_ls_indices.size(); ++k) - { - ReadyCellGraphDiagnostics zero_entity_graph; - populate_committed_zero_entity_graph_diagnostics( - ac, - intersected_ls_cells[k], - intersected_ls_indices[k], - graph_options, - zero_entity_graph); - graph_diag.zero_entities.insert( - graph_diag.zero_entities.end(), - zero_entity_graph.zero_entities.begin(), - zero_entity_graph.zero_entities.end()); - } // Persist only level sets actively changing sign in this parent cell. - // Non-active level sets may still touch a vertex/face, but they are not - // curving constraints for this cut-cell. + // Non-active level sets may still touch a vertex/face, but they do not + // change sign in this cut-cell. for (auto& ls_cell : intersected_ls_cells) hc.level_set_cells.push_back(std::move(ls_cell)); hc.ls_offsets.push_back( static_cast(hc.level_set_cells.size())); hc.active_level_set_mask.push_back(cell_active_mask); - hc.graph_diagnostics.push_back(graph_diag); hc.adapt_cells.push_back(std::move(ac)); hc.parent_cell_ids.push_back(ci); @@ -721,37 +644,33 @@ HOMeshPart select_part(const HOCutCells& cut_cells, // cut() single LS template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool, - const ReadyCellGraphOptions&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool, - const ReadyCellGraphOptions&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool, - const ReadyCellGraphOptions&); +cut(const MeshView&, const LevelSetFunction&, bool); template std::pair, BackgroundMeshData> -cut(const MeshView&, const LevelSetFunction&, bool, - const ReadyCellGraphOptions&); +cut(const MeshView&, const LevelSetFunction&, bool); // cut() multi LS template std::pair, BackgroundMeshData> cut(const MeshView&, const std::vector>&, - bool, const ReadyCellGraphOptions&); + bool); template std::pair, BackgroundMeshData> cut(const MeshView&, const std::vector>&, - bool, const ReadyCellGraphOptions&); + bool); template std::pair, BackgroundMeshData> cut(const MeshView&, const std::vector>&, - bool, const ReadyCellGraphOptions&); + bool); template std::pair, BackgroundMeshData> cut(const MeshView&, const std::vector>&, - bool, const ReadyCellGraphOptions&); + bool); // select_part() template HOMeshPart diff --git a/cpp/src/ho_cut_mesh.h b/cpp/src/ho_cut_mesh.h index 23337b2..bdfdcc7 100644 --- a/cpp/src/ho_cut_mesh.h +++ b/cpp/src/ho_cut_mesh.h @@ -14,7 +14,6 @@ #include "adapt_cell.h" #include "cell_flags.h" #include "cell_certification.h" -#include "curving.h" #include "level_set.h" #include "level_set_cell.h" #include "mesh_view.h" @@ -90,14 +89,6 @@ struct HOCutCells /// Size = num_cut_cells. std::vector active_level_set_mask; - /// Aggregated graph preflight diagnostics for each cut cell. These are - /// collected before the committed cut decomposition is built. - std::vector> graph_diagnostics; - - /// Central curved zero-entity identity/cache data. - /// Mutable because HOMeshPart is a const view but curving is a lazy cache. - mutable curving::CurvingData curving; - /// Number of intersected cells. int num_cut_cells() const { @@ -153,8 +144,7 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const LevelSetFunction& ls, - bool triangulate_cut_parts = false, - const ReadyCellGraphOptions& graph_options = {}); + bool triangulate_cut_parts = false); /// Build HOCutCells and BackgroundMeshData from a mesh and multiple level sets. /// @@ -165,8 +155,7 @@ template std::pair, BackgroundMeshData> cut(const MeshView& mesh, const std::vector>& level_sets, - bool triangulate_cut_parts = true, - const ReadyCellGraphOptions& graph_options = {}); + bool triangulate_cut_parts = true); // ===================================================================== // select_part() — builds HOMeshPart diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index 813cded..385ed2b 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -5,25 +5,18 @@ #include "ho_mesh_part_output.h" +#include "cell_topology.h" #include "mapping.h" #include "quadrature_tables.h" #include "reference_cell.h" -#include "cell_topology.h" -#include "quad_midpoint_split.h" -#include "prism_midpoint_split.h" #include "triangulation.h" -#include "write_vtk.h" #include #include -#include #include -#include #include -#include #include #include -#include #include namespace cutcells::output @@ -31,3216 +24,545 @@ namespace cutcells::output namespace { -struct SelectedEntity -{ - cell::type type = cell::type::point; - std::vector vertices; - int zero_entity_index = -1; -}; - -constexpr int vtk_lagrange_curve = 68; -constexpr int vtk_lagrange_triangle = 69; -constexpr int vtk_lagrange_quadrilateral = 70; -constexpr int vtk_lagrange_tetrahedron = 71; -constexpr int vtk_lagrange_wedge = 73; - -template -struct CurvedBoundaryEdge -{ - std::array local_vertices = {-1, -1}; - std::array local_global_vertices = {-1, -1}; - std::array zero_vertices = {-1, -1}; - const curving::CurvedZeroEntityState* state = nullptr; -}; - -template -struct CurvedBoundaryFace -{ - std::array local_vertices = {-1, -1, -1, -1}; - int num_local_vertices = 0; - std::array zero_vertices = {-1, -1, -1, -1}; - int num_zero_vertices = 0; - cell::type zero_face_type = cell::type::triangle; - const curving::CurvedZeroEntityState* state = nullptr; -}; - -template -struct LocalCurvedSimplexMap -{ - cell::type simplex_type = cell::type::point; - int dim = 0; - int parent_tdim = 0; - int gdim = 0; - cell::type parent_cell_type = cell::type::point; - int geometry_order = 1; - curving::NodeFamily node_family = curving::NodeFamily::lagrange; - std::vector vertices; - std::vector ref_vertex_coords; - std::vector parent_physical_coords; - std::vector> curved_edges; - std::vector> curved_faces; -}; - -inline bool is_simplex(cell::type cell_type) +template +std::vector parent_cell_vertex_coords_vtk(const MeshView& mesh, + I cell_id) { - return cell_type == cell::type::interval - || cell_type == cell::type::triangle - || cell_type == cell::type::tetrahedron; -} + const auto ctype = mesh.cell_type(cell_id); + const int nv = cell::get_num_vertices(ctype); + std::vector coords(static_cast(nv * mesh.gdim), T(0)); -inline cell::type simplex_type_for_dim(int dim) -{ - switch (dim) + for (int vtk_v = 0; vtk_v < nv; ++vtk_v) { - case 1: - return cell::type::interval; - case 2: - return cell::type::triangle; - case 3: - return cell::type::tetrahedron; - default: - throw std::runtime_error("Unsupported simplex dimension"); + const int local_v = mesh.vtk_vertex_order + ? vtk_v + : cell::vtk_to_basix_vertex(ctype, vtk_v); + const I node_id = mesh.cell_node(cell_id, static_cast(local_v)); + const T* x = mesh.node(node_id); + for (int d = 0; d < mesh.gdim; ++d) + coords[static_cast(vtk_v * mesh.gdim + d)] = x[d]; } + return coords; } -inline bool supports_curved_lagrange_cell(cell::type cell_type) +inline bool is_simplex(cell::type cell_type) { - return is_simplex(cell_type) - || cell_type == cell::type::quadrilateral - || cell_type == cell::type::prism; + using cell::type; + return cell_type == type::point + || cell_type == type::interval + || cell_type == type::triangle + || cell_type == type::tetrahedron; } -template -std::pair legendre_value_and_derivative(int order, T x) +inline cell::type simplex_type_for_dim(int dim) { - if (order == 0) - return {T(1), T(0)}; - - T pm2 = T(1); - T pm1 = x; - for (int n = 2; n <= order; ++n) + using cell::type; + switch (dim) { - const T p = ((T(2 * n - 1) * x * pm1) - T(n - 1) * pm2) / T(n); - pm2 = pm1; - pm1 = p; + case 0: + return type::point; + case 1: + return type::interval; + case 2: + return type::triangle; + case 3: + return type::tetrahedron; + default: + throw std::runtime_error("Unsupported simplex dimension"); } - - const T denom = T(1) - x * x; - if (std::abs(denom) <= T(64) * std::numeric_limits::epsilon()) - return {pm1, T(0)}; - const T derivative = T(order) * (pm2 - x * pm1) / denom; - return {pm1, derivative}; } -template -std::vector gll_parameters(int order) +bool cell_contains_all_vertices(std::span cell_verts, + std::span entity_verts) { - order = std::max(order, 1); - std::vector params(static_cast(order + 1), T(0)); - params.front() = T(0); - params.back() = T(1); - if (order == 1) - return params; - - const T pi = std::acos(T(-1)); - const T eps = T(128) * std::numeric_limits::epsilon(); - for (int i = 1; i < order; ++i) + for (const int v : entity_verts) { - T x = -std::cos(pi * T(i) / T(order)); - for (int iter = 0; iter < 32; ++iter) + bool found = false; + for (const auto cv : cell_verts) { - const auto [p, dp] = legendre_value_and_derivative(order, x); - const T denom = T(1) - x * x; - if (std::abs(denom) <= eps) - break; - const T d2p = (T(2) * x * dp - T(order * (order + 1)) * p) / denom; - if (std::abs(d2p) <= eps) - break; - const T step = dp / d2p; - x -= step; - x = std::clamp(x, -T(1) + eps, T(1) - eps); - if (std::abs(step) <= eps) + if (cv == v) + { + found = true; break; + } } - params[static_cast(i)] = T(0.5) * (x + T(1)); + if (!found) + return false; } - return params; + return true; } template -std::vector interpolation_parameters(int order, curving::NodeFamily family) +bool vertex_state_for_level_set(const AdaptCell& ac, + int vertex_id, + int level_set_id, + bool& is_negative, + bool& is_positive, + bool& is_zero) { - order = std::max(order, 1); - std::vector params(static_cast(order + 1), T(0)); - if (family == curving::NodeFamily::gll) - return gll_parameters(order); - - for (int i = 0; i <= order; ++i) - params[static_cast(i)] = T(i) / T(order); - return params; -} + if (level_set_id < 0 || level_set_id >= 64) + return false; -inline curving::NodeFamily construction_node_family(int geometry_order, - curving::NodeFamily output_family) -{ - (void)geometry_order; - return output_family; + const std::uint64_t bit = std::uint64_t(1) << level_set_id; + const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; + const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; + is_zero = (zm & bit) != 0; + is_negative = !is_zero && ((nm & bit) != 0); + is_positive = !is_zero && !is_negative; + return true; } -template -std::vector> lagrange_simplex_nodes_basix(cell::type simplex_type, int order) +template +bool leaf_cell_matches_sign_requirements( + const AdaptCell& ac, + std::span cell_verts, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) { - order = std::max(order, 1); - std::vector> nodes; - if (simplex_type == cell::type::interval) + const int nls = std::min(bg.num_level_sets, 64); + for (int li = 0; li < nls; ++li) { - nodes.push_back({T(0)}); - nodes.push_back({T(1)}); - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order)}); - return nodes; - } + const std::uint64_t bit = std::uint64_t(1) << li; + const bool require_neg = (expr.negative_required & bit) != 0; + const bool require_pos = (expr.positive_required & bit) != 0; + if (!require_neg && !require_pos) + continue; - if (simplex_type == cell::type::triangle) - { - nodes.push_back({T(0), T(0)}); - nodes.push_back({T(1), T(0)}); - nodes.push_back({T(0), T(1)}); - const auto edges = cell::edges(cell::type::triangle); - const auto verts = cell::reference_vertices(cell::type::triangle); - for (const auto& edge : edges) + const bool ls_is_active = (cut_cell_active_mask & bit) != 0; + if (!ls_is_active) { - for (int i = 1; i < order; ++i) - { - const T s = T(i) / T(order); - std::vector x(2, T(0)); - for (int d = 0; d < 2; ++d) - { - const T x0 = verts[static_cast(edge[0] * 2 + d)]; - const T x1 = verts[static_cast(edge[1] * 2 + d)]; - x[static_cast(d)] = (T(1) - s) * x0 + s * x1; - } - nodes.push_back(std::move(x)); - } + const auto dom = bg.domain(li, parent_cell_id); + if (require_neg && dom != cell::domain::inside) + return false; + if (require_pos && dom != cell::domain::outside) + return false; + continue; } - for (int j = 1; j < order; ++j) - for (int i = 1; i < order - j; ++i) - nodes.push_back({T(i) / T(order), T(j) / T(order)}); - return nodes; - } - if (simplex_type == cell::type::tetrahedron) - { - nodes.push_back({T(0), T(0), T(0)}); - nodes.push_back({T(1), T(0), T(0)}); - nodes.push_back({T(0), T(1), T(0)}); - nodes.push_back({T(0), T(0), T(1)}); - const auto edges = cell::edges(cell::type::tetrahedron); - const auto verts = cell::reference_vertices(cell::type::tetrahedron); - for (const auto& edge : edges) - { - for (int i = 1; i < order; ++i) - { - const T s = T(i) / T(order); - std::vector x(3, T(0)); - for (int d = 0; d < 3; ++d) - { - const T x0 = verts[static_cast(edge[0] * 3 + d)]; - const T x1 = verts[static_cast(edge[1] * 3 + d)]; - x[static_cast(d)] = (T(1) - s) * x0 + s * x1; - } - nodes.push_back(std::move(x)); - } - } - for (int f = 0; f < 4; ++f) + bool has_neg = false; + bool has_pos = false; + for (const auto cv : cell_verts) { - const auto fv = cell::face_vertices(cell::type::tetrahedron, f); - for (int j = 1; j < order; ++j) - { - for (int i = 1; i < order - j; ++i) - { - const T w0 = T(1) - T(i + j) / T(order); - const T w1 = T(i) / T(order); - const T w2 = T(j) / T(order); - std::vector x(3, T(0)); - for (int d = 0; d < 3; ++d) - { - x[static_cast(d)] = - w0 * verts[static_cast(fv[0] * 3 + d)] - + w1 * verts[static_cast(fv[1] * 3 + d)] - + w2 * verts[static_cast(fv[2] * 3 + d)]; - } - nodes.push_back(std::move(x)); - } - } + bool is_neg = false; + bool is_pos = false; + bool is_zero = false; + vertex_state_for_level_set( + ac, static_cast(cv), li, is_neg, is_pos, is_zero); + has_neg = has_neg || is_neg; + has_pos = has_pos || is_pos; } - for (int k = 1; k < order; ++k) - for (int j = 1; j < order - k; ++j) - for (int i = 1; i < order - j - k; ++i) - nodes.push_back({T(i) / T(order), T(j) / T(order), T(k) / T(order)}); - return nodes; - } - - throw std::runtime_error("lagrange_simplex_nodes_basix: unsupported cell type"); -} - -template -std::vector> lagrange_quadrilateral_nodes_vtk(int order) -{ - order = std::max(order, 1); - std::vector> nodes; - nodes.reserve(static_cast((order + 1) * (order + 1))); - - nodes.push_back({T(0), T(0)}); - nodes.push_back({T(1), T(0)}); - nodes.push_back({T(1), T(1)}); - nodes.push_back({T(0), T(1)}); - - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order), T(0)}); - for (int j = 1; j < order; ++j) - nodes.push_back({T(1), T(j) / T(order)}); - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order), T(1)}); - for (int j = 1; j < order; ++j) - nodes.push_back({T(0), T(j) / T(order)}); - - for (int j = 1; j < order; ++j) - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order), T(j) / T(order)}); - - return nodes; -} -template -std::vector> lagrange_prism_nodes_vtk(int order) -{ - order = std::max(order, 1); - const int tri_nodes = (order + 1) * (order + 2) / 2; - std::vector> nodes; - nodes.reserve(static_cast(tri_nodes * (order + 1))); - - nodes.push_back({T(0), T(0), T(0)}); - nodes.push_back({T(1), T(0), T(0)}); - nodes.push_back({T(0), T(1), T(0)}); - nodes.push_back({T(0), T(0), T(1)}); - nodes.push_back({T(1), T(0), T(1)}); - nodes.push_back({T(0), T(1), T(1)}); - - auto append_triangle_edges = [&](T z) - { - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order), T(0), z}); - for (int r = 1; r < order; ++r) - nodes.push_back({T(order - r) / T(order), T(r) / T(order), z}); - for (int r = 1; r < order; ++r) - nodes.push_back({T(0), T(order - r) / T(order), z}); - }; - - append_triangle_edges(T(0)); - append_triangle_edges(T(1)); - - for (const auto base : {std::array{T(0), T(0)}, - std::array{T(1), T(0)}, - std::array{T(0), T(1)}}) - { - for (int k = 1; k < order; ++k) - nodes.push_back({base[0], base[1], T(k) / T(order)}); + if (require_neg && (has_pos || !has_neg)) + return false; + if (require_pos && (has_neg || !has_pos)) + return false; } - auto append_triangle_interior = [&](T z) - { - for (int j = 1; j < order; ++j) - for (int i = 1; i < order - j; ++i) - nodes.push_back({T(i) / T(order), T(j) / T(order), z}); - }; - - append_triangle_interior(T(0)); - append_triangle_interior(T(1)); - - for (int k = 1; k < order; ++k) - for (int i = 1; i < order; ++i) - nodes.push_back({T(i) / T(order), T(0), T(k) / T(order)}); - for (int k = 1; k < order; ++k) - for (int r = 1; r < order; ++r) - nodes.push_back({T(order - r) / T(order), T(r) / T(order), T(k) / T(order)}); - for (int k = 1; k < order; ++k) - for (int r = 1; r < order; ++r) - nodes.push_back({T(0), T(order - r) / T(order), T(k) / T(order)}); - - for (int k = 1; k < order; ++k) - for (int j = 1; j < order; ++j) - for (int i = 1; i < order - j; ++i) - nodes.push_back({T(i) / T(order), T(j) / T(order), T(k) / T(order)}); - - return nodes; -} - -template -std::vector> lagrange_cell_nodes(cell::type cell_type, int order) -{ - if (is_simplex(cell_type)) - return lagrange_simplex_nodes_basix(cell_type, order); - if (cell_type == cell::type::quadrilateral) - return lagrange_quadrilateral_nodes_vtk(order); - if (cell_type == cell::type::prism) - return lagrange_prism_nodes_vtk(order); - throw std::runtime_error("lagrange_cell_nodes: unsupported cell type"); + return true; } template -std::vector parent_cell_vertex_coords_vtk(const MeshView& mesh, I cell_id) +bool zero_entity_matches( + const AdaptCell& ac, + int zero_entity_index, + int target_dim, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) { - const auto ctype = mesh.cell_type(cell_id); - const int nv = cell::get_num_vertices(ctype); - std::vector coords(static_cast(nv * mesh.gdim), T(0)); - - for (int vtk_v = 0; vtk_v < nv; ++vtk_v) + if (ac.zero_entity_dim[static_cast(zero_entity_index)] + != target_dim) { - const int local_v = mesh.vtk_vertex_order - ? vtk_v - : cell::vtk_to_basix_vertex(ctype, vtk_v); - const I node_id = mesh.cell_node(cell_id, static_cast(local_v)); - const T* x = mesh.node(node_id); - for (int d = 0; d < mesh.gdim; ++d) - coords[static_cast(vtk_v * mesh.gdim + d)] = x[d]; + return false; } - return coords; -} - -template -std::vector barycentric_from_simplex_point(cell::type simplex_type, - std::span xi) -{ - if (simplex_type == cell::type::interval) - return {T(1) - xi[0], xi[0]}; - if (simplex_type == cell::type::triangle) - return {T(1) - xi[0] - xi[1], xi[0], xi[1]}; - if (simplex_type == cell::type::tetrahedron) - return {T(1) - xi[0] - xi[1] - xi[2], xi[0], xi[1], xi[2]}; - throw std::runtime_error("barycentric_from_simplex_point: unsupported simplex"); -} + const auto zero_mask = + ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; + if ((zero_mask & expr.zero_required) != expr.zero_required) + return false; -template -std::vector cell_vertex_shape_weights(cell::type cell_type, - std::span xi) -{ - if (cell_type == cell::type::interval) - return {T(1) - xi[0], xi[0]}; - if (cell_type == cell::type::triangle) - return {T(1) - xi[0] - xi[1], xi[0], xi[1]}; - if (cell_type == cell::type::quadrilateral) - { - const T u = xi[0]; - const T v = xi[1]; - return {(T(1) - u) * (T(1) - v), - u * (T(1) - v), - (T(1) - u) * v, - u * v}; - } - if (cell_type == cell::type::tetrahedron) - return {T(1) - xi[0] - xi[1] - xi[2], xi[0], xi[1], xi[2]}; - if (cell_type == cell::type::prism) - { - const T u = xi[0]; - const T v = xi[1]; - const T z = xi[2]; - const T w0 = T(1) - u - v; - return {w0 * (T(1) - z), - u * (T(1) - z), - v * (T(1) - z), - w0 * z, - u * z, - v * z}; - } - throw std::runtime_error("cell_vertex_shape_weights: unsupported cell type"); -} + if (expr.negative_required == 0 && expr.positive_required == 0) + return true; -template -std::vector cell_vertex_shape_gradients(cell::type cell_type, - std::span xi) -{ - if (cell_type == cell::type::interval) - return {T(-1), T(1)}; - if (cell_type == cell::type::triangle) - return {T(-1), T(-1), - T(1), T(0), - T(0), T(1)}; - if (cell_type == cell::type::quadrilateral) + std::vector zero_verts; + const int zdim = ac.zero_entity_dim[static_cast(zero_entity_index)]; + const int zid = ac.zero_entity_id[static_cast(zero_entity_index)]; + if (zdim == 0) { - const T u = xi[0]; - const T v = xi[1]; - return {-(T(1) - v), -(T(1) - u), - (T(1) - v), -u, - -v, T(1) - u, - v, u}; + zero_verts.push_back(zid); } - if (cell_type == cell::type::tetrahedron) - return {T(-1), T(-1), T(-1), - T(1), T(0), T(0), - T(0), T(1), T(0), - T(0), T(0), T(1)}; - if (cell_type == cell::type::prism) + else { - const T u = xi[0]; - const T v = xi[1]; - const T z = xi[2]; - const T w0 = T(1) - u - v; - return {-(T(1) - z), -(T(1) - z), -w0, - (T(1) - z), T(0), -u, - T(0), (T(1) - z), -v, - -z, -z, w0, - z, T(0), u, - T(0), z, v}; + auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; + zero_verts.reserve(verts.size()); + for (const auto v : verts) + zero_verts.push_back(static_cast(v)); } - throw std::runtime_error("cell_vertex_shape_gradients: unsupported cell type"); -} -template -std::vector affine_ref_from_bary(const LocalCurvedSimplexMap& map, - std::span bary) -{ - std::vector x(static_cast(map.parent_tdim), T(0)); - for (std::size_t v = 0; v < bary.size(); ++v) + const int tdim = ac.tdim; + const int n_cells = ac.n_entities(tdim); + for (int c = 0; c < n_cells; ++c) { - for (int d = 0; d < map.parent_tdim; ++d) + auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; + if (!cell_contains_all_vertices( + cell_verts, + std::span(zero_verts.data(), zero_verts.size()))) { - x[static_cast(d)] += bary[v] * map.ref_vertex_coords[ - v * static_cast(map.parent_tdim) + static_cast(d)]; + continue; } - } - return x; -} -template -std::vector straight_ref_from_cell_point(const LocalCurvedSimplexMap& map, - std::span xi) -{ - const auto weights = cell_vertex_shape_weights(map.simplex_type, xi); - std::vector x(static_cast(map.parent_tdim), T(0)); - for (std::size_t v = 0; v < weights.size(); ++v) - { - for (int d = 0; d < map.parent_tdim; ++d) + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, expr, cut_cell_active_mask, bg, parent_cell_id)) { - x[static_cast(d)] += weights[v] * map.ref_vertex_coords[ - v * static_cast(map.parent_tdim) + static_cast(d)]; + return true; } } - return x; -} -template -std::vector straight_ref_jacobian_from_cell_point( - const LocalCurvedSimplexMap& map, - std::span xi) -{ - const auto gradients = cell_vertex_shape_gradients(map.simplex_type, xi); - const int nv = cell::get_num_vertices(map.simplex_type); - std::vector J(static_cast(map.dim * map.parent_tdim), T(0)); - for (int c = 0; c < map.dim; ++c) - { - for (int v = 0; v < nv; ++v) - { - const T dNv = gradients[static_cast(v * map.dim + c)]; - for (int d = 0; d < map.parent_tdim; ++d) - { - J[static_cast(c * map.parent_tdim + d)] += - dNv * map.ref_vertex_coords[ - static_cast(v * map.parent_tdim + d)]; - } - } - } - return J; + return false; } -template -std::vector push_parent_ref_to_physical(const LocalCurvedSimplexMap& map, - std::span ref_point) +struct SelectedEntity { - const auto phys = cell::push_forward_affine_map( - map.parent_cell_type, - map.parent_physical_coords, - map.gdim, - ref_point); - return phys; -} + int local_zero_entity_id = -1; + cell::type type = cell::type::point; + std::vector vertices; +}; -template -std::vector parent_ref_to_physical_jacobian( - const LocalCurvedSimplexMap& map) +template +std::vector selected_entities(const HOMeshPart& part, + const AdaptCell& adapt_cell, + int cut_cell_id) { - const auto cols = cell::jacobian_col_indices(map.parent_cell_type); - const T* x0 = map.parent_physical_coords.data(); - std::vector J( - static_cast(map.parent_tdim * map.gdim), T(0)); - for (int c = 0; c < map.parent_tdim; ++c) - { - const T* xc = - map.parent_physical_coords.data() - + static_cast(cols[c] * map.gdim); - for (int r = 0; r < map.gdim; ++r) - J[static_cast(c * map.gdim + r)] = xc[r] - x0[r]; - } - return J; -} + const I parent_cell_id = + part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]; + const std::uint64_t cut_active_mask = + part.cut_cells->active_level_set_mask[static_cast(cut_cell_id)]; -template -std::vector compose_ref_to_physical_jacobian( - const LocalCurvedSimplexMap& map, - std::span ref_jacobian) -{ - const auto parent_J = parent_ref_to_physical_jacobian(map); - std::vector J(static_cast(map.dim * map.gdim), T(0)); - for (int c = 0; c < map.dim; ++c) + std::vector entities; + if (part.dim == adapt_cell.tdim) { - for (int p = 0; p < map.parent_tdim; ++p) + const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); + entities.reserve(static_cast(n_cells)); + for (int c = 0; c < n_cells; ++c) { - const T dref = - ref_jacobian[static_cast(c * map.parent_tdim + p)]; - for (int r = 0; r < map.gdim; ++r) + auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][ + static_cast(c)]; + if (!leaf_cell_matches_sign_requirements( + adapt_cell, verts, part.expr, cut_active_mask, + *part.bg, parent_cell_id)) { - J[static_cast(c * map.gdim + r)] += - dref * parent_J[static_cast(p * map.gdim + r)]; + continue; } - } - } - return J; -} -template -T measure_from_jacobian(int dim, int gdim, std::span J) -{ - if (dim == 1) - { - T n2 = T(0); - for (int r = 0; r < gdim; ++r) - n2 += J[static_cast(r)] - * J[static_cast(r)]; - return std::sqrt(n2); + SelectedEntity entity; + entity.type = adapt_cell.entity_types[adapt_cell.tdim][ + static_cast(c)]; + entity.vertices.assign(verts.begin(), verts.end()); + entities.push_back(std::move(entity)); + } + return entities; } - if (dim == gdim) + const int n_zero = adapt_cell.n_zero_entities(); + entities.reserve(static_cast(n_zero)); + for (int z = 0; z < n_zero; ++z) { - if (dim == 2) - return J[0] * J[3] - J[2] * J[1]; - if (dim == 3) + if (!zero_entity_matches( + adapt_cell, z, part.dim, part.expr, cut_active_mask, + *part.bg, parent_cell_id)) { - return J[0] * (J[4] * J[8] - J[7] * J[5]) - - J[3] * (J[1] * J[8] - J[7] * J[2]) - + J[6] * (J[1] * J[5] - J[4] * J[2]); + continue; } - } - T G[9] = {}; - for (int i = 0; i < dim; ++i) - { - for (int j = 0; j < dim; ++j) - { - for (int r = 0; r < gdim; ++r) - { - G[i * dim + j] += J[static_cast(i * gdim + r)] - * J[static_cast(j * gdim + r)]; - } - } - } - if (dim == 2) - return std::sqrt(std::max(T(0), G[0] * G[3] - G[1] * G[2])); - return T(0); -} - -inline bool same_unordered(std::span a, std::span b) -{ - if (a.size() != b.size()) - return false; - for (const int av : a) - { - bool found = false; - for (const int bv : b) - found = found || (av == bv); - if (!found) - return false; - } - return true; -} - -template -std::vector zero_entity_vertex_ids(const AdaptCell& ac, int local_zero_entity_id) -{ - const int zdim = ac.zero_entity_dim[static_cast(local_zero_entity_id)]; - const int zid = ac.zero_entity_id[static_cast(local_zero_entity_id)]; - if (zdim == 0) - return {zid}; - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - std::vector out; - out.reserve(verts.size()); - for (const auto v : verts) - out.push_back(static_cast(v)); - return out; -} - -template -T lagrange_basis_1d(int i, std::span params, T x) -{ - T value = T(1); - const T xi = params[static_cast(i)]; - for (int j = 0; j < static_cast(params.size()); ++j) - { - if (j == i) - continue; - value *= (x - params[static_cast(j)]) - / (xi - params[static_cast(j)]); - } - return value; -} - -template -T lagrange_basis_1d_derivative(int i, std::span params, T x) -{ - const T xi = params[static_cast(i)]; - T derivative = T(0); - for (int m = 0; m < static_cast(params.size()); ++m) - { - if (m == i) - continue; - T term = T(1) / (xi - params[static_cast(m)]); - for (int j = 0; j < static_cast(params.size()); ++j) - { - if (j == i || j == m) - continue; - term *= (x - params[static_cast(j)]) - / (xi - params[static_cast(j)]); - } - derivative += term; - } - return derivative; -} - -template -T warp_factor(int order, T r) -{ - if (order <= 1) - return T(0); - - std::vector equispaced(static_cast(order + 1), T(0)); - std::vector gll = gll_parameters(order); - for (int i = 0; i <= order; ++i) - { - equispaced[static_cast(i)] = -T(1) + T(2 * i) / T(order); - gll[static_cast(i)] = T(2) * gll[static_cast(i)] - T(1); - } - - T warp = T(0); - for (int i = 0; i <= order; ++i) - { - const T Li = lagrange_basis_1d( - i, std::span(equispaced.data(), equispaced.size()), r); - warp += Li * (gll[static_cast(i)] - - equispaced[static_cast(i)]); - } - - const T edge_factor = T(1) - r * r; - if (std::abs(edge_factor) > T(64) * std::numeric_limits::epsilon()) - warp /= edge_factor; - return warp; -} - -template -std::array equilateral_to_reference_barycentric(T x, T y) -{ - const T sqrt3 = std::sqrt(T(3)); - std::array w = {}; - w[2] = (sqrt3 * y + T(1)) / T(3); - w[1] = (x + T(1) - w[2]) / T(2); - w[0] = T(1) - w[1] - w[2]; - - T sum = T(0); - for (T& wi : w) - { - if (std::abs(wi) < T(256) * std::numeric_limits::epsilon()) - wi = T(0); - if (std::abs(wi - T(1)) < T(256) * std::numeric_limits::epsilon()) - wi = T(1); - wi = std::clamp(wi, T(0), T(1)); - sum += wi; - } - if (sum > T(0)) - for (T& wi : w) - wi /= sum; - return w; -} - -template -std::vector> triangle_interpolation_barycentric_nodes( - int order, - curving::NodeFamily family) -{ - order = std::max(order, 1); - std::vector> nodes; - nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); + const int zdim = adapt_cell.zero_entity_dim[static_cast(z)]; + const int zid = adapt_cell.zero_entity_id[static_cast(z)]; - if (family != curving::NodeFamily::gll) - { - for (int j = 0; j <= order; ++j) + SelectedEntity entity; + entity.local_zero_entity_id = z; + if (zdim == 0) { - for (int i = 0; i <= order - j; ++i) - { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - nodes.push_back({T(1) - u - v, u, v}); - } + entity.type = cell::type::point; + entity.vertices.push_back(zid); } - return nodes; - } - - constexpr std::array alpha_opt = { - T(0.0), T(0.0), T(1.4152), T(0.1001), - T(0.2751), T(0.9800), T(1.0999), T(1.2832), - T(1.3648), T(1.4773), T(1.4959), T(1.5743), - T(1.5770), T(1.6223), T(1.6258), T(1.6530)}; - const T alpha = (order < static_cast(alpha_opt.size())) - ? alpha_opt[static_cast(order)] - : T(5) / T(3); - const T sqrt3 = std::sqrt(T(3)); - const T cos120 = -T(0.5); - const T sin120 = sqrt3 / T(2); - const T cos240 = -T(0.5); - const T sin240 = -sqrt3 / T(2); - - for (int j = 0; j <= order; ++j) - { - for (int i = 0; i <= order - j; ++i) + else { - const T u = T(i) / T(order); - const T v = T(j) / T(order); - const T lambda0 = T(1) - u - v; - const T lambda1 = u; - const T lambda2 = v; - - T x = -lambda0 + lambda1; - T y = (-lambda0 - lambda1 + T(2) * lambda2) / sqrt3; - - const T L1 = lambda2; - const T L2 = lambda0; - const T L3 = lambda1; - const T warp1 = T(4) * L2 * L3 * warp_factor(order, L3 - L2) - * (T(1) + (alpha * L1) * (alpha * L1)); - const T warp2 = T(4) * L1 * L3 * warp_factor(order, L1 - L3) - * (T(1) + (alpha * L2) * (alpha * L2)); - const T warp3 = T(4) * L1 * L2 * warp_factor(order, L2 - L1) - * (T(1) + (alpha * L3) * (alpha * L3)); - - x += warp1 + cos120 * warp2 + cos240 * warp3; - y += sin120 * warp2 + sin240 * warp3; - nodes.push_back(equilateral_to_reference_barycentric(x, y)); + entity.type = adapt_cell.entity_types[zdim][static_cast(zid)]; + auto verts = adapt_cell.entity_to_vertex[zdim][static_cast(zid)]; + entity.vertices.assign(verts.begin(), verts.end()); } + entities.push_back(std::move(entity)); } - return nodes; + return entities; } template -std::vector triangle_monomials(int order, std::span bary) +std::vector entity_reference_coords(const AdaptCell& adapt_cell, + std::span entity_vertices) { - const T u = bary[1]; - const T v = bary[2]; - std::vector values; - values.reserve(static_cast((order + 1) * (order + 2) / 2)); - for (int total = 0; total <= order; ++total) - { - for (int j = 0; j <= total; ++j) - { - const int i = total - j; - values.push_back(std::pow(u, i) * std::pow(v, j)); - } - } - return values; -} + std::vector coords( + static_cast(entity_vertices.size() * adapt_cell.tdim), T(0)); -template -std::pair, std::vector> triangle_monomial_gradients( - int order, - std::span bary) -{ - const T u = bary[1]; - const T v = bary[2]; - std::vector du; - std::vector dv; - du.reserve(static_cast((order + 1) * (order + 2) / 2)); - dv.reserve(static_cast((order + 1) * (order + 2) / 2)); - for (int total = 0; total <= order; ++total) + for (std::size_t j = 0; j < entity_vertices.size(); ++j) { - for (int j = 0; j <= total; ++j) + const int gv = entity_vertices[j]; + for (int d = 0; d < adapt_cell.tdim; ++d) { - const int i = total - j; - du.push_back(i == 0 ? T(0) - : T(i) * std::pow(u, i - 1) * std::pow(v, j)); - dv.push_back(j == 0 ? T(0) - : T(j) * std::pow(u, i) * std::pow(v, j - 1)); + coords[static_cast(j * adapt_cell.tdim + d)] = + adapt_cell.vertex_coords[static_cast( + gv * adapt_cell.tdim + d)]; } } - return {std::move(du), std::move(dv)}; + + return coords; } template -bool solve_dense(std::vector A, std::vector b, int n, std::vector& x) +void gather_subcell_vertices(std::span coords, + int coord_dim, + std::span vertex_ids, + std::vector& out) { - x.assign(static_cast(n), T(0)); - for (int k = 0; k < n; ++k) + out.resize(static_cast(vertex_ids.size() * coord_dim)); + for (std::size_t j = 0; j < vertex_ids.size(); ++j) { - int pivot = k; - T best = std::abs(A[static_cast(k * n + k)]); - for (int r = k + 1; r < n; ++r) - { - const T value = std::abs(A[static_cast(r * n + k)]); - if (value > best) - { - best = value; - pivot = r; - } - } - if (best <= T(256) * std::numeric_limits::epsilon()) - return false; - if (pivot != k) - { - for (int c = k; c < n; ++c) - std::swap(A[static_cast(k * n + c)], - A[static_cast(pivot * n + c)]); - std::swap(b[static_cast(k)], b[static_cast(pivot)]); - } - - const T diag = A[static_cast(k * n + k)]; - for (int c = k; c < n; ++c) - A[static_cast(k * n + c)] /= diag; - b[static_cast(k)] /= diag; - - for (int r = 0; r < n; ++r) + const int local_v = vertex_ids[j]; + for (int d = 0; d < coord_dim; ++d) { - if (r == k) - continue; - const T factor = A[static_cast(r * n + k)]; - if (factor == T(0)) - continue; - for (int c = k; c < n; ++c) - A[static_cast(r * n + c)] -= - factor * A[static_cast(k * n + c)]; - b[static_cast(r)] -= factor * b[static_cast(k)]; + out[static_cast(j * coord_dim + d)] = + coords[static_cast(local_v * coord_dim + d)]; } } - x = std::move(b); - return true; -} - -template -struct TriangleLagrangeBasisData -{ - std::vector values; - std::vector du; - std::vector dv; -}; - -template -TriangleLagrangeBasisData triangle_lagrange_basis_data( - int order, - curving::NodeFamily node_family, - std::span bary) -{ - const auto nodes = - triangle_interpolation_barycentric_nodes(order, node_family); - const int n = static_cast(nodes.size()); - std::vector matrix(static_cast(n * n), T(0)); - for (int row = 0; row < n; ++row) - { - const auto mono = triangle_monomials( - order, - std::span(nodes[static_cast(row)].data(), 3)); - for (int col = 0; col < n; ++col) - matrix[static_cast(col * n + row)] = - mono[static_cast(col)]; - } - - const auto rhs = triangle_monomials(order, bary); - const auto [rhs_du, rhs_dv] = triangle_monomial_gradients(order, bary); - - TriangleLagrangeBasisData out; - if (!solve_dense(matrix, rhs, n, out.values) - || !solve_dense(matrix, rhs_du, n, out.du) - || !solve_dense(std::move(matrix), rhs_dv, n, out.dv)) - { - throw std::runtime_error("triangle_lagrange_basis: singular interpolation matrix"); - } - return out; -} - -template -std::vector triangle_lagrange_basis(int order, - curving::NodeFamily node_family, - std::span bary) -{ - return triangle_lagrange_basis_data(order, node_family, bary).values; -} - -template -T simplex_lagrange_factor(int alpha, T lambda, int order) -{ - T value = T(1); - for (int r = 0; r < alpha; ++r) - value *= (T(order) * lambda - T(r)) / T(r + 1); - return value; -} - -template -std::vector eval_curved_edge_ref(const CurvedBoundaryEdge& edge, - int order, - curving::NodeFamily family, - T t_local) -{ - const bool same_orientation = - edge.zero_vertices[0] == edge.local_global_vertices[0] - && edge.zero_vertices[1] == edge.local_global_vertices[1]; - const bool reverse_orientation = - edge.zero_vertices[0] == edge.local_global_vertices[1] - && edge.zero_vertices[1] == edge.local_global_vertices[0]; - if (!same_orientation && !reverse_orientation) - throw std::runtime_error("eval_curved_edge_ref: edge orientation mismatch"); - const T t = same_orientation ? t_local : T(1) - t_local; - const auto params = interpolation_parameters(order, family); - const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); - std::vector x(static_cast(tdim), T(0)); - for (int i = 0; i <= order; ++i) - { - const T Li = lagrange_basis_1d(i, std::span(params.data(), params.size()), t); - for (int d = 0; d < tdim; ++d) - x[static_cast(d)] += Li * edge.state->ref_nodes[ - static_cast(i * tdim + d)]; - } - return x; -} - -template -std::vector eval_curved_edge_ref_derivative(const CurvedBoundaryEdge& edge, - int order, - curving::NodeFamily family, - T t_local) -{ - const bool same_orientation = - edge.zero_vertices[0] == edge.local_global_vertices[0] - && edge.zero_vertices[1] == edge.local_global_vertices[1]; - const bool reverse_orientation = - edge.zero_vertices[0] == edge.local_global_vertices[1] - && edge.zero_vertices[1] == edge.local_global_vertices[0]; - if (!same_orientation && !reverse_orientation) - throw std::runtime_error("eval_curved_edge_ref_derivative: edge orientation mismatch"); - const T sign = same_orientation ? T(1) : T(-1); - const T t = same_orientation ? t_local : T(1) - t_local; - const auto params = interpolation_parameters(order, family); - const int tdim = static_cast(edge.state->ref_nodes.size()) / (order + 1); - std::vector dx(static_cast(tdim), T(0)); - for (int i = 0; i <= order; ++i) - { - const T dLi = - sign * lagrange_basis_1d_derivative( - i, std::span(params.data(), params.size()), t); - for (int d = 0; d < tdim; ++d) - dx[static_cast(d)] += dLi * edge.state->ref_nodes[ - static_cast(i * tdim + d)]; - } - return dx; } template -struct CurvedFaceEval -{ - std::vector point; - // Derivatives are stored column-major: du column, then dv column. - std::vector jacobian; -}; - -template -CurvedFaceEval eval_curved_face_ref_with_jacobian( - const CurvedBoundaryFace& face, - int order, - curving::NodeFamily node_family, - std::span coordinates) +void map_canonical_to_subcell_points(const T* canonical_points, + int num_points, + int simplex_dim, + const T* subcell_vertices, + int parent_tdim, + T* out_points) { - const int nodes_per_face = - (face.zero_face_type == cell::type::quadrilateral) - ? (order + 1) * (order + 1) - : (order + 1) * (order + 2) / 2; - const int tdim = static_cast(face.state->ref_nodes.size()) / nodes_per_face; - CurvedFaceEval out; - out.point.assign(static_cast(tdim), T(0)); - out.jacobian.assign(static_cast(2 * tdim), T(0)); - - if (face.zero_face_type == cell::type::quadrilateral) - { - const T u = coordinates[0]; - const T v = coordinates[1]; - const auto params = interpolation_parameters(order, node_family); - int node = 0; - for (int j = 0; j <= order; ++j) - { - const T Lj = lagrange_basis_1d( - j, std::span(params.data(), params.size()), v); - const T dLj = lagrange_basis_1d_derivative( - j, std::span(params.data(), params.size()), v); - for (int i = 0; i <= order; ++i) - { - const T Li = lagrange_basis_1d( - i, std::span(params.data(), params.size()), u); - const T dLi = lagrange_basis_1d_derivative( - i, std::span(params.data(), params.size()), u); - const T L = Li * Lj; - for (int d = 0; d < tdim; ++d) - { - const T node_value = face.state->ref_nodes[ - static_cast(node * tdim + d)]; - out.point[static_cast(d)] += L * node_value; - out.jacobian[static_cast(d)] += dLi * Lj * node_value; - out.jacobian[static_cast(tdim + d)] += Li * dLj * node_value; - } - ++node; - } - } - return out; - } - - const auto basis = triangle_lagrange_basis_data(order, node_family, coordinates); - for (int node = 0; node < static_cast(basis.values.size()); ++node) + const T* v0 = subcell_vertices; + for (int q = 0; q < num_points; ++q) { - const T L = basis.values[static_cast(node)]; - const T dLdu = basis.du[static_cast(node)]; - const T dLdv = basis.dv[static_cast(node)]; - for (int d = 0; d < tdim; ++d) - { - const T node_value = face.state->ref_nodes[ - static_cast(node * tdim + d)]; - out.point[static_cast(d)] += L * node_value; - out.jacobian[static_cast(d)] += dLdu * node_value; - out.jacobian[static_cast(tdim + d)] += dLdv * node_value; - } - } - return out; -} + const T* X = canonical_points + q * simplex_dim; + T* x = out_points + q * parent_tdim; -template -std::vector eval_curved_face_ref(const CurvedBoundaryFace& face, - int order, - curving::NodeFamily node_family, - std::span coordinates) -{ - return eval_curved_face_ref_with_jacobian( - face, order, node_family, coordinates).point; -} + for (int d = 0; d < parent_tdim; ++d) + x[d] = v0[d]; -template -bool edge_is_in_any_curved_face(const CurvedBoundaryEdge& edge, - std::span> faces) -{ - for (const auto& face : faces) - { - bool has0 = false; - bool has1 = false; - for (int i = 0; i < face.num_local_vertices; ++i) + for (int i = 1; i <= simplex_dim; ++i) { - const int fv = face.local_vertices[static_cast(i)]; - has0 = has0 || (fv == edge.local_vertices[0]); - has1 = has1 || (fv == edge.local_vertices[1]); + const T* vi = subcell_vertices + i * parent_tdim; + for (int d = 0; d < parent_tdim; ++d) + x[d] += X[i - 1] * (vi[d] - v0[d]); } - if (has0 && has1) - return true; } - return false; } template -struct CurvedMapRefEval -{ - std::vector point; - // Derivatives d(point) / d(xi_c), column-major in local coordinates. - std::vector jacobian; -}; - -template -CurvedMapRefEval curved_map_ref_point_with_jacobian( - const LocalCurvedSimplexMap& map, - std::span xi) +T simplex_physical_measure(const T* vertices, + int simplex_dim, + int gdim) { - const auto vertex_weights = cell_vertex_shape_weights(map.simplex_type, xi); - const auto vertex_gradients = cell_vertex_shape_gradients(map.simplex_type, xi); - CurvedMapRefEval out; - out.point = straight_ref_from_cell_point(map, xi); - out.jacobian = straight_ref_jacobian_from_cell_point(map, xi); - constexpr T eps = T(64) * std::numeric_limits::epsilon(); - - if (map.dim == 1) - { - for (const auto& edge : map.curved_edges) - { - out.point = eval_curved_edge_ref( - edge, map.geometry_order, map.node_family, xi[0]); - const auto dcurved = eval_curved_edge_ref_derivative( - edge, map.geometry_order, map.node_family, xi[0]); - for (int d = 0; d < map.parent_tdim; ++d) - out.jacobian[static_cast(d)] = - dcurved[static_cast(d)]; - } - return out; - } + T J[9] = {}; + const T* v0 = vertices; - for (const auto& face : map.curved_faces) + for (int col = 0; col < simplex_dim; ++col) { - T s = T(0); - std::array local_w = {}; - std::array local_dw = {}; - std::array ds = {}; - for (int i = 0; i < face.num_local_vertices; ++i) - { - const int lv = face.local_vertices[static_cast(i)]; - local_w[static_cast(i)] = - vertex_weights[static_cast(lv)]; - s += local_w[static_cast(i)]; - for (int c = 0; c < map.dim; ++c) - { - const T value = - vertex_gradients[static_cast(lv * map.dim + c)]; - local_dw[static_cast(i * map.dim + c)] = value; - ds[static_cast(c)] += value; - } - } - if (s <= eps) - continue; - - std::array normalized_w = {}; - std::array normalized_dw = {}; - for (int i = 0; i < face.num_local_vertices; ++i) - { - normalized_w[static_cast(i)] = - local_w[static_cast(i)] / s; - for (int c = 0; c < map.dim; ++c) - { - normalized_dw[static_cast(i * map.dim + c)] = - (local_dw[static_cast(i * map.dim + c)] * s - - local_w[static_cast(i)] - * ds[static_cast(c)]) - / (s * s); - } - } - - std::array zero_w = {}; - std::array zero_dw = {}; - std::array zero_uv = {}; - std::array zero_duv = {}; - for (int zi = 0; zi < face.num_zero_vertices; ++zi) - { - for (int li = 0; li < face.num_local_vertices; ++li) - { - if (face.zero_vertices[static_cast(zi)] - == map.vertices[static_cast( - face.local_vertices[static_cast(li)])]) - { - if (face.zero_face_type == cell::type::quadrilateral) - { - const T u = (zi == 1 || zi == 3) ? T(1) : T(0); - const T v = (zi == 2 || zi == 3) ? T(1) : T(0); - zero_uv[0] += normalized_w[static_cast(li)] * u; - zero_uv[1] += normalized_w[static_cast(li)] * v; - for (int c = 0; c < map.dim; ++c) - { - zero_duv[static_cast(c)] += - normalized_dw[static_cast(li * map.dim + c)] * u; - zero_duv[static_cast(map.dim + c)] += - normalized_dw[static_cast(li * map.dim + c)] * v; - } - } - else - { - zero_w[static_cast(zi)] = - normalized_w[static_cast(li)]; - for (int c = 0; c < map.dim; ++c) - { - zero_dw[static_cast(zi * map.dim + c)] = - normalized_dw[static_cast(li * map.dim + c)]; - } - } - } - } - } - - std::span face_coordinates = - (face.zero_face_type == cell::type::quadrilateral) - ? std::span(zero_uv.data(), zero_uv.size()) - : std::span(zero_w.data(), zero_w.size()); - const auto curved = eval_curved_face_ref_with_jacobian( - face, map.geometry_order, map.node_family, face_coordinates); - std::vector straight(static_cast(map.parent_tdim), T(0)); - std::vector straight_jacobian( - static_cast(map.dim * map.parent_tdim), T(0)); - for (int li = 0; li < face.num_local_vertices; ++li) - { - const int lv = face.local_vertices[static_cast(li)]; - const T w = normalized_w[static_cast(li)]; - for (int d = 0; d < map.parent_tdim; ++d) - { - straight[static_cast(d)] += w * map.ref_vertex_coords[ - static_cast(lv * map.parent_tdim + d)]; - for (int c = 0; c < map.dim; ++c) - { - straight_jacobian[static_cast( - c * map.parent_tdim + d)] += - normalized_dw[static_cast(li * map.dim + c)] - * map.ref_vertex_coords[ - static_cast(lv * map.parent_tdim + d)]; - } - } - } - for (int d = 0; d < map.parent_tdim; ++d) - { - const T diff = - curved.point[static_cast(d)] - - straight[static_cast(d)]; - out.point[static_cast(d)] += s * diff; - for (int c = 0; c < map.dim; ++c) - { - T dcurved = T(0); - if (face.zero_face_type == cell::type::quadrilateral) - { - dcurved = - curved.jacobian[static_cast(d)] - * zero_duv[static_cast(c)] - + curved.jacobian[static_cast(map.parent_tdim + d)] - * zero_duv[static_cast(map.dim + c)]; - } - else - { - dcurved = - curved.jacobian[static_cast(d)] - * zero_dw[static_cast(map.dim + c)] - + curved.jacobian[static_cast(map.parent_tdim + d)] - * zero_dw[static_cast(2 * map.dim + c)]; - } - const std::size_t jd = - static_cast(c * map.parent_tdim + d); - out.jacobian[jd] += - ds[static_cast(c)] * diff - + s * (dcurved - straight_jacobian[jd]); - } - } + const T* vi = vertices + (col + 1) * gdim; + for (int row = 0; row < gdim; ++row) + J[col * gdim + row] = vi[row] - v0[row]; } - for (const auto& edge : map.curved_edges) + if (simplex_dim == gdim) { - if (edge_is_in_any_curved_face( - edge, std::span>(map.curved_faces.data(), map.curved_faces.size()))) - { - continue; - } - const T a = vertex_weights[static_cast(edge.local_vertices[0])]; - const T b = vertex_weights[static_cast(edge.local_vertices[1])]; - const T s = a + b; - if (s <= eps) - continue; - const T t = b / s; - const auto curved = eval_curved_edge_ref( - edge, map.geometry_order, map.node_family, t); - const auto curved_dt = eval_curved_edge_ref_derivative( - edge, map.geometry_order, map.node_family, t); - std::vector straight(static_cast(map.parent_tdim), T(0)); - std::vector straight_dt(static_cast(map.parent_tdim), T(0)); - for (int d = 0; d < map.parent_tdim; ++d) - { - const T x0 = map.ref_vertex_coords[ - static_cast(edge.local_vertices[0] * map.parent_tdim + d)]; - const T x1 = map.ref_vertex_coords[ - static_cast(edge.local_vertices[1] * map.parent_tdim + d)]; - straight[static_cast(d)] = (T(1) - t) * x0 + t * x1; - straight_dt[static_cast(d)] = x1 - x0; - const T diff = - curved[static_cast(d)] - - straight[static_cast(d)]; - out.point[static_cast(d)] += s * diff; - for (int c = 0; c < map.dim; ++c) - { - const T da = - vertex_gradients[static_cast( - edge.local_vertices[0] * map.dim + c)]; - const T db = - vertex_gradients[static_cast( - edge.local_vertices[1] * map.dim + c)]; - const T ds_edge = da + db; - const T dt = (db * s - b * ds_edge) / (s * s); - out.jacobian[static_cast(c * map.parent_tdim + d)] += - ds_edge * diff - + s * (curved_dt[static_cast(d)] - - straight_dt[static_cast(d)]) * dt; - } - } - } - - return out; -} - -template -std::vector curved_map_ref_point(const LocalCurvedSimplexMap& map, - std::span xi) -{ - return curved_map_ref_point_with_jacobian(map, xi).point; -} - -template -std::vector curved_map_physical_point(const LocalCurvedSimplexMap& map, - std::span xi) -{ - const auto ref = curved_map_ref_point(map, xi); - return push_parent_ref_to_physical(map, std::span(ref.data(), ref.size())); -} + if (simplex_dim == 1) + return std::abs(J[0]); + if (simplex_dim == 2) + return std::abs(J[0] * J[3] - J[2] * J[1]); -template -std::vector canonical_simplex_vertices(cell::type simplex_type) -{ - return cell::reference_vertices(simplex_type); -} - -template -std::vector map_child_xi_to_parent_xi(cell::type simplex_type, - std::span child_vertices, - std::span xi) -{ - const int dim = cell::get_tdim(simplex_type); - const auto weights = cell_vertex_shape_weights(simplex_type, xi); - std::vector out(static_cast(dim), T(0)); - for (std::size_t v = 0; v < weights.size(); ++v) - { - for (int d = 0; d < dim; ++d) - { - out[static_cast(d)] += weights[v] * child_vertices[ - v * static_cast(dim) + static_cast(d)]; - } - } - return out; -} - -template -std::vector child_point(cell::type cell_type, - std::span child_vertices, - std::initializer_list xi) -{ - std::vector coords(xi); - return map_child_xi_to_parent_xi( - cell_type, - child_vertices, - std::span(coords.data(), coords.size())); -} - -template -std::vector> subdivide_child_simplex(cell::type simplex_type, - std::span vertices) -{ - const int dim = cell::get_tdim(simplex_type); - auto vertex = [&](int i) - { - return std::vector( - vertices.begin() + static_cast(i * dim), - vertices.begin() + static_cast((i + 1) * dim)); - }; - auto midpoint = [&](const std::vector& a, const std::vector& b) - { - std::vector m(static_cast(dim), T(0)); - for (int d = 0; d < dim; ++d) - m[static_cast(d)] = T(0.5) * (a[static_cast(d)] - + b[static_cast(d)]); - return m; - }; - auto pack = [&](std::initializer_list> pts) - { - std::vector child; - child.reserve(pts.size() * static_cast(dim)); - for (const auto& p : pts) - child.insert(child.end(), p.begin(), p.end()); - return child; - }; - - if (simplex_type == cell::type::interval) - { - const auto v0 = vertex(0); - const auto v1 = vertex(1); - const auto m01 = midpoint(v0, v1); - return {pack({v0, m01}), pack({m01, v1})}; - } - - if (simplex_type == cell::type::triangle) - { - const auto v0 = vertex(0); - const auto v1 = vertex(1); - const auto v2 = vertex(2); - const auto m01 = midpoint(v0, v1); - const auto m12 = midpoint(v1, v2); - const auto m20 = midpoint(v2, v0); - return { - pack({v0, m01, m20}), - pack({m01, v1, m12}), - pack({m20, m12, v2}), - pack({m01, m12, m20}) - }; - } - - if (simplex_type == cell::type::tetrahedron) - { - const auto v0 = vertex(0); - const auto v1 = vertex(1); - const auto v2 = vertex(2); - const auto v3 = vertex(3); - const auto m01 = midpoint(v0, v1); - const auto m02 = midpoint(v0, v2); - const auto m03 = midpoint(v0, v3); - const auto m12 = midpoint(v1, v2); - const auto m13 = midpoint(v1, v3); - const auto m23 = midpoint(v2, v3); - return { - pack({v0, m01, m02, m03}), - pack({m01, v1, m12, m13}), - pack({m02, m12, v2, m23}), - pack({m03, m13, m23, v3}), - pack({m01, m02, m03, m13}), - pack({m01, m02, m12, m13}), - pack({m02, m12, m13, m23}), - pack({m02, m03, m13, m23}) - }; - } - - if (simplex_type == cell::type::quadrilateral) - { - return { - pack({child_point(simplex_type, vertices, {T(0), T(0)}), - child_point(simplex_type, vertices, {T(0.5), T(0)}), - child_point(simplex_type, vertices, {T(0), T(0.5)}), - child_point(simplex_type, vertices, {T(0.5), T(0.5)})}), - pack({child_point(simplex_type, vertices, {T(0.5), T(0)}), - child_point(simplex_type, vertices, {T(1), T(0)}), - child_point(simplex_type, vertices, {T(0.5), T(0.5)}), - child_point(simplex_type, vertices, {T(1), T(0.5)})}), - pack({child_point(simplex_type, vertices, {T(0), T(0.5)}), - child_point(simplex_type, vertices, {T(0.5), T(0.5)}), - child_point(simplex_type, vertices, {T(0), T(1)}), - child_point(simplex_type, vertices, {T(0.5), T(1)})}), - pack({child_point(simplex_type, vertices, {T(0.5), T(0.5)}), - child_point(simplex_type, vertices, {T(1), T(0.5)}), - child_point(simplex_type, vertices, {T(0.5), T(1)}), - child_point(simplex_type, vertices, {T(1), T(1)})}) - }; - } - - if (simplex_type == cell::type::prism) - { - const std::array, 6> tri = {{ - {T(0), T(0)}, {T(1), T(0)}, {T(0), T(1)}, - {T(0.5), T(0)}, {T(0.5), T(0.5)}, {T(0), T(0.5)} - }}; - const std::array, 4> sub_tri = {{ - {0, 3, 5}, - {3, 1, 4}, - {5, 4, 2}, - {3, 4, 5} - }}; - std::vector> children; - children.reserve(8); - for (int slab = 0; slab < 2; ++slab) - { - const T z0 = T(slab) * T(0.5); - const T z1 = T(slab + 1) * T(0.5); - for (const auto& st : sub_tri) - { - children.push_back(pack({ - child_point(simplex_type, vertices, {tri[st[0]][0], tri[st[0]][1], z0}), - child_point(simplex_type, vertices, {tri[st[1]][0], tri[st[1]][1], z0}), - child_point(simplex_type, vertices, {tri[st[2]][0], tri[st[2]][1], z0}), - child_point(simplex_type, vertices, {tri[st[0]][0], tri[st[0]][1], z1}), - child_point(simplex_type, vertices, {tri[st[1]][0], tri[st[1]][1], z1}), - child_point(simplex_type, vertices, {tri[st[2]][0], tri[st[2]][1], z1}) - })); - } - } - return children; - } - - throw std::runtime_error("subdivide_child_simplex: unsupported simplex type"); -} - -template -T curved_map_measure(const LocalCurvedSimplexMap& map, - std::span xi) -{ - const auto ref_eval = curved_map_ref_point_with_jacobian(map, xi); - const auto physical_J = compose_ref_to_physical_jacobian( - map, - std::span(ref_eval.jacobian.data(), ref_eval.jacobian.size())); - return measure_from_jacobian( - map.dim, - map.gdim, - std::span(physical_J.data(), physical_J.size())); -} - -template -T straight_map_measure(const LocalCurvedSimplexMap& map, - std::span xi) -{ - const auto ref_J = straight_ref_jacobian_from_cell_point(map, xi); - const auto physical_J = compose_ref_to_physical_jacobian( - map, - std::span(ref_J.data(), ref_J.size())); - return std::abs(measure_from_jacobian( - map.dim, - map.gdim, - std::span(physical_J.data(), physical_J.size()))); -} - -template -T child_map_measure(cell::type cell_type, - std::span child_vertices, - std::span xi) -{ - const int d = cell::get_tdim(cell_type); - const int nv = cell::get_num_vertices(cell_type); - const auto gradients = cell_vertex_shape_gradients(cell_type, xi); - std::vector J(static_cast(d * d), T(0)); - for (int c = 0; c < d; ++c) - { - for (int v = 0; v < nv; ++v) - { - const T dNv = gradients[static_cast(v * d + c)]; - for (int r = 0; r < d; ++r) - { - J[static_cast(c * d + r)] += - dNv * child_vertices[static_cast(v * d + r)]; - } - } - } - - return std::abs(measure_from_jacobian( - d, d, std::span(J.data(), J.size()))); -} - -template -bool curved_map_valid(const LocalCurvedSimplexMap& map) -{ - std::vector> samples; - if (map.simplex_type == cell::type::interval) - samples = {{T(0.25)}, {T(0.5)}, {T(0.75)}}; - else if (map.simplex_type == cell::type::triangle) - samples = {{T(1) / T(3), T(1) / T(3)}, - {T(0.5), T(0.25)}, - {T(0.25), T(0.5)}, - {T(0.25), T(0.25)}}; - else if (map.simplex_type == cell::type::quadrilateral) - samples = {{T(0.5), T(0.5)}, - {T(0.25), T(0.25)}, - {T(0.75), T(0.25)}, - {T(0.25), T(0.75)}, - {T(0.75), T(0.75)}}; - else if (map.simplex_type == cell::type::tetrahedron) - samples = {{T(0.25), T(0.25), T(0.25)}, - {T(0.5), T(1) / T(6), T(1) / T(6)}, - {T(1) / T(6), T(0.5), T(1) / T(6)}, - {T(1) / T(6), T(1) / T(6), T(0.5)}}; - else if (map.simplex_type == cell::type::prism) - samples = {{T(1) / T(3), T(1) / T(3), T(0.5)}, - {T(0.2), T(0.2), T(0.25)}, - {T(0.6), T(0.2), T(0.25)}, - {T(0.2), T(0.6), T(0.75)}, - {T(0.2), T(0.2), T(0.75)}}; - else - return true; - - constexpr T tol = T(1e-12); - for (const auto& sample : samples) - { - const T measure = curved_map_measure( - map, std::span(sample.data(), sample.size())); - if (!(std::abs(measure) > tol)) - return false; - } - return true; -} - -template -bool curved_child_map_valid(const LocalCurvedSimplexMap& map, - std::span child_vertices) -{ - if (map.curved_edges.empty() && map.curved_faces.empty()) - return true; - - std::vector> samples; - if (map.simplex_type == cell::type::interval) - samples = {{T(0.25)}, {T(0.5)}, {T(0.75)}}; - else if (map.simplex_type == cell::type::triangle) - samples = {{T(1) / T(3), T(1) / T(3)}, - {T(0.5), T(0.25)}, - {T(0.25), T(0.5)}, - {T(0.25), T(0.25)}}; - else if (map.simplex_type == cell::type::quadrilateral) - samples = {{T(0.5), T(0.5)}, - {T(0.25), T(0.25)}, - {T(0.75), T(0.25)}, - {T(0.25), T(0.75)}, - {T(0.75), T(0.75)}}; - else if (map.simplex_type == cell::type::tetrahedron) - samples = {{T(0.25), T(0.25), T(0.25)}, - {T(0.5), T(1) / T(6), T(1) / T(6)}, - {T(1) / T(6), T(0.5), T(1) / T(6)}, - {T(1) / T(6), T(1) / T(6), T(0.5)}}; - else if (map.simplex_type == cell::type::prism) - samples = {{T(1) / T(3), T(1) / T(3), T(0.5)}, - {T(0.2), T(0.2), T(0.25)}, - {T(0.6), T(0.2), T(0.25)}, - {T(0.2), T(0.6), T(0.75)}, - {T(0.2), T(0.2), T(0.75)}}; - else - return true; - - constexpr T tol = T(1e-12); - for (const auto& sample : samples) - { - const auto xi_parent = map_child_xi_to_parent_xi( - map.simplex_type, - child_vertices, - std::span(sample.data(), sample.size())); - const T measure = curved_map_measure( - map, std::span(xi_parent.data(), xi_parent.size())); - if (!(std::abs(measure) > tol)) - return false; - } - return true; -} - - -template -bool vertex_is_zero_for_level_set(const AdaptCell& adapt_cell, - int vertex_id, - int level_set_id) -{ - const std::uint64_t bit = std::uint64_t(1) << level_set_id; - return (adapt_cell.zero_mask_per_vertex[static_cast(vertex_id)] & bit) != 0; -} - -template -bool cell_contains_all_vertices(std::span cell_verts, - std::span entity_verts) -{ - for (const int v : entity_verts) - { - bool found = false; - for (const auto cv : cell_verts) - { - if (cv == v) - { - found = true; - break; - } - } - if (!found) - return false; - } - return true; -} - -template -void vertex_state_for_level_set(const AdaptCell& ac, - int vertex_id, - int level_set_id, - bool& is_negative, - bool& is_positive, - bool& is_zero) -{ - const std::uint64_t bit = std::uint64_t(1) << level_set_id; - const auto zm = ac.zero_mask_per_vertex[static_cast(vertex_id)]; - const auto nm = ac.negative_mask_per_vertex[static_cast(vertex_id)]; - is_zero = (zm & bit) != 0; - is_negative = !is_zero && ((nm & bit) != 0); - is_positive = !is_zero && !is_negative; -} - -template -bool graph_checks_allow_curving(const HOMeshPart& part, int cut_cell_id) -{ - if (!part.cut_cells) - return true; - - const auto& diagnostics = part.cut_cells->graph_diagnostics; - if (cut_cell_id < 0 - || cut_cell_id >= static_cast(diagnostics.size())) - { - return true; - } - return diagnostics[static_cast(cut_cell_id)].accepted; -} - -template -bool graph_checks_allow_zero_entity_curving(const HOMeshPart& part, - int cut_cell_id, - int local_zero_entity_id) -{ - if (!part.cut_cells) - return true; - - const auto& diagnostics = part.cut_cells->graph_diagnostics; - if (cut_cell_id < 0 - || cut_cell_id >= static_cast(diagnostics.size())) - { - return true; - } - - const auto& cell_diag = diagnostics[static_cast(cut_cell_id)]; - for (const auto& record : cell_diag.zero_entities) - { - if (record.local_zero_entity_id == local_zero_entity_id) - return record.accepted; - } - - return true; -} - -template -const curving::CurvedZeroEntityState* accepted_curved_state( - const HOMeshPart& part, - int cut_cell_id, - int local_zero_entity_id, - const curving::CurvingOptions& options) -{ - if (!graph_checks_allow_zero_entity_curving( - part, cut_cell_id, local_zero_entity_id)) - return nullptr; - - const auto& state = curving::ensure_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - cut_cell_id, - local_zero_entity_id, - options); - if (state.status != curving::CurvingStatus::curved) - return nullptr; - return &state; -} - -template -bool zero_edge_is_same_mask_boundary_of_zero_face(const AdaptCell& ac, - const LocalCurvedSimplexMap& map, - int zero_edge_id, - std::span edge_vertices) -{ - const auto edge_mask = ac.zero_entity_zero_mask[static_cast(zero_edge_id)]; - for (int z = 0; z < ac.n_zero_entities(); ++z) - { - if (ac.zero_entity_dim[static_cast(z)] != 2) - continue; - if (ac.zero_entity_zero_mask[static_cast(z)] != edge_mask) - continue; - - const auto face_vertices = zero_entity_vertex_ids(ac, z); - bool edge_in_face = true; - for (const int ev : edge_vertices) - { - bool found = false; - for (const int fv : face_vertices) - found = found || (ev == fv); - edge_in_face = edge_in_face && found; - } - if (!edge_in_face) - continue; - - bool face_in_map = true; - for (const int fv : face_vertices) - { - bool found = false; - for (const int mv : map.vertices) - found = found || (fv == mv); - face_in_map = face_in_map && found; - } - if (face_in_map) - return true; - } - return false; -} - -template -void attach_curved_boundaries(LocalCurvedSimplexMap& map, - const HOMeshPart& part, - const AdaptCell& ac, - int cut_cell_id, - const curving::CurvingOptions& options, - int required_zero_entity_index = -1) -{ - const int nzero = ac.n_zero_entities(); - for (int z = 0; z < nzero; ++z) - { - if (required_zero_entity_index >= 0 && z != required_zero_entity_index) - continue; - - const int zdim = ac.zero_entity_dim[static_cast(z)]; - if (zdim != 1 && zdim != 2) - continue; - - const auto zverts = zero_entity_vertex_ids(ac, z); - - if (zdim == 1) - { - if (map.parent_tdim == 3 && map.dim >= 2 - && zero_edge_is_same_mask_boundary_of_zero_face( - ac, map, z, std::span(zverts.data(), zverts.size()))) - { - continue; - } - - for (const auto& edge : cell::edges(map.simplex_type)) - { - std::array local_pair = { - map.vertices[static_cast(edge[0])], - map.vertices[static_cast(edge[1])] - }; - if (!same_unordered( - std::span(local_pair.data(), local_pair.size()), - std::span(zverts.data(), zverts.size()))) - { - continue; - } - - const auto* state = accepted_curved_state(part, cut_cell_id, z, options); - if (state == nullptr) - continue; - - CurvedBoundaryEdge ce; - ce.local_vertices = edge; - ce.local_global_vertices = local_pair; - ce.zero_vertices = {zverts[0], zverts[1]}; - ce.state = state; - map.curved_edges.push_back(ce); - } - } - else if (zdim == 2 - && (map.simplex_type == cell::type::tetrahedron - || map.simplex_type == cell::type::triangle - || map.simplex_type == cell::type::quadrilateral - || map.simplex_type == cell::type::prism) - && (zverts.size() == 3 || zverts.size() == 4)) - { - auto local_face_is_on_zero_face = [&](std::span local_face) - { - for (const int v : local_face) - { - bool found = false; - for (const int zv : zverts) - found = found || (v == zv); - if (!found) - return false; - } - return true; - }; - - auto append_face = [&](std::array local_ids, int nlocal) - { - std::array local_face = {-1, -1, -1, -1}; - for (int i = 0; i < nlocal; ++i) - local_face[static_cast(i)] = - map.vertices[static_cast(local_ids[static_cast(i)])]; - if (!local_face_is_on_zero_face( - std::span(local_face.data(), static_cast(nlocal)))) - { - return; - } - - const auto* state = accepted_curved_state(part, cut_cell_id, z, options); - if (state == nullptr) - return; - - CurvedBoundaryFace cf; - cf.local_vertices = local_ids; - cf.num_local_vertices = nlocal; - cf.num_zero_vertices = static_cast(zverts.size()); - cf.zero_face_type = ac.entity_types[2][ - static_cast(ac.zero_entity_id[static_cast(z)])]; - for (std::size_t i = 0; i < zverts.size(); ++i) - cf.zero_vertices[i] = zverts[i]; - cf.state = state; - map.curved_faces.push_back(cf); - }; - - if (map.simplex_type == cell::type::triangle) - { - append_face({0, 1, 2, -1}, 3); - } - else if (map.simplex_type == cell::type::quadrilateral) - { - append_face({0, 1, 2, 3}, 4); - } - else if (map.simplex_type == cell::type::prism) - { - for (int f = 0; f < cell::num_faces(cell::type::prism); ++f) - { - const auto fv = cell::face_vertices(cell::type::prism, f); - std::array local_ids = {-1, -1, -1, -1}; - for (std::size_t i = 0; i < fv.size(); ++i) - local_ids[i] = fv[i]; - append_face(local_ids, static_cast(fv.size())); - } - } - else - { - for (int f = 0; f < cell::num_faces(cell::type::tetrahedron); ++f) - { - const auto fv = cell::face_vertices(cell::type::tetrahedron, f); - append_face({fv[0], fv[1], fv[2], -1}, 3); - } - } - } - } -} - -template -bool leaf_cell_matches_sign_requirements( - const AdaptCell& ac, - std::span cell_verts, - const SelectionExpr& expr, - std::uint64_t cut_cell_active_mask, - const BackgroundMeshData& bg, - I parent_cell_id) -{ - const int nls = std::min(bg.num_level_sets, 64); - for (int li = 0; li < nls; ++li) - { - const std::uint64_t bit = std::uint64_t(1) << li; - const bool require_neg = (expr.negative_required & bit) != 0; - const bool require_pos = (expr.positive_required & bit) != 0; - if (!require_neg && !require_pos) - continue; - - const bool ls_is_active = (cut_cell_active_mask & bit) != 0; - if (!ls_is_active) - { - const auto dom = bg.domain(li, parent_cell_id); - if (require_neg && dom != cell::domain::inside) - return false; - if (require_pos && dom != cell::domain::outside) - return false; - continue; - } - - bool has_neg = false; - bool has_pos = false; - for (const auto cv : cell_verts) - { - bool is_neg = false; - bool is_pos = false; - bool is_zero = false; - vertex_state_for_level_set(ac, static_cast(cv), li, is_neg, is_pos, is_zero); - has_neg = has_neg || is_neg; - has_pos = has_pos || is_pos; - } - - if (require_neg) - { - if (has_pos || !has_neg) - return false; - } - if (require_pos) - { - if (has_neg || !has_pos) - return false; - } - } - - return true; -} - -template -bool zero_entity_matches(const HOMeshPart& part, - const AdaptCell& ac, - int zero_entity_index, - std::uint64_t cut_cell_active_mask, - I parent_cell_id) -{ - if (ac.zero_entity_dim[static_cast(zero_entity_index)] != part.dim) - return false; - - const auto zero_mask = ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; - if ((zero_mask & part.expr.zero_required) != part.expr.zero_required) - return false; - - if (part.expr.negative_required == 0 && part.expr.positive_required == 0) - return true; - - std::vector zero_verts; - const int zdim = ac.zero_entity_dim[static_cast(zero_entity_index)]; - const int zid = ac.zero_entity_id[static_cast(zero_entity_index)]; - if (zdim == 0) - { - zero_verts.push_back(zid); - } - else - { - auto verts = ac.entity_to_vertex[zdim][static_cast(zid)]; - zero_verts.reserve(verts.size()); - for (const auto v : verts) - zero_verts.push_back(static_cast(v)); - } - - const int tdim = ac.tdim; - const int n_cells = ac.n_entities(tdim); - for (int c = 0; c < n_cells; ++c) - { - auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; - if (!cell_contains_all_vertices(cell_verts, std::span(zero_verts))) - continue; - - if (leaf_cell_matches_sign_requirements( - ac, cell_verts, part.expr, cut_cell_active_mask, *part.bg, parent_cell_id)) - { - return true; - } - } - - return false; -} - -template -std::vector selected_entities(const HOMeshPart& part, - const AdaptCell& adapt_cell, - int cut_cell_id) -{ - std::vector entities; - const I parent_cell_id = - part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]; - const std::uint64_t cut_active_mask = - part.cut_cells->active_level_set_mask[static_cast(cut_cell_id)]; - - if (part.dim == adapt_cell.tdim) - { - const int n_cells = adapt_cell.n_entities(adapt_cell.tdim); - entities.reserve(static_cast(n_cells)); - for (int c = 0; c < n_cells; ++c) - { - auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][static_cast(c)]; - if (!leaf_cell_matches_sign_requirements( - adapt_cell, verts, part.expr, cut_active_mask, *part.bg, parent_cell_id)) - { - continue; - } - - SelectedEntity entity; - entity.type = adapt_cell.entity_types[adapt_cell.tdim][static_cast(c)]; - entity.vertices.assign(verts.begin(), verts.end()); - entities.push_back(std::move(entity)); - } - return entities; - } - - if ((cut_active_mask & part.expr.zero_required) != part.expr.zero_required) - return entities; - - const int n_zero = adapt_cell.n_zero_entities(); - entities.reserve(static_cast(n_zero)); - for (int z = 0; z < n_zero; ++z) - { - if (!zero_entity_matches(part, adapt_cell, z, cut_active_mask, parent_cell_id)) - continue; - - const int zdim = adapt_cell.zero_entity_dim[static_cast(z)]; - const int zid = adapt_cell.zero_entity_id[static_cast(z)]; - SelectedEntity entity; - entity.type = (zdim == 0) - ? cell::type::point - : adapt_cell.entity_types[zdim][static_cast(zid)]; - entity.zero_entity_index = z; - if (zdim == 0) - { - entity.vertices.push_back(zid); - } - else - { - auto verts = adapt_cell.entity_to_vertex[zdim][static_cast(zid)]; - entity.vertices.assign(verts.begin(), verts.end()); - } - - entities.push_back(std::move(entity)); - } - - return entities; -} - -} // namespace - -template -std::vector selected_zero_entity_infos( - const HOMeshPart& part) -{ - if (!part.cut_cells) - throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - - std::vector out; - for (std::int32_t cut_id : part.cut_cell_ids) - { - if (cut_id < 0 - || cut_id >= static_cast(part.cut_cells->adapt_cells.size())) - { - continue; - } - - const auto& adapt_cell = - part.cut_cells->adapt_cells[static_cast(cut_id)]; - const auto entities = selected_entities(part, adapt_cell, cut_id); - const auto parent_cell_id = - part.cut_cells->parent_cell_ids[static_cast(cut_id)]; - for (const auto& entity : entities) - { - if (entity.zero_entity_index < 0) - continue; - - SelectedZeroEntityInfo info; - info.cut_cell_id = cut_id; - info.parent_cell_id = static_cast(parent_cell_id); - info.local_zero_entity_id = - static_cast(entity.zero_entity_index); - info.dimension = static_cast( - adapt_cell.zero_entity_dim[ - static_cast(entity.zero_entity_index)]); - out.push_back(info); - } - } - return out; -} - -namespace -{ - -template -std::vector entity_reference_coords(const AdaptCell& adapt_cell, - std::span entity_vertices) -{ - std::vector coords( - static_cast(entity_vertices.size() * adapt_cell.tdim), T(0)); - - for (std::size_t j = 0; j < entity_vertices.size(); ++j) - { - const int gv = entity_vertices[j]; - for (int d = 0; d < adapt_cell.tdim; ++d) - { - coords[static_cast(j * adapt_cell.tdim + d)] = - adapt_cell.vertex_coords[static_cast(gv * adapt_cell.tdim + d)]; - } - } - - return coords; -} - -template -void gather_subcell_vertices(std::span coords, - int coord_dim, - std::span vertex_ids, - std::vector& out) -{ - out.resize(static_cast(vertex_ids.size() * coord_dim)); - for (std::size_t j = 0; j < vertex_ids.size(); ++j) - { - const int local_v = vertex_ids[j]; - for (int d = 0; d < coord_dim; ++d) - { - out[static_cast(j * coord_dim + d)] = - coords[static_cast(local_v * coord_dim + d)]; - } - } -} - -template -void map_canonical_to_subcell_points(const T* canonical_points, - int num_points, - int simplex_dim, - const T* subcell_vertices, - int parent_tdim, - T* out_points) -{ - const T* v0 = subcell_vertices; - - for (int q = 0; q < num_points; ++q) - { - const T* X = canonical_points + q * simplex_dim; - T* x = out_points + q * parent_tdim; - - for (int d = 0; d < parent_tdim; ++d) - x[d] = v0[d]; - - for (int i = 1; i <= simplex_dim; ++i) - { - const T* vi = subcell_vertices + i * parent_tdim; - for (int d = 0; d < parent_tdim; ++d) - x[d] += X[i - 1] * (vi[d] - v0[d]); - } - } -} - -template -T simplex_physical_measure(const T* vertices, - int simplex_dim, - int gdim) -{ - T J[9] = {}; - const T* v0 = vertices; - - for (int col = 0; col < simplex_dim; ++col) - { - const T* vi = vertices + (col + 1) * gdim; - for (int row = 0; row < gdim; ++row) - J[col * gdim + row] = vi[row] - v0[row]; - } - - if (simplex_dim == gdim) - { - if (simplex_dim == 1) - return std::abs(J[0]); - if (simplex_dim == 2) - return std::abs(J[0] * J[3] - J[2] * J[1]); - - const T det = - J[0] * (J[4] * J[8] - J[7] * J[5]) - - J[3] * (J[1] * J[8] - J[7] * J[2]) - + J[6] * (J[1] * J[5] - J[4] * J[2]); - return std::abs(det); - } - - T G[9] = {}; - for (int i = 0; i < simplex_dim; ++i) - { - for (int j = 0; j < simplex_dim; ++j) - { - T sum = 0; - for (int k = 0; k < gdim; ++k) - sum += J[i * gdim + k] * J[j * gdim + k]; - G[i * simplex_dim + j] = sum; - } - } - - if (simplex_dim == 1) - return std::sqrt(G[0]); - if (simplex_dim == 2) - return std::sqrt(G[0] * G[3] - G[1] * G[2]); - - const T det = - G[0] * (G[4] * G[8] - G[7] * G[5]) - - G[3] * (G[1] * G[8] - G[7] * G[2]) - + G[6] * (G[1] * G[5] - G[4] * G[2]); - return std::sqrt(det); -} - -template -std::vector reorder_vertex_coords_to_vtk(cell::type cell_type, - std::span coords, - int coord_dim) -{ - const auto perm = cell::basix_to_vtk_vertex_permutation(cell_type); - return cell::permute_vertex_data(coords, coord_dim, perm); -} - -inline std::vector vtk_local_ids_from_basix(cell::type cell_type) -{ - const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); - return std::vector(perm.begin(), perm.end()); -} - -template -void append_mesh_entity(mesh::CutMesh& out, - std::span physical_coords, - int gdim, - cell::type cell_type, - int parent_cell_id, - bool triangulate, - bool input_is_basix, - cell::type source_parent_type, - std::span root_vertex_flags) -{ - if (out._gdim == 0) - out._gdim = gdim; - if (out._tdim == 0) - out._tdim = cell::get_tdim(cell_type); - - // CutMesh stores vertex coordinates in basix ordering internally. - // Basix-ordered inputs are stored as-is; VTK-ordered inputs are - // permuted to basix ordering before storage. - std::vector reordered_coords; - std::span output_coords = physical_coords; - if (!input_is_basix && !is_simplex(cell_type)) - { - const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); - reordered_coords = cell::permute_vertex_data(physical_coords, gdim, perm); - output_coords = std::span(reordered_coords.data(), reordered_coords.size()); - } - - const int nv = static_cast(output_coords.size()) / gdim; - const int vertex_base = out._num_vertices; - - if (triangulate - && cell_type == cell::type::quadrilateral - && source_parent_type == cell::type::triangle - && root_vertex_flags.size() == static_cast(nv)) - { - std::array quad_tokens = {}; - for (int i = 0; i < nv; ++i) - { - quad_tokens[static_cast(i)] - = root_vertex_flags[static_cast(i)] ? i : 100 + i; - } - - int next_token_base = 200; - auto split = cell::quad_midpoint::split_triangle_derived_quadrilateral( - output_coords, - gdim, - std::span(quad_tokens.data(), quad_tokens.size()), - next_token_base); - - out._vertex_coords.insert( - out._vertex_coords.end(), output_coords.begin(), output_coords.end()); - out._num_vertices += nv; - - const int midpoint_vertex_base = out._num_vertices; - out._vertex_coords.insert( - out._vertex_coords.end(), - split.added_vertex_coords.begin(), - split.added_vertex_coords.end()); - out._num_vertices += static_cast(split.added_vertex_tokens.size()); - - std::array token_to_vertex; - token_to_vertex.fill(-1); - for (int i = 0; i < nv; ++i) - token_to_vertex[quad_tokens[static_cast(i)]] = vertex_base + i; - for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) - { - token_to_vertex[split.added_vertex_tokens[i]] - = midpoint_vertex_base + static_cast(i); - } - - for (const auto& tri_tokens : split.triangles) - { - for (int k = 0; k < 3; ++k) - out._connectivity.push_back(token_to_vertex[tri_tokens[static_cast(k)]]); - out._offset.push_back(static_cast(out._connectivity.size())); - out._types.push_back(cell::type::triangle); - out._parent_map.push_back(parent_cell_id); - out._num_cells += 1; - } - return; - } - - if (triangulate - && cell_type == cell::type::prism - && source_parent_type == cell::type::tetrahedron - && root_vertex_flags.size() == static_cast(nv)) - { - std::array prism_tokens = {}; - for (int i = 0; i < nv; ++i) - prism_tokens[static_cast(i)] = root_vertex_flags[static_cast(i)] ? i : 100 + i; - - int next_token_base = 200; - auto split = cell::prism_midpoint::split_tetra_derived_prism( - output_coords, - gdim, - std::span(prism_tokens.data(), prism_tokens.size()), - next_token_base); - - out._vertex_coords.insert( - out._vertex_coords.end(), output_coords.begin(), output_coords.end()); - out._num_vertices += nv; - - const int midpoint_vertex_base = out._num_vertices; - out._vertex_coords.insert( - out._vertex_coords.end(), - split.added_vertex_coords.begin(), - split.added_vertex_coords.end()); - out._num_vertices += static_cast(split.added_vertex_tokens.size()); - - std::array token_to_vertex; - token_to_vertex.fill(-1); - for (int i = 0; i < nv; ++i) - token_to_vertex[prism_tokens[static_cast(i)]] = vertex_base + i; - for (std::size_t i = 0; i < split.added_vertex_tokens.size(); ++i) - token_to_vertex[split.added_vertex_tokens[i]] = midpoint_vertex_base + static_cast(i); - - for (const auto& tet_tokens : split.tets) - { - for (int k = 0; k < 4; ++k) - out._connectivity.push_back(token_to_vertex[tet_tokens[static_cast(k)]]); - out._offset.push_back(static_cast(out._connectivity.size())); - out._types.push_back(cell::type::tetrahedron); - out._parent_map.push_back(parent_cell_id); - out._num_cells += 1; - } - return; - } - - out._vertex_coords.insert( - out._vertex_coords.end(), output_coords.begin(), output_coords.end()); - out._num_vertices += nv; - - if (triangulate && !is_simplex(cell_type) && cell::get_tdim(cell_type) >= 2) - { - std::vector local_ids(static_cast(nv)); - std::iota(local_ids.begin(), local_ids.end(), 0); - - std::vector> simplices; - cell::triangulation(cell_type, local_ids.data(), simplices); - const auto simplex_type = simplex_type_for_dim(cell::get_tdim(cell_type)); - - for (const auto& simplex : simplices) - { - for (int lv : simplex) - out._connectivity.push_back(vertex_base + lv); - out._offset.push_back(static_cast(out._connectivity.size())); - out._types.push_back(simplex_type); - out._parent_map.push_back(parent_cell_id); - out._num_cells += 1; - } - return; - } - - for (int lv = 0; lv < nv; ++lv) - out._connectivity.push_back(vertex_base + lv); - out._offset.push_back(static_cast(out._connectivity.size())); - out._types.push_back(cell_type); - out._parent_map.push_back(parent_cell_id); - out._num_cells += 1; -} - -template -void append_simplex_quadrature(quadrature::QuadratureRules& rules, - cell::type simplex_type, - std::span ref_vertices, - std::span physical_vertices, - int parent_tdim, - int gdim, - int order) -{ - const auto ref_rule = quadrature::get_reference_rule(simplex_type, order); - const int num_points = ref_rule._num_points; - const int simplex_dim = ref_rule._tdim; - - std::vector mapped_ref_points( - static_cast(num_points * parent_tdim), T(0)); - map_canonical_to_subcell_points( - ref_rule._points.data(), - num_points, - simplex_dim, - ref_vertices.data(), - parent_tdim, - mapped_ref_points.data()); - - rules._points.insert( - rules._points.end(), mapped_ref_points.begin(), mapped_ref_points.end()); - - const T measure = simplex_physical_measure( - physical_vertices.data(), simplex_dim, gdim); - for (int q = 0; q < num_points; ++q) - rules._weights.push_back(ref_rule._weights[q] * measure); -} - -template -void append_entity_quadrature(quadrature::QuadratureRules& rules, - cell::type cell_type, - std::span ref_vertices, - std::span physical_vertices, - int parent_tdim, - int gdim, - int parent_cell_id, - int order, - bool triangulate, - bool input_is_basix, - cell::type source_parent_type, - std::span root_vertex_flags) -{ - if (rules._tdim == 0) - rules._tdim = parent_tdim; - if (rules._offset.empty()) - rules._offset.push_back(0); - - std::span ref_use = ref_vertices; - std::span phys_use = physical_vertices; - std::vector ref_vertices_basix; - std::vector phys_vertices_basix; - if (triangulate && !input_is_basix && !is_simplex(cell_type)) - { - const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); - ref_vertices_basix = cell::permute_vertex_data(ref_vertices, parent_tdim, perm); - phys_vertices_basix = cell::permute_vertex_data(physical_vertices, gdim, perm); - ref_use = std::span(ref_vertices_basix.data(), ref_vertices_basix.size()); - phys_use = std::span(phys_vertices_basix.data(), phys_vertices_basix.size()); - } - - const int entity_dim = cell::get_tdim(cell_type); - const int nv = static_cast(ref_use.size()) / parent_tdim; - - if (triangulate - && cell_type == cell::type::quadrilateral - && source_parent_type == cell::type::triangle - && root_vertex_flags.size() == static_cast(nv)) - { - std::array quad_tokens = {}; - for (int i = 0; i < nv; ++i) - { - quad_tokens[static_cast(i)] - = root_vertex_flags[static_cast(i)] ? i : 100 + i; - } - - int next_ref_token = 200; - auto ref_split = cell::quad_midpoint::split_triangle_derived_quadrilateral( - ref_use, - parent_tdim, - std::span(quad_tokens.data(), quad_tokens.size()), - next_ref_token); - - int next_phys_token = 200; - auto phys_split = cell::quad_midpoint::split_triangle_derived_quadrilateral( - phys_use, - gdim, - std::span(quad_tokens.data(), quad_tokens.size()), - next_phys_token); - - std::array token_to_local; - token_to_local.fill(-1); - for (int i = 0; i < nv; ++i) - token_to_local[quad_tokens[static_cast(i)]] = i; - for (std::size_t i = 0; i < ref_split.added_vertex_tokens.size(); ++i) - token_to_local[ref_split.added_vertex_tokens[i]] = nv + static_cast(i); - - std::vector ref_all(ref_use.begin(), ref_use.end()); - ref_all.insert( - ref_all.end(), - ref_split.added_vertex_coords.begin(), - ref_split.added_vertex_coords.end()); - - std::vector phys_all(phys_use.begin(), phys_use.end()); - phys_all.insert( - phys_all.end(), - phys_split.added_vertex_coords.begin(), - phys_split.added_vertex_coords.end()); - - std::vector ref_simplex; - std::vector phys_simplex; - for (const auto& tri_tokens : ref_split.triangles) - { - const std::array local_ids = { - token_to_local[tri_tokens[0]], - token_to_local[tri_tokens[1]], - token_to_local[tri_tokens[2]], - }; - gather_subcell_vertices( - std::span(ref_all.data(), ref_all.size()), - parent_tdim, - std::span(local_ids.data(), local_ids.size()), - ref_simplex); - gather_subcell_vertices( - std::span(phys_all.data(), phys_all.size()), - gdim, - std::span(local_ids.data(), local_ids.size()), - phys_simplex); - append_simplex_quadrature( - rules, - cell::type::triangle, - std::span(ref_simplex.data(), ref_simplex.size()), - std::span(phys_simplex.data(), phys_simplex.size()), - parent_tdim, - gdim, - order); - } - - rules._parent_map.push_back(parent_cell_id); - rules._offset.push_back(static_cast(rules._weights.size())); - return; - } - - if (triangulate - && cell_type == cell::type::prism - && source_parent_type == cell::type::tetrahedron - && root_vertex_flags.size() == static_cast(nv)) - { - std::array prism_tokens = {}; - for (int i = 0; i < nv; ++i) - { - prism_tokens[static_cast(i)] - = root_vertex_flags[static_cast(i)] ? i : 100 + i; - } - - int next_ref_token = 200; - auto ref_split = cell::prism_midpoint::split_tetra_derived_prism( - ref_use, - parent_tdim, - std::span(prism_tokens.data(), prism_tokens.size()), - next_ref_token); - - int next_phys_token = 200; - auto phys_split = cell::prism_midpoint::split_tetra_derived_prism( - phys_use, - gdim, - std::span(prism_tokens.data(), prism_tokens.size()), - next_phys_token); - - std::array token_to_local; - token_to_local.fill(-1); - for (int i = 0; i < nv; ++i) - token_to_local[prism_tokens[static_cast(i)]] = i; - for (std::size_t i = 0; i < ref_split.added_vertex_tokens.size(); ++i) - token_to_local[ref_split.added_vertex_tokens[i]] = nv + static_cast(i); - - std::vector ref_all(ref_use.begin(), ref_use.end()); - ref_all.insert( - ref_all.end(), - ref_split.added_vertex_coords.begin(), - ref_split.added_vertex_coords.end()); - - std::vector phys_all(phys_use.begin(), phys_use.end()); - phys_all.insert( - phys_all.end(), - phys_split.added_vertex_coords.begin(), - phys_split.added_vertex_coords.end()); - - std::vector ref_simplex; - std::vector phys_simplex; - for (const auto& tet_tokens : ref_split.tets) - { - const std::array local_ids = { - token_to_local[tet_tokens[0]], - token_to_local[tet_tokens[1]], - token_to_local[tet_tokens[2]], - token_to_local[tet_tokens[3]], - }; - gather_subcell_vertices( - std::span(ref_all.data(), ref_all.size()), - parent_tdim, - std::span(local_ids.data(), local_ids.size()), - ref_simplex); - gather_subcell_vertices( - std::span(phys_all.data(), phys_all.size()), - gdim, - std::span(local_ids.data(), local_ids.size()), - phys_simplex); - append_simplex_quadrature( - rules, - cell::type::tetrahedron, - std::span(ref_simplex.data(), ref_simplex.size()), - std::span(phys_simplex.data(), phys_simplex.size()), - parent_tdim, - gdim, - order); - } - - rules._parent_map.push_back(parent_cell_id); - rules._offset.push_back(static_cast(rules._weights.size())); - return; - } - - if (triangulate && !is_simplex(cell_type) && entity_dim >= 2) - { - std::vector local_ids(static_cast(nv)); - std::iota(local_ids.begin(), local_ids.end(), 0); - - std::vector> simplices; - cell::triangulation(cell_type, local_ids.data(), simplices); - const auto simplex_type = simplex_type_for_dim(entity_dim); - - std::vector ref_simplex; - std::vector phys_simplex; - for (const auto& simplex : simplices) - { - gather_subcell_vertices( - ref_use, parent_tdim, - std::span(simplex.data(), simplex.size()), - ref_simplex); - gather_subcell_vertices( - phys_use, gdim, - std::span(simplex.data(), simplex.size()), - phys_simplex); - append_simplex_quadrature( - rules, - simplex_type, - std::span(ref_simplex.data(), ref_simplex.size()), - std::span(phys_simplex.data(), phys_simplex.size()), - parent_tdim, - gdim, - order); - } - } - else if (is_simplex(cell_type)) - { - append_simplex_quadrature( - rules, - cell_type, - ref_vertices, - physical_vertices, - parent_tdim, - gdim, - order); - } - else - { - const auto ref_rule = quadrature::get_reference_rule(cell_type, order); - const T measure = cell::affine_volume_factor( - cell_type, phys_use.data(), gdim); - rules._points.insert( - rules._points.end(), ref_rule._points.begin(), ref_rule._points.end()); - for (int q = 0; q < ref_rule._num_points; ++q) - rules._weights.push_back(ref_rule._weights[q] * measure); - } - - rules._parent_map.push_back(parent_cell_id); - rules._offset.push_back(static_cast(rules._weights.size())); -} - -template -void append_lagrange_child_simplex(CurvedVTUGrid& out, - const LocalCurvedSimplexMap& map, - int parent_cell_id, - int curving_status, - std::span child_vertices, - int subdivision_depth, - int max_subdivision_depth) -{ - const int order = std::max(map.geometry_order, 1); - const bool curving_failed = - curving_status == static_cast(curving::CurvingStatus::failed); - const bool map_valid = curved_child_map_valid(map, child_vertices); - const bool valid = !curving_failed && map_valid; - if (!map_valid && !curving_failed && subdivision_depth < max_subdivision_depth) - { - const auto children = subdivide_child_simplex(map.simplex_type, child_vertices); - for (const auto& child : children) - { - append_lagrange_child_simplex( - out, - map, - parent_cell_id, - curving_status, - std::span(child.data(), child.size()), - subdivision_depth + 1, - max_subdivision_depth); - } - return; - } - - const auto local_nodes = lagrange_cell_nodes(map.simplex_type, order); - const int local_dofs = static_cast(local_nodes.size()); - const int point_base = static_cast(out.points.size()) / out.gdim; - - for (const auto& xi : local_nodes) - { - const auto xi_parent = map_child_xi_to_parent_xi( - map.simplex_type, - child_vertices, - std::span(xi.data(), xi.size())); - std::vector x; - if (valid) - { - x = curved_map_physical_point( - map, std::span(xi_parent.data(), xi_parent.size())); - } - else - { - const auto ref = straight_ref_from_cell_point( - map, std::span(xi_parent.data(), xi_parent.size())); - x = push_parent_ref_to_physical(map, std::span(ref.data(), ref.size())); - } - out.points.insert(out.points.end(), x.begin(), x.end()); - } - - std::vector perm; - int vtk_type = 0; - if (map.simplex_type == cell::type::interval) - { - vtk_type = vtk_lagrange_curve; - perm.resize(static_cast(local_dofs)); - std::iota(perm.begin(), perm.end(), 0); - } - else if (map.simplex_type == cell::type::triangle) - { - vtk_type = vtk_lagrange_triangle; - perm = io::basix_to_vtk_lagrange_permutation( - cell::type::triangle, local_dofs, order); - } - else if (map.simplex_type == cell::type::quadrilateral) - { - vtk_type = vtk_lagrange_quadrilateral; - perm.resize(static_cast(local_dofs)); - std::iota(perm.begin(), perm.end(), 0); - } - else if (map.simplex_type == cell::type::tetrahedron) - { - vtk_type = vtk_lagrange_tetrahedron; - perm = io::basix_to_vtk_lagrange_permutation( - cell::type::tetrahedron, local_dofs, order); - } - else if (map.simplex_type == cell::type::prism) - { - vtk_type = vtk_lagrange_wedge; - perm.resize(static_cast(local_dofs)); - std::iota(perm.begin(), perm.end(), 0); - } - else - { - throw std::runtime_error("append_lagrange_simplex: unsupported simplex type"); - } - - for (const int p : perm) - out.connectivity.push_back(point_base + p); - out.offsets.push_back(static_cast(out.connectivity.size())); - out.vtk_types.push_back(vtk_type); - out.parent_map.push_back(static_cast(parent_cell_id)); - out.curved_valid.push_back(valid ? 1 : 0); - out.subdivision_depth.push_back(static_cast(subdivision_depth)); - out.curving_status.push_back(static_cast(curving_status)); -} - -template -void append_lagrange_simplex(CurvedVTUGrid& out, - const LocalCurvedSimplexMap& map, - int parent_cell_id, - int curving_status, - int max_subdivision_depth) -{ - const auto child = canonical_simplex_vertices(map.simplex_type); - append_lagrange_child_simplex( - out, - map, - parent_cell_id, - curving_status, - std::span(child.data(), child.size()), - 0, - max_subdivision_depth); -} - -template -void append_linear_entity_to_curved_grid(CurvedVTUGrid& out, - const AdaptCell& adapt_cell, - const SelectedEntity& entity, - std::span parent_vertex_coords, - int parent_cell_id) -{ - const auto ref_coords = entity_reference_coords( - adapt_cell, - std::span(entity.vertices.data(), entity.vertices.size())); - const std::vector parent_coords(parent_vertex_coords.begin(), - parent_vertex_coords.end()); - const auto phys_coords = cell::push_forward_affine_map( - adapt_cell.parent_cell_type, - parent_coords, - out.gdim, - std::span(ref_coords.data(), ref_coords.size())); - - const int point_base = static_cast(out.points.size()) / out.gdim; - out.points.insert(out.points.end(), phys_coords.begin(), phys_coords.end()); - - if (is_simplex(entity.type)) - { - for (int i = 0; i < static_cast(entity.vertices.size()); ++i) - out.connectivity.push_back(point_base + i); + const T det = + J[0] * (J[4] * J[8] - J[7] * J[5]) + - J[3] * (J[1] * J[8] - J[7] * J[2]) + + J[6] * (J[1] * J[5] - J[4] * J[2]); + return std::abs(det); } - else + + T G[9] = {}; + for (int i = 0; i < simplex_dim; ++i) { - const auto perm = cell::basix_to_vtk_vertex_permutation(entity.type); - for (const int p : perm) - out.connectivity.push_back(point_base + p); + for (int j = 0; j < simplex_dim; ++j) + { + T sum = 0; + for (int k = 0; k < gdim; ++k) + sum += J[i * gdim + k] * J[j * gdim + k]; + G[i * simplex_dim + j] = sum; + } } - out.offsets.push_back(static_cast(out.connectivity.size())); - out.vtk_types.push_back(static_cast(cell::map_cell_type_to_vtk(entity.type))); - out.parent_map.push_back(static_cast(parent_cell_id)); - out.curved_valid.push_back(0); - out.subdivision_depth.push_back(0); - out.curving_status.push_back(0); + if (simplex_dim == 1) + return std::sqrt(G[0]); + if (simplex_dim == 2) + return std::sqrt(G[0] * G[3] - G[1] * G[2]); + + const T det = + G[0] * (G[4] * G[8] - G[7] * G[5]) + - G[3] * (G[1] * G[8] - G[7] * G[2]) + + G[6] * (G[1] * G[5] - G[4] * G[2]); + return std::sqrt(det); } template -void append_curved_child_simplex_quadrature(quadrature::QuadratureRules& rules, - const LocalCurvedSimplexMap& map, - int parent_cell_id, - int order, - std::span child_vertices, - int subdivision_depth, - int max_subdivision_depth) +void append_mesh_entity(mesh::CutMesh& out, + std::span physical_coords, + int gdim, + cell::type cell_type, + int parent_cell_id, + bool input_is_basix) { - if (rules._tdim == 0) - rules._tdim = map.parent_tdim; - if (rules._offset.empty()) - rules._offset.push_back(0); + if (out._gdim == 0) + out._gdim = gdim; + if (out._tdim == 0) + out._tdim = cell::get_tdim(cell_type); - const bool valid = curved_child_map_valid(map, child_vertices); - if (!valid && subdivision_depth < max_subdivision_depth) + std::vector reordered_coords; + std::span output_coords = physical_coords; + if (!input_is_basix && !is_simplex(cell_type)) { - const auto children = subdivide_child_simplex(map.simplex_type, child_vertices); - for (const auto& child : children) - { - append_curved_child_simplex_quadrature( - rules, - map, - parent_cell_id, - order, - std::span(child.data(), child.size()), - subdivision_depth + 1, - max_subdivision_depth); - } - return; + const auto perm = cell::vtk_to_basix_vertex_permutation(cell_type); + reordered_coords = cell::permute_vertex_data(physical_coords, gdim, perm); + output_coords = std::span( + reordered_coords.data(), reordered_coords.size()); } - const auto ref_rule = quadrature::get_reference_rule(map.simplex_type, order); - for (int q = 0; q < ref_rule._num_points; ++q) - { - std::span xi_child( - ref_rule._points.data() + static_cast(q * ref_rule._tdim), - static_cast(ref_rule._tdim)); - const auto xi = map_child_xi_to_parent_xi( - map.simplex_type, - child_vertices, - xi_child); - std::vector ref; - if (valid) - ref = curved_map_ref_point(map, std::span(xi.data(), xi.size())); - else - { - ref = straight_ref_from_cell_point(map, std::span(xi.data(), xi.size())); - } - rules._points.insert(rules._points.end(), ref.begin(), ref.end()); + const int nv = static_cast(output_coords.size()) / gdim; + const int vertex_base = out._num_vertices; + out._vertex_coords.insert( + out._vertex_coords.end(), output_coords.begin(), output_coords.end()); + out._num_vertices += nv; - const T child_measure = child_map_measure( - map.simplex_type, child_vertices, xi_child); - T measure = T(0); - if (valid) - { - measure = std::abs(curved_map_measure( - map, std::span(xi.data(), xi.size()))) * child_measure; - } - else - { - measure = straight_map_measure( - map, std::span(xi.data(), xi.size())) * child_measure; - } - rules._weights.push_back(ref_rule._weights[static_cast(q)] * measure); - } - rules._parent_map.push_back(static_cast(parent_cell_id)); - rules._offset.push_back(static_cast(rules._weights.size())); + for (int lv = 0; lv < nv; ++lv) + out._connectivity.push_back(vertex_base + lv); + out._offset.push_back(static_cast(out._connectivity.size())); + out._types.push_back(cell_type); + out._parent_map.push_back(parent_cell_id); + out._num_cells += 1; } template -void append_curved_simplex_quadrature(quadrature::QuadratureRules& rules, - const LocalCurvedSimplexMap& map, - int parent_cell_id, - int order, - int max_subdivision_depth) +void append_simplex_quadrature(quadrature::QuadratureRules& rules, + cell::type simplex_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int order) { - const auto child = canonical_simplex_vertices(map.simplex_type); - append_curved_child_simplex_quadrature( - rules, - map, - parent_cell_id, - order, - std::span(child.data(), child.size()), - 0, - max_subdivision_depth); -} + const auto ref_rule = quadrature::get_reference_rule(simplex_type, order); + const int num_points = ref_rule._num_points; + const int simplex_dim = ref_rule._tdim; -template -LocalCurvedSimplexMap make_local_map(const HOMeshPart& part, - const AdaptCell& adapt_cell, - int cut_cell_id, - std::span simplex_vertices, - cell::type simplex_type, - std::span parent_vertex_coords, - int geometry_order, - curving::NodeFamily node_family, - const curving::CurvingOptions& options, - int required_zero_entity_index = -1) -{ - const auto& mesh = *part.bg->mesh; - LocalCurvedSimplexMap map; - map.simplex_type = simplex_type; - map.dim = cell::get_tdim(simplex_type); - map.parent_tdim = mesh.tdim; - map.gdim = mesh.gdim; - map.parent_cell_type = adapt_cell.parent_cell_type; - map.geometry_order = geometry_order; - map.node_family = node_family; - map.vertices.assign(simplex_vertices.begin(), simplex_vertices.end()); - map.parent_physical_coords.assign(parent_vertex_coords.begin(), parent_vertex_coords.end()); - map.ref_vertex_coords = entity_reference_coords(adapt_cell, simplex_vertices); - - attach_curved_boundaries( - map, part, adapt_cell, cut_cell_id, options, required_zero_entity_index); - return map; + std::vector mapped_ref_points( + static_cast(num_points * parent_tdim), T(0)); + map_canonical_to_subcell_points( + ref_rule._points.data(), + num_points, + simplex_dim, + ref_vertices.data(), + parent_tdim, + mapped_ref_points.data()); + + rules._points.insert( + rules._points.end(), mapped_ref_points.begin(), mapped_ref_points.end()); + + const T measure = simplex_physical_measure( + physical_vertices.data(), simplex_dim, gdim); + for (int q = 0; q < num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); } -template -void append_curved_entity(CurvedVTUGrid& out, - const HOMeshPart& part, - const AdaptCell& adapt_cell, - int cut_cell_id, - const SelectedEntity& entity, - std::span parent_vertex_coords, - int geometry_order, - curving::NodeFamily node_family, - const curving::CurvingOptions& options) +template +void append_entity_quadrature(quadrature::QuadratureRules& rules, + cell::type cell_type, + std::span ref_vertices, + std::span physical_vertices, + int parent_tdim, + int gdim, + int parent_cell_id, + int order) { - const int entity_dim = cell::get_tdim(entity.type); - if (entity_dim == 0) - return; + if (rules._tdim == 0) + rules._tdim = parent_tdim; + if (rules._offset.empty()) + rules._offset.push_back(0); - std::vector> simplices; - cell::type simplex_type = entity.type; - if (is_simplex(entity.type)) - { - simplices.push_back(entity.vertices); - } - else if (supports_curved_lagrange_cell(entity.type)) - { - simplices.push_back(entity.vertices); - } - else + if (cell_type == cell::type::point) { - append_linear_entity_to_curved_grid( - out, - adapt_cell, - entity, - parent_vertex_coords, - static_cast( - part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)])); + rules._points.insert( + rules._points.end(), + ref_vertices.begin(), + ref_vertices.begin() + parent_tdim); + rules._weights.push_back(T(1)); + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); return; } - for (const auto& simplex_vertices : simplices) - { - auto map = make_local_map( - part, - adapt_cell, - cut_cell_id, - std::span(simplex_vertices.data(), simplex_vertices.size()), - simplex_type, - parent_vertex_coords, - geometry_order, - node_family, - options, - entity.zero_entity_index); - int status = 0; - if (entity.zero_entity_index >= 0) - { - if (graph_checks_allow_zero_entity_curving( - part, cut_cell_id, entity.zero_entity_index)) - { - const auto& state = curving::ensure_curved( - part.cut_cells->curving, - std::span(part.cut_cells->parent_cell_ids), - std::span>(part.cut_cells->adapt_cells), - std::span>(part.cut_cells->level_set_cells), - std::span(part.cut_cells->ls_offsets), - cut_cell_id, - entity.zero_entity_index, - options); - status = static_cast(state.status); - } - else - { - status = static_cast(curving::CurvingStatus::failed); - } - } - else if (!map.curved_faces.empty() || !map.curved_edges.empty()) - { - status = static_cast(curving::CurvingStatus::curved); - } - append_lagrange_simplex( - out, map, - static_cast(part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]), - status, - options.max_subdivision_depth); - } -} - -template -void append_curved_entity_quadrature(quadrature::QuadratureRules& rules, - const HOMeshPart& part, - const AdaptCell& adapt_cell, - int cut_cell_id, - const SelectedEntity& entity, - std::span parent_vertex_coords, - int geometry_order, - curving::NodeFamily node_family, - const curving::CurvingOptions& options, - int quadrature_order) -{ - const int entity_dim = cell::get_tdim(entity.type); - if (entity_dim == 0) - return; - - std::vector> simplices; - cell::type simplex_type = entity.type; - if (is_simplex(entity.type)) + const int entity_dim = cell::get_tdim(cell_type); + if (is_simplex(cell_type)) { - simplices.push_back(entity.vertices); + append_simplex_quadrature( + rules, cell_type, ref_vertices, physical_vertices, + parent_tdim, gdim, order); } - else if (supports_curved_lagrange_cell(entity.type)) + else if (entity_dim >= 2) { - simplices.push_back(entity.vertices); + const auto ref_rule = quadrature::get_reference_rule(cell_type, order); + const T measure = cell::affine_volume_factor( + cell_type, physical_vertices.data(), gdim); + rules._points.insert( + rules._points.end(), ref_rule._points.begin(), ref_rule._points.end()); + for (int q = 0; q < ref_rule._num_points; ++q) + rules._weights.push_back(ref_rule._weights[q] * measure); } else { - return; - } - - const int parent_cell_id = - static_cast(part.cut_cells->parent_cell_ids[static_cast(cut_cell_id)]); - for (const auto& simplex_vertices : simplices) - { - auto map = make_local_map( - part, - adapt_cell, - cut_cell_id, - std::span(simplex_vertices.data(), simplex_vertices.size()), - simplex_type, - parent_vertex_coords, - geometry_order, - node_family, - options, - entity.zero_entity_index); - append_curved_simplex_quadrature( - rules, map, parent_cell_id, quadrature_order, - options.max_subdivision_depth); - } -} - -template -void append_curved_cut_quadrature(quadrature::QuadratureRules& rules, - const HOMeshPart& part, - int quadrature_order, - int geometry_order, - curving::NodeFamily node_family, - const curving::CurvingOptions& options) -{ - const auto& mesh = *part.bg->mesh; - for (std::int32_t cut_id : part.cut_cell_ids) - { - const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; - const auto entities = selected_entities(part, adapt_cell, cut_id); - if (entities.empty()) - continue; + std::vector local_ids( + static_cast(ref_vertices.size() / parent_tdim)); + std::iota(local_ids.begin(), local_ids.end(), 0); + std::vector> simplices; + cell::triangulation(cell_type, local_ids.data(), simplices); + const auto simplex_type = simplex_type_for_dim(entity_dim); - const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; - const auto cut_active_mask = - part.cut_cells->active_level_set_mask[static_cast(cut_id)]; - const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); - for (const auto& entity : entities) + std::vector ref_simplex; + std::vector phys_simplex; + for (const auto& simplex : simplices) { - append_curved_entity_quadrature( + gather_subcell_vertices( + ref_vertices, parent_tdim, + std::span(simplex.data(), simplex.size()), + ref_simplex); + gather_subcell_vertices( + physical_vertices, gdim, + std::span(simplex.data(), simplex.size()), + phys_simplex); + append_simplex_quadrature( rules, - part, - adapt_cell, - cut_id, - entity, - std::span(parent_vertex_coords.data(), parent_vertex_coords.size()), - geometry_order, - node_family, - options, - quadrature_order); + simplex_type, + std::span(ref_simplex.data(), ref_simplex.size()), + std::span(phys_simplex.data(), phys_simplex.size()), + parent_tdim, + gdim, + order); } } + + rules._parent_map.push_back(parent_cell_id); + rules._offset.push_back(static_cast(rules._weights.size())); } template @@ -3252,42 +574,22 @@ void append_cut_entities(mesh::CutMesh& out, const auto& mesh = *part.bg->mesh; for (std::int32_t cut_id : part.cut_cell_ids) { - const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; + const auto& adapt_cell = + part.cut_cells->adapt_cells[static_cast(cut_id)]; const auto entities = selected_entities(part, adapt_cell, cut_id); if (entities.empty()) continue; - const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; - const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); + const I parent_cell_id = + part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + const auto parent_vertex_coords = + parent_cell_vertex_coords_vtk(mesh, parent_cell_id); for (const auto& entity : entities) { - std::vector root_vertex_flags; - if ((entity.type == cell::type::prism - && adapt_cell.parent_cell_type == cell::type::tetrahedron) - || (entity.type == cell::type::quadrilateral - && adapt_cell.parent_cell_type == cell::type::triangle)) - { - root_vertex_flags.resize(entity.vertices.size(), 0); - for (std::size_t j = 0; j < entity.vertices.size(); ++j) - { - const auto zm = adapt_cell.zero_mask_per_vertex[ - static_cast(entity.vertices[j])]; - bool is_root = false; - if (part.expr.zero_required != 0) - { - is_root = (zm & part.expr.zero_required) == part.expr.zero_required; - } - else - { - is_root = zm != 0; - } - root_vertex_flags[j] = is_root ? 1 : 0; - } - } - const auto ref_coords = entity_reference_coords( - adapt_cell, std::span(entity.vertices.data(), entity.vertices.size())); + adapt_cell, + std::span(entity.vertices.data(), entity.vertices.size())); const auto phys_coords = cell::push_forward_affine_map( adapt_cell.parent_cell_type, parent_vertex_coords, @@ -3300,10 +602,7 @@ void append_cut_entities(mesh::CutMesh& out, mesh.gdim, entity.type, static_cast(parent_cell_id), - /*triangulate=*/false, - /*input_is_basix=*/true, - adapt_cell.parent_cell_type, - std::span(root_vertex_flags.data(), root_vertex_flags.size())); + /*input_is_basix=*/true); if (rules != nullptr) { @@ -3315,11 +614,7 @@ void append_cut_entities(mesh::CutMesh& out, mesh.tdim, mesh.gdim, static_cast(parent_cell_id), - quadrature_order, - /*triangulate=*/false, - /*input_is_basix=*/true, - adapt_cell.parent_cell_type, - std::span(root_vertex_flags.data(), root_vertex_flags.size())); + quadrature_order); } } } @@ -3345,10 +640,7 @@ void append_uncut_volume_cells(mesh::CutMesh& out, mesh.gdim, ctype, static_cast(cell_id), - /*triangulate=*/false, - /*input_is_basix=*/false, - cell::type::point, - std::span()); + /*input_is_basix=*/false); if (rules != nullptr) { @@ -3361,58 +653,57 @@ void append_uncut_volume_cells(mesh::CutMesh& out, mesh.tdim, mesh.gdim, static_cast(cell_id), - quadrature_order, - /*triangulate=*/false, - /*input_is_basix=*/false, - cell::type::point, - std::span()); + quadrature_order); } } } +} // namespace + template -void append_uncut_curved_cells(CurvedVTUGrid& out, - const HOMeshPart& part, - int geometry_order, - curving::NodeFamily node_family) +std::vector selected_zero_entity_infos( + const HOMeshPart& part) { - const auto& mesh = *part.bg->mesh; - if (part.dim != mesh.tdim) - return; + if (!part.cut_cells || !part.bg) + throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - for (I cell_id : part.uncut_cell_ids) + std::vector infos; + if (part.dim == part.cut_cells->tdim) + return infos; + + for (std::int32_t cut_id : part.cut_cell_ids) { - const auto ctype = mesh.cell_type(cell_id); - if (!supports_curved_lagrange_cell(ctype)) - continue; + const auto& ac = + part.cut_cells->adapt_cells[static_cast(cut_id)]; + const I parent_cell_id = + part.cut_cells->parent_cell_ids[static_cast(cut_id)]; + const std::uint64_t cut_active_mask = + part.cut_cells->active_level_set_mask[static_cast(cut_id)]; + + const int n_zero = ac.n_zero_entities(); + for (int z = 0; z < n_zero; ++z) + { + if (!zero_entity_matches( + ac, z, part.dim, part.expr, cut_active_mask, + *part.bg, parent_cell_id)) + { + continue; + } - const auto parent_coords = parent_cell_vertex_coords_vtk(mesh, cell_id); - const auto ref_vertices = cell::reference_vertices(ctype); - const int nv = cell::get_num_vertices(ctype); - LocalCurvedSimplexMap map; - map.simplex_type = ctype; - map.dim = cell::get_tdim(ctype); - map.parent_tdim = mesh.tdim; - map.gdim = mesh.gdim; - map.parent_cell_type = ctype; - map.geometry_order = geometry_order; - map.node_family = node_family; - map.vertices.resize(static_cast(nv)); - std::iota(map.vertices.begin(), map.vertices.end(), 0); - map.ref_vertex_coords = ref_vertices; - map.parent_physical_coords = parent_coords; - append_lagrange_simplex(out, map, static_cast(cell_id), 0, 0); + infos.push_back({ + cut_id, + static_cast(parent_cell_id), + static_cast(z), + static_cast(part.dim)}); + } } -} -} // namespace + return infos; +} template mesh::CutMesh visualization_mesh(const HOMeshPart& part, - bool include_uncut_cells, - int geometry_order, - curving::NodeFamily node_family, - curving::CurvingDirectionMode direction_mode) + bool include_uncut_cells) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); @@ -3442,107 +733,30 @@ mesh::CutMesh visualization_mesh(const HOMeshPart& part, template quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, - bool include_uncut_cells, - int geometry_order, - curving::NodeFamily node_family, - curving::CurvingDirectionMode direction_mode) + bool include_uncut_cells) { if (!part.cut_cells || !part.bg || !part.bg->mesh) throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - - curving::CurvingOptions options; - if (geometry_order > 1) - { - options.geometry_order = geometry_order; - options.node_family = construction_node_family(geometry_order, node_family); - options.direction_mode = direction_mode; - } mesh::CutMesh unused_mesh; quadrature::QuadratureRules rules; rules._offset.push_back(0); - if (geometry_order > 1) - { - append_curved_cut_quadrature( - rules, part, order, geometry_order, options.node_family, options); - } - else - { - append_cut_entities(unused_mesh, &rules, part, order); - } + append_cut_entities(unused_mesh, &rules, part, order); if (include_uncut_cells) append_uncut_volume_cells(unused_mesh, &rules, part, order); return rules; } -template -CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, - bool include_uncut_cells, - int geometry_order, - curving::NodeFamily node_family, - curving::CurvingDirectionMode direction_mode) -{ - if (!part.cut_cells || !part.bg || !part.bg->mesh) - throw std::runtime_error("HOMeshPart is not attached to cut-cell storage"); - if (geometry_order <= 1) - throw std::runtime_error("curved_lagrange_grid requires geometry_order > 1"); - - curving::CurvingOptions options; - options.geometry_order = geometry_order; - options.node_family = construction_node_family(geometry_order, node_family); - options.direction_mode = direction_mode; - - CurvedVTUGrid out; - out.gdim = part.bg->mesh->gdim; - out.tdim = part.dim; - out.geometry_order = geometry_order; - out.offsets.push_back(0); - - const auto& mesh = *part.bg->mesh; - for (std::int32_t cut_id : part.cut_cell_ids) - { - const auto& adapt_cell = part.cut_cells->adapt_cells[static_cast(cut_id)]; - const auto entities = selected_entities(part, adapt_cell, cut_id); - if (entities.empty()) - continue; - - const I parent_cell_id = part.cut_cells->parent_cell_ids[static_cast(cut_id)]; - const auto parent_vertex_coords = parent_cell_vertex_coords_vtk(mesh, parent_cell_id); - for (const auto& entity : entities) - { - append_curved_entity( - out, - part, - adapt_cell, - cut_id, - entity, - std::span(parent_vertex_coords.data(), parent_vertex_coords.size()), - geometry_order, - options.node_family, - options); - } - } - - if (include_uncut_cells) - append_uncut_curved_cells(out, part, geometry_order, node_family); - - return out; -} - template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, bool); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, bool); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, bool); template mesh::CutMesh visualization_mesh( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, bool); template std::vector selected_zero_entity_infos( const HOMeshPart&); @@ -3554,29 +768,12 @@ template std::vector selected_zero_entity_infos( const HOMeshPart&); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, int, bool); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, int, bool); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, int, bool); template quadrature::QuadratureRules quadrature_rules( - const HOMeshPart&, int, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); - -template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); -template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); -template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); -template CurvedVTUGrid curved_lagrange_grid( - const HOMeshPart&, bool, int, curving::NodeFamily, - curving::CurvingDirectionMode); + const HOMeshPart&, int, bool); } // namespace cutcells::output diff --git a/cpp/src/ho_mesh_part_output.h b/cpp/src/ho_mesh_part_output.h index 2fb37b3..13994a1 100644 --- a/cpp/src/ho_mesh_part_output.h +++ b/cpp/src/ho_mesh_part_output.h @@ -10,31 +10,12 @@ #include #include "cut_mesh.h" -#include "curving.h" #include "ho_cut_mesh.h" #include "quadrature.h" namespace cutcells::output { -template -struct CurvedVTUGrid -{ - int gdim = 0; - int tdim = 0; - int geometry_order = 1; - - std::vector points; - std::vector connectivity; - std::vector offsets; - std::vector vtk_types; - - std::vector parent_map; - std::vector curved_valid; - std::vector subdivision_depth; - std::vector curving_status; -}; - struct SelectedZeroEntityInfo { std::int32_t cut_cell_id = -1; @@ -49,27 +30,11 @@ std::vector selected_zero_entity_infos( template mesh::CutMesh visualization_mesh(const HOMeshPart& part, - bool include_uncut_cells, - int geometry_order = -1, - curving::NodeFamily node_family = curving::NodeFamily::gll, - curving::CurvingDirectionMode direction_mode = - curving::CurvingDirectionMode::straight_zero_entity_normal); + bool include_uncut_cells); template quadrature::QuadratureRules quadrature_rules(const HOMeshPart& part, int order, - bool include_uncut_cells, - int geometry_order = -1, - curving::NodeFamily node_family = curving::NodeFamily::gll, - curving::CurvingDirectionMode direction_mode = - curving::CurvingDirectionMode::straight_zero_entity_normal); - -template -CurvedVTUGrid curved_lagrange_grid(const HOMeshPart& part, - bool include_uncut_cells, - int geometry_order, - curving::NodeFamily node_family = curving::NodeFamily::lagrange, - curving::CurvingDirectionMode direction_mode = - curving::CurvingDirectionMode::straight_zero_entity_normal); + bool include_uncut_cells); } // namespace cutcells::output diff --git a/cpp/src/tangent_shift.h b/cpp/src/tangent_shift.h deleted file mode 100644 index 1443bb3..0000000 --- a/cpp/src/tangent_shift.h +++ /dev/null @@ -1,873 +0,0 @@ -// Copyright (c) 2026 ONERA -// Authors: Susanne Claus -// This file is part of CutCells -// -// SPDX-License-Identifier: MIT -#pragma once - -#include "geometric_quantity.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cutcells::tangent_shift -{ - -enum class FailureReason : std::uint8_t -{ - none = 0, - invalid_input, - invalid_parent_entity, - invalid_curve, - node_index_out_of_range, - target_arclength_out_of_range, - current_point_outside_parent_entity, - correction_direction_not_admissible, - correction_left_parent_entity, - correction_failed, - phi_residual_too_large, - ordering_destroyed, - spacing_too_small, - arclength_error_too_large, - refinement_requested -}; - -inline std::string_view failure_reason_name(FailureReason reason) -{ - switch (reason) - { - case FailureReason::none: - return "none"; - case FailureReason::invalid_input: - return "invalid_input"; - case FailureReason::invalid_parent_entity: - return "invalid_parent_entity"; - case FailureReason::invalid_curve: - return "invalid_curve"; - case FailureReason::node_index_out_of_range: - return "node_index_out_of_range"; - case FailureReason::target_arclength_out_of_range: - return "target_arclength_out_of_range"; - case FailureReason::current_point_outside_parent_entity: - return "current_point_outside_parent_entity"; - case FailureReason::correction_direction_not_admissible: - return "correction_direction_not_admissible"; - case FailureReason::correction_left_parent_entity: - return "correction_left_parent_entity"; - case FailureReason::correction_failed: - return "correction_failed"; - case FailureReason::phi_residual_too_large: - return "phi_residual_too_large"; - case FailureReason::ordering_destroyed: - return "ordering_destroyed"; - case FailureReason::spacing_too_small: - return "spacing_too_small"; - case FailureReason::arclength_error_too_large: - return "arclength_error_too_large"; - case FailureReason::refinement_requested: - return "refinement_requested"; - } - return "unknown"; -} - -template -struct Options -{ - T tolerance = geom::default_tolerance(); - T phi_tolerance = std::sqrt(std::numeric_limits::epsilon()); - T min_direction_norm = T(64) * std::numeric_limits::epsilon(); - - T max_relative_arclength_drift = T(1.0e-3); - T max_absolute_arclength_drift = std::sqrt(std::numeric_limits::epsilon()); - T max_relative_final_arclength_error = T(1.0e-3); - T max_absolute_final_arclength_error = std::sqrt(std::numeric_limits::epsilon()); - - T min_relative_spacing = std::sqrt(std::numeric_limits::epsilon()); - T min_absolute_spacing = T(0); - - int max_correction_iterations = 16; - int max_line_search_iterations = 16; -}; - -template -struct Metrics -{ - T curve_length = T(0); - T desired_arclength = T(0); - T actual_arclength = T(0); - T initial_arclength_error = T(0); - T final_arclength = T(0); - T final_arclength_error = T(0); - T phi_residual = std::numeric_limits::infinity(); - T minimum_spacing = std::numeric_limits::infinity(); - T minimum_spacing_ratio = std::numeric_limits::infinity(); -}; - -template -struct CorrectionReport -{ - bool accepted = false; - bool left_parent_entity = false; - FailureReason failure_reason = FailureReason::none; - std::vector point; - T residual = std::numeric_limits::infinity(); - int iterations = 0; -}; - -template -struct ShiftReport -{ - bool accepted = false; - bool corrected = false; - bool request_refinement = false; - FailureReason failure_reason = FailureReason::none; - - Metrics metrics; - int correction_iterations = 0; - - std::vector shifted_seed; - std::vector corrected_node; - std::vector corrected_edge_points; -}; - -template -struct ArclengthPoint -{ - bool valid = false; - std::vector point; - std::size_t segment = 0; - T segment_parameter = T(0); -}; - -template -struct ClosestArclength -{ - bool valid = false; - T arclength = T(0); - T distance = std::numeric_limits::infinity(); - std::size_t segment = 0; - T segment_parameter = T(0); -}; - -template -inline std::span vector_span(const std::vector& v) -{ - return std::span(v.data(), v.size()); -} - -template -inline std::span point_span(std::span points, - int point_dim, - std::size_t i) -{ - return points.subspan( - i * static_cast(point_dim), - static_cast(point_dim)); -} - -template -inline std::span mutable_point_span(std::span points, - int point_dim, - std::size_t i) -{ - return points.subspan( - i * static_cast(point_dim), - static_cast(point_dim)); -} - -template -inline bool flat_point_array_valid(std::span points, int point_dim) -{ - return point_dim > 0 - && !points.empty() - && points.size() % static_cast(point_dim) == 0; -} - -template -inline T point_distance(std::span a, std::span b) -{ - const auto delta = geom::subtract(a, b); - return geom::norm(vector_span(delta)); -} - -template -inline T arclength_tolerance(T length, T absolute_tolerance, T relative_tolerance) -{ - return absolute_tolerance + relative_tolerance * length; -} - -template -inline bool cumulative_arclength(std::span points, - int point_dim, - std::vector& cumulative, - T tol = geom::default_tolerance()) -{ - if (!flat_point_array_valid(points, point_dim)) - return false; - - const std::size_t n = points.size() / static_cast(point_dim); - if (n < 2) - return false; - - cumulative.assign(n, T(0)); - for (std::size_t i = 1; i < n; ++i) - { - const T ds = point_distance( - point_span(points, point_dim, i - 1), - point_span(points, point_dim, i)); - if (!std::isfinite(ds)) - return false; - cumulative[i] = cumulative[i - 1] + ds; - } - - return cumulative.back() > tol; -} - -template -inline ArclengthPoint point_at_arclength(std::span points, - int point_dim, - T target_arclength, - T tol = geom::default_tolerance()) -{ - ArclengthPoint out; - std::vector cumulative; - if (!cumulative_arclength(points, point_dim, cumulative, tol)) - return out; - - const T length = cumulative.back(); - if (target_arclength < -tol || target_arclength > length + tol) - return out; - - const std::size_t n = cumulative.size(); - if (target_arclength <= tol) - { - out.valid = true; - out.point.assign(points.begin(), points.begin() + point_dim); - out.segment = 0; - out.segment_parameter = T(0); - return out; - } - if (length - target_arclength <= tol) - { - out.valid = true; - out.point.assign( - points.end() - static_cast(point_dim), - points.end()); - out.segment = n - 2; - out.segment_parameter = T(1); - return out; - } - - for (std::size_t i = 1; i < n; ++i) - { - if (target_arclength > cumulative[i] + tol) - continue; - - const T segment_length = cumulative[i] - cumulative[i - 1]; - if (segment_length <= tol) - continue; - - T u = (target_arclength - cumulative[i - 1]) / segment_length; - if (u < -tol || u > T(1) + tol) - return out; - if (u < T(0)) - u = T(0); - if (u > T(1)) - u = T(1); - - const auto a = point_span(points, point_dim, i - 1); - const auto b = point_span(points, point_dim, i); - out.point.assign(static_cast(point_dim), T(0)); - for (int d = 0; d < point_dim; ++d) - out.point[static_cast(d)] = - (T(1) - u) * a[static_cast(d)] - + u * b[static_cast(d)]; - out.valid = true; - out.segment = i - 1; - out.segment_parameter = u; - return out; - } - - return out; -} - -template -inline ClosestArclength closest_arclength_on_polyline( - std::span points, - int point_dim, - std::span x, - T tol = geom::default_tolerance()) -{ - ClosestArclength out; - if (!flat_point_array_valid(points, point_dim) - || x.size() != static_cast(point_dim)) - { - return out; - } - - std::vector cumulative; - if (!cumulative_arclength(points, point_dim, cumulative, tol)) - return out; - - const std::size_t n = cumulative.size(); - T best_distance2 = std::numeric_limits::infinity(); - for (std::size_t i = 1; i < n; ++i) - { - const auto a = point_span(points, point_dim, i - 1); - const auto b = point_span(points, point_dim, i); - const auto ab = geom::subtract(b, a); - const T ab2 = geom::squared_norm(vector_span(ab)); - if (ab2 <= tol * tol) - continue; - - T u = T(0); - for (int d = 0; d < point_dim; ++d) - u += (x[static_cast(d)] - a[static_cast(d)]) - * ab[static_cast(d)]; - u /= ab2; - const T u_closest = std::clamp(u, T(0), T(1)); - - T distance2 = T(0); - for (int d = 0; d < point_dim; ++d) - { - const T closest = - a[static_cast(d)] - + u_closest * ab[static_cast(d)]; - const T r = x[static_cast(d)] - closest; - distance2 += r * r; - } - - if (distance2 < best_distance2) - { - best_distance2 = distance2; - out.valid = true; - out.distance = std::sqrt(distance2); - out.segment = i - 1; - out.segment_parameter = u_closest; - out.arclength = - cumulative[i - 1] - + u_closest * (cumulative[i] - cumulative[i - 1]); - } - } - - return out; -} - -template -struct LayoutReport -{ - bool accepted = false; - FailureReason failure_reason = FailureReason::none; - std::vector arclengths; - T minimum_spacing = std::numeric_limits::infinity(); - T minimum_spacing_ratio = std::numeric_limits::infinity(); -}; - -template -inline LayoutReport evaluate_ordering_and_spacing( - std::span provisional_curve_points, - std::span edge_points, - int point_dim, - const Options& options = {}) -{ - LayoutReport report; - if (!flat_point_array_valid(provisional_curve_points, point_dim) - || !flat_point_array_valid(edge_points, point_dim)) - { - report.failure_reason = FailureReason::invalid_input; - return report; - } - - const std::size_t n = - edge_points.size() / static_cast(point_dim); - if (n < 2) - { - report.failure_reason = FailureReason::invalid_input; - return report; - } - - std::vector cumulative; - if (!cumulative_arclength( - provisional_curve_points, point_dim, cumulative, options.tolerance)) - { - report.failure_reason = FailureReason::invalid_curve; - return report; - } - const T length = cumulative.back(); - const T min_spacing = - options.min_absolute_spacing + options.min_relative_spacing * length; - const T ordering_tol = options.tolerance * std::max(T(1), length); - - report.arclengths.assign(n, T(0)); - for (std::size_t i = 0; i < n; ++i) - { - const auto closest = closest_arclength_on_polyline( - provisional_curve_points, - point_dim, - point_span(edge_points, point_dim, i), - options.tolerance); - if (!closest.valid) - { - report.failure_reason = FailureReason::invalid_curve; - return report; - } - report.arclengths[i] = closest.arclength; - } - - for (std::size_t i = 1; i < n; ++i) - { - const T gap = report.arclengths[i] - report.arclengths[i - 1]; - if (gap < -ordering_tol) - { - report.failure_reason = FailureReason::ordering_destroyed; - return report; - } - - report.minimum_spacing = std::min(report.minimum_spacing, gap); - if (gap <= min_spacing) - { - report.minimum_spacing_ratio = length > options.tolerance - ? gap / length - : std::numeric_limits::infinity(); - report.failure_reason = FailureReason::spacing_too_small; - return report; - } - } - - report.accepted = true; - report.failure_reason = FailureReason::none; - report.minimum_spacing_ratio = length > options.tolerance - ? report.minimum_spacing / length - : std::numeric_limits::infinity(); - return report; -} - -template -inline CorrectionReport correct_seed_to_zero( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - std::span seed, - Phi&& phi, - Grad&& grad, - const Options& options = {}) -{ - CorrectionReport report; - const int tdim = cell::get_tdim(parent_cell_type); - if (!admissible_parent_entity.valid() - || admissible_parent_entity.dim > tdim - || static_cast(seed.size()) != tdim) - { - report.failure_reason = FailureReason::invalid_parent_entity; - return report; - } - - if (!geom::point_in_parent_entity( - parent_cell_type, admissible_parent_entity, seed, options.tolerance)) - { - report.failure_reason = FailureReason::correction_left_parent_entity; - report.left_parent_entity = true; - return report; - } - - report.point.assign(seed.begin(), seed.end()); - T f = static_cast(std::invoke( - phi, - std::span(report.point.data(), report.point.size()))); - report.residual = std::fabs(f); - if (!std::isfinite(report.residual)) - { - report.failure_reason = FailureReason::correction_failed; - return report; - } - if (report.residual <= options.phi_tolerance) - { - report.accepted = true; - report.failure_reason = FailureReason::none; - return report; - } - - std::vector g(static_cast(tdim), T(0)); - bool full_step_left_parent = false; - for (int iter = 0; iter < options.max_correction_iterations; ++iter) - { - std::fill(g.begin(), g.end(), T(0)); - std::invoke( - grad, - std::span(report.point.data(), report.point.size()), - std::span(g.data(), g.size())); - - const auto direction = geom::admissible_direction_in_parent_frame( - parent_cell_type, - admissible_parent_entity, - std::span(g.data(), g.size()), - options.tolerance); - if (direction.degenerate() || direction.norm <= options.min_direction_norm) - { - report.failure_reason = FailureReason::correction_direction_not_admissible; - return report; - } - - const T denom = geom::dot( - std::span(g.data(), g.size()), - vector_span(direction.value)); - if (std::fabs(denom) <= options.min_direction_norm * direction.norm) - { - report.failure_reason = FailureReason::correction_direction_not_admissible; - return report; - } - - std::vector step(static_cast(tdim), T(0)); - const T scale = -f / denom; - for (int d = 0; d < tdim; ++d) - step[static_cast(d)] = - scale * direction.value[static_cast(d)]; - if (geom::norm(vector_span(step)) <= options.tolerance) - break; - - std::vector full_step_point(report.point); - for (int d = 0; d < tdim; ++d) - full_step_point[static_cast(d)] += - step[static_cast(d)]; - if (!geom::point_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - vector_span(full_step_point), - options.tolerance)) - { - full_step_left_parent = true; - report.left_parent_entity = true; - } - - bool accepted_step = false; - std::vector candidate(static_cast(tdim), T(0)); - T candidate_f = f; - T alpha = T(1); - for (int ls = 0; ls < options.max_line_search_iterations; ++ls) - { - for (int d = 0; d < tdim; ++d) - candidate[static_cast(d)] = - report.point[static_cast(d)] - + alpha * step[static_cast(d)]; - - if (!geom::point_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - vector_span(candidate), - options.tolerance)) - { - alpha *= T(0.5); - continue; - } - - candidate_f = static_cast(std::invoke( - phi, - std::span(candidate.data(), candidate.size()))); - if (!std::isfinite(candidate_f)) - { - alpha *= T(0.5); - continue; - } - if (std::fabs(candidate_f) <= options.phi_tolerance - || std::fabs(candidate_f) < report.residual) - { - accepted_step = true; - break; - } - alpha *= T(0.5); - } - - if (!accepted_step) - { - report.failure_reason = full_step_left_parent - ? FailureReason::correction_left_parent_entity - : FailureReason::correction_failed; - return report; - } - - report.point = std::move(candidate); - f = candidate_f; - report.residual = std::fabs(f); - report.iterations = iter + 1; - if (report.residual <= options.phi_tolerance) - { - report.accepted = true; - report.failure_reason = FailureReason::none; - return report; - } - } - - report.failure_reason = full_step_left_parent - ? FailureReason::correction_left_parent_entity - : FailureReason::phi_residual_too_large; - return report; -} - -template -inline void set_failure(ShiftReport& report, - FailureReason reason, - bool request_refinement = true) -{ - report.accepted = false; - report.request_refinement = request_refinement; - report.failure_reason = reason == FailureReason::none - ? FailureReason::refinement_requested - : reason; -} - -template -inline bool validate_candidate( - ShiftReport& report, - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - std::span provisional_curve_points, - int point_dim, - int node_index, - T desired_arclength, - Phi&& phi, - const Options& options) -{ - if (!geom::point_in_parent_entity( - parent_cell_type, - admissible_parent_entity, - vector_span(report.corrected_node), - options.tolerance)) - { - set_failure(report, FailureReason::correction_left_parent_entity); - return false; - } - - const T residual = std::fabs(static_cast(std::invoke( - phi, - std::span( - report.corrected_node.data(), - report.corrected_node.size())))); - report.metrics.phi_residual = residual; - if (!std::isfinite(residual) || residual > options.phi_tolerance) - { - set_failure(report, FailureReason::phi_residual_too_large); - return false; - } - - const auto layout = evaluate_ordering_and_spacing( - provisional_curve_points, - std::span( - report.corrected_edge_points.data(), - report.corrected_edge_points.size()), - point_dim, - options); - report.metrics.minimum_spacing = layout.minimum_spacing; - report.metrics.minimum_spacing_ratio = layout.minimum_spacing_ratio; - if (!layout.accepted) - { - set_failure(report, layout.failure_reason); - return false; - } - - const auto final_closest = closest_arclength_on_polyline( - provisional_curve_points, - point_dim, - vector_span(report.corrected_node), - options.tolerance); - if (!final_closest.valid) - { - set_failure(report, FailureReason::invalid_curve); - return false; - } - - report.metrics.final_arclength = final_closest.arclength; - report.metrics.final_arclength_error = - std::fabs(final_closest.arclength - desired_arclength); - const T final_tol = arclength_tolerance( - report.metrics.curve_length, - options.max_absolute_final_arclength_error, - options.max_relative_final_arclength_error); - if (report.metrics.final_arclength_error > final_tol) - { - set_failure(report, FailureReason::arclength_error_too_large); - return false; - } - - if (node_index >= 0 - && static_cast(node_index) < layout.arclengths.size()) - { - report.metrics.final_arclength = - layout.arclengths[static_cast(node_index)]; - } - - report.accepted = true; - report.request_refinement = false; - report.failure_reason = FailureReason::none; - return true; -} - -template -inline ShiftReport correct_projected_edge_node( - cell::type parent_cell_type, - geom::ParentEntity admissible_parent_entity, - std::span provisional_curve_points, - std::span projected_edge_points, - int point_dim, - std::span desired_arclength_fractions, - int node_index, - Phi&& phi, - Grad&& grad, - const Options& options = {}) -{ - ShiftReport report; - if (!flat_point_array_valid(provisional_curve_points, point_dim) - || !flat_point_array_valid(projected_edge_points, point_dim) - || point_dim != cell::get_tdim(parent_cell_type)) - { - set_failure(report, FailureReason::invalid_input, false); - return report; - } - - const std::size_t n_nodes = - projected_edge_points.size() / static_cast(point_dim); - if (desired_arclength_fractions.size() != n_nodes) - { - set_failure(report, FailureReason::invalid_input, false); - return report; - } - if (node_index < 0 || static_cast(node_index) >= n_nodes) - { - set_failure(report, FailureReason::node_index_out_of_range, false); - return report; - } - - const int tdim = cell::get_tdim(parent_cell_type); - if (!admissible_parent_entity.valid() - || admissible_parent_entity.dim > tdim) - { - set_failure(report, FailureReason::invalid_parent_entity, false); - return report; - } - - std::vector cumulative; - if (!cumulative_arclength( - provisional_curve_points, point_dim, cumulative, options.tolerance)) - { - set_failure(report, FailureReason::invalid_curve); - return report; - } - report.metrics.curve_length = cumulative.back(); - - const auto current = point_span( - projected_edge_points, - point_dim, - static_cast(node_index)); - if (!geom::point_in_parent_entity( - parent_cell_type, admissible_parent_entity, current, options.tolerance)) - { - set_failure(report, FailureReason::current_point_outside_parent_entity); - return report; - } - - const T fraction = desired_arclength_fractions[static_cast(node_index)]; - if (fraction < -options.tolerance || fraction > T(1) + options.tolerance) - { - set_failure(report, FailureReason::target_arclength_out_of_range); - return report; - } - - report.metrics.desired_arclength = fraction * report.metrics.curve_length; - const auto actual = closest_arclength_on_polyline( - provisional_curve_points, - point_dim, - current, - options.tolerance); - if (!actual.valid) - { - set_failure(report, FailureReason::invalid_curve); - return report; - } - report.metrics.actual_arclength = actual.arclength; - report.metrics.initial_arclength_error = - std::fabs(actual.arclength - report.metrics.desired_arclength); - - report.corrected_edge_points.assign( - projected_edge_points.begin(), projected_edge_points.end()); - report.corrected_node.assign(current.begin(), current.end()); - - const T drift_tol = arclength_tolerance( - report.metrics.curve_length, - options.max_absolute_arclength_drift, - options.max_relative_arclength_drift); - if (report.metrics.initial_arclength_error <= drift_tol) - { - validate_candidate( - report, - parent_cell_type, - admissible_parent_entity, - provisional_curve_points, - point_dim, - node_index, - report.metrics.desired_arclength, - phi, - options); - return report; - } - - const auto target = point_at_arclength( - provisional_curve_points, - point_dim, - report.metrics.desired_arclength, - options.tolerance); - if (!target.valid) - { - set_failure(report, FailureReason::target_arclength_out_of_range); - return report; - } - report.shifted_seed = target.point; - - auto correction = correct_seed_to_zero( - parent_cell_type, - admissible_parent_entity, - vector_span(report.shifted_seed), - phi, - grad, - options); - report.correction_iterations = correction.iterations; - report.metrics.phi_residual = correction.residual; - if (!correction.accepted) - { - set_failure(report, correction.failure_reason); - return report; - } - - report.corrected = true; - report.corrected_node = std::move(correction.point); - auto corrected_span = std::span( - report.corrected_edge_points.data(), - report.corrected_edge_points.size()); - const auto node = mutable_point_span( - corrected_span, - point_dim, - static_cast(node_index)); - for (int d = 0; d < point_dim; ++d) - node[static_cast(d)] = - report.corrected_node[static_cast(d)]; - - validate_candidate( - report, - parent_cell_type, - admissible_parent_entity, - provisional_curve_points, - point_dim, - node_index, - report.metrics.desired_arclength, - phi, - options); - return report; -} - -} // namespace cutcells::tangent_shift diff --git a/cpp/src/write_vtk.cpp b/cpp/src/write_vtk.cpp index 32ce2a4..b4142fd 100644 --- a/cpp/src/write_vtk.cpp +++ b/cpp/src/write_vtk.cpp @@ -626,9 +626,7 @@ namespace cutcells::io const std::span vtk_types, int gdim, const std::span parent_map, - const std::span curved_valid, - const std::span subdivision_depth, - const std::span curving_status) + const std::span subdivision_depth) { if (gdim < 1 || gdim > 3) throw std::runtime_error("write_lagrange_vtk: gdim must be 1, 2, or 3"); @@ -645,9 +643,7 @@ namespace cutcells::io throw std::runtime_error(std::string("write_lagrange_vtk: cell data size mismatch for ") + name); }; check_cell_data(parent_map, "parent_map"); - check_cell_data(curved_valid, "curved_valid"); check_cell_data(subdivision_depth, "subdivision_depth"); - check_cell_data(curving_status, "curving_status"); std::ofstream ofs(filename.c_str(), std::ios::out); if (!ofs) @@ -673,8 +669,7 @@ namespace cutcells::io ofs << "\n" << "\t\t\t\n"; - if (!parent_map.empty() || !curved_valid.empty() - || !subdivision_depth.empty() || !curving_status.empty()) + if (!parent_map.empty() || !subdivision_depth.empty()) { ofs << "\t\t\t\n"; auto write_i32_data = [&ofs](std::span data, @@ -689,9 +684,7 @@ namespace cutcells::io ofs << "\n"; }; write_i32_data(parent_map, "parent_id"); - write_i32_data(curved_valid, "curved_valid"); write_i32_data(subdivision_depth, "subdivision_depth"); - write_i32_data(curving_status, "curving_status"); ofs << "\t\t\t\n"; } diff --git a/cpp/src/write_vtk.h b/cpp/src/write_vtk.h index ea37335..be68f53 100644 --- a/cpp/src/write_vtk.h +++ b/cpp/src/write_vtk.h @@ -91,9 +91,7 @@ namespace cutcells::io const std::span vtk_types, int gdim, const std::span parent_map = {}, - const std::span curved_valid = {}, - const std::span subdivision_depth = {}, - const std::span curving_status = {}); + const std::span subdivision_depth = {}); template void write_lagrange_vtk(std::string filename, @@ -103,9 +101,7 @@ namespace cutcells::io const std::span vtk_types, int gdim, const std::span parent_map = {}, - const std::span curved_valid = {}, - const std::span subdivision_depth = {}, - const std::span curving_status = {}) + const std::span subdivision_depth = {}) { std::vector coords_d(point_coords.begin(), point_coords.end()); write_lagrange_vtk( @@ -116,8 +112,6 @@ namespace cutcells::io vtk_types, gdim, parent_map, - curved_valid, - subdivision_depth, - curving_status); + subdivision_depth); } } diff --git a/python/cutcells/__init__.py b/python/cutcells/__init__.py index 5630988..02215cb 100644 --- a/python/cutcells/__init__.py +++ b/python/cutcells/__init__.py @@ -4,6 +4,7 @@ # # SPDX-License-Identifier: MIT +import importlib as _importlib from importlib import util as _importlib_util from pathlib import Path as _Path import ctypes as _ctypes @@ -121,13 +122,7 @@ def _load_cpp_module(): except Exception: pass - spec = _importlib_util.spec_from_file_location("cutcells._cutcellscpp", so_path) - if spec is None or spec.loader is None: - raise ImportError(f"Could not load installed extension from {so_path}") - module = _importlib_util.module_from_spec(spec) - _sys.modules["cutcells._cutcellscpp"] = module - spec.loader.exec_module(module) - return module + return _importlib.import_module("cutcells._cutcellscpp") # Fall back to an installed extension (e.g. site-packages) when running from # a source tree without an up-to-date in-tree build. @@ -221,7 +216,6 @@ def _load_cpp_module(): fill_all_vertex_signs_from_level_set = _cutcellscpp.fill_all_vertex_signs_from_level_set classify_leaf_cells = _cutcellscpp.classify_leaf_cells process_ready_to_cut_cells = _cutcellscpp.process_ready_to_cut_cells -check_ready_to_cut_cell_graphs = _cutcellscpp.check_ready_to_cut_cell_graphs refine_ready_cell_on_largest_midpoint_value = ( _cutcellscpp.refine_ready_cell_on_largest_midpoint_value ) @@ -231,9 +225,6 @@ def _load_cpp_module(): certify_refine_and_process_ready_cells = ( _cutcellscpp.certify_refine_and_process_ready_cells ) -certify_refine_graph_check_and_process_ready_cells = ( - _cutcellscpp.certify_refine_graph_check_and_process_ready_cells -) cut = _cutcellscpp.cut higher_order_cut = _cutcellscpp.higher_order_cut create_cut_mesh = _cutcellscpp.create_cut_mesh @@ -276,6 +267,6 @@ def _load_cpp_module(): single_cell_mesh, summarize_analysis, ) -except ModuleNotFoundError as exc: - if exc.name != f"{__name__}.triangulation_analysis": +except (ModuleNotFoundError, ImportError) as exc: + if isinstance(exc, ModuleNotFoundError) and exc.name != f"{__name__}.triangulation_analysis": raise diff --git a/python/cutcells/wrapper.cpp b/python/cutcells/wrapper.cpp index 6e53f41..6923671 100644 --- a/python/cutcells/wrapper.cpp +++ b/python/cutcells/wrapper.cpp @@ -57,130 +57,6 @@ T default_sqrt_epsilon_tol() return std::sqrt(std::numeric_limits::epsilon()); } -cutcells::GraphProjectionMode graph_projection_mode_from_string( - std::string_view name) -{ - if (name == "straight_zero_entity_normal" - || name == "zero_entity_normal" - || name == "straight_normal" - || name == "normal") - { - return cutcells::GraphProjectionMode::straight_zero_entity_normal; - } - if (name == "level_set_gradient" - || name == "level-set-gradient" - || name == "gradient") - { - return cutcells::GraphProjectionMode::level_set_gradient; - } - throw std::invalid_argument("unknown graph projection mode"); -} - -cutcells::GraphRefinementMode graph_refinement_mode_from_string( - std::string_view name) -{ - if (name == "green_edge" - || name == "green" - || name == "edge") - { - return cutcells::GraphRefinementMode::green_edge; - } - if (name == "red_failed_cell" - || name == "red" - || name == "cell") - { - return cutcells::GraphRefinementMode::red_failed_cell; - } - if (name == "green_orthogonal_surface_edge" - || name == "orthogonal_surface_edge" - || name == "surface_orthogonal_edge" - || name == "orthogonal_edge") - { - return cutcells::GraphRefinementMode::green_orthogonal_surface_edge; - } - if (name == "green_midpoint_residual" - || name == "midpoint_residual" - || name == "residual") - { - return cutcells::GraphRefinementMode::green_midpoint_residual; - } - if (name == "green_normal_variation" - || name == "normal_variation" - || name == "curvature") - { - return cutcells::GraphRefinementMode::green_normal_variation; - } - throw std::invalid_argument("unknown graph refinement mode"); -} - -template -cutcells::ReadyCellGraphOptions make_graph_options( - std::string_view projection_direction, - std::string_view refinement_mode, - int max_refinements, - T max_relative_correction_distance, - T max_relative_tangential_shift, - T max_drift_amplification, - T min_host_normal_alignment, - T min_level_set_gradient_host_alignment, - bool enabled) -{ - cutcells::ReadyCellGraphOptions options; - options.enabled = enabled; - options.projection_mode = graph_projection_mode_from_string(projection_direction); - options.refinement_mode = graph_refinement_mode_from_string(refinement_mode); - options.max_refinements = max_refinements; - options.criteria.max_relative_correction_distance = - max_relative_correction_distance; - options.criteria.max_relative_tangential_shift = - max_relative_tangential_shift; - options.criteria.max_drift_amplification = max_drift_amplification; - options.criteria.min_host_normal_alignment = min_host_normal_alignment; - options.min_level_set_gradient_host_alignment = - min_level_set_gradient_host_alignment; - return options; -} - -template -nb::dict graph_diagnostics_to_dict( - const cutcells::ReadyCellGraphDiagnostics& diagnostics) -{ - nb::dict out; - out["accepted"] = diagnostics.accepted; - out["checked_cells"] = diagnostics.checked_cells; - out["checked_edges"] = diagnostics.checked_edges; - out["checked_faces"] = diagnostics.checked_faces; - out["failed_checks"] = diagnostics.failed_checks; - out["graph_refinements"] = diagnostics.graph_refinements; - out["first_failed_cell"] = diagnostics.first_failed_cell; - out["first_failure_reason"] = std::string( - cutcells::graph_criteria::failure_reason_name( - diagnostics.first_failure_reason)); - out["min_true_transversality"] = diagnostics.min_true_transversality; - out["min_host_normal_alignment"] = diagnostics.min_host_normal_alignment; - out["max_drift_amplification"] = diagnostics.max_drift_amplification; - out["max_relative_correction_distance"] = - diagnostics.max_relative_correction_distance; - out["max_relative_tangential_shift"] = - diagnostics.max_relative_tangential_shift; - out["min_edge_gap_ratio"] = diagnostics.min_edge_gap_ratio; - out["min_face_area_ratio"] = diagnostics.min_face_area_ratio; - out["min_surface_jacobian_ratio"] = diagnostics.min_face_area_ratio; - out["min_level_set_gradient_host_alignment"] = - diagnostics.min_level_set_gradient_host_alignment; - out["first_failed_face_triangle_index"] = - diagnostics.first_failed_face_triangle_index; - out["first_failed_face_area_ratio"] = - diagnostics.first_failed_face_area_ratio; - out["first_failed_surface_jacobian_ratio"] = - diagnostics.first_failed_face_area_ratio; - out["first_requested_refinement_entity_dim"] = - diagnostics.first_requested_refinement_entity_dim; - out["first_requested_refinement_entity_id"] = - diagnostics.first_requested_refinement_entity_id; - return out; -} - const std::string& cell_domain_to_str(cell::domain domain_id) { static const std::map type_to_name @@ -856,68 +732,30 @@ inline bool part_mode_is_cut_only(std::string_view mode) template cutcells::mesh::CutMesh part_visualization_mesh( const cutcells::HOMeshPart& part, - std::string_view mode, - int geometry_order, - std::string_view node_family, - std::string_view projection_direction) + std::string_view mode) { const bool cut_only = part_mode_is_cut_only(mode); - const auto family = cutcells::curving::node_family_from_string(node_family); - const auto direction = - cutcells::curving::direction_mode_from_string(projection_direction); return cutcells::output::visualization_mesh( - part, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); + part, /*include_uncut_cells=*/!cut_only); } template cutcells::quadrature::QuadratureRules part_quadrature( const cutcells::HOMeshPart& part, int order, - std::string_view mode, - int geometry_order, - std::string_view node_family, - std::string_view projection_direction) + std::string_view mode) { const bool cut_only = part_mode_is_cut_only(mode); - const auto family = cutcells::curving::node_family_from_string(node_family); - const auto direction = - cutcells::curving::direction_mode_from_string(projection_direction); return cutcells::output::quadrature_rules( - part, order, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); + part, order, /*include_uncut_cells=*/!cut_only); } template void part_write_vtu(const cutcells::HOMeshPart& part, const std::string& filename, - std::string_view mode, - int geometry_order, - std::string_view node_family, - std::string_view projection_direction) + std::string_view mode) { - const bool cut_only = part_mode_is_cut_only(mode); - auto family = cutcells::curving::node_family_from_string(node_family); - const auto direction = - cutcells::curving::direction_mode_from_string(projection_direction); - if (geometry_order > 1) - { - const auto grid = cutcells::output::curved_lagrange_grid( - part, /*include_uncut_cells=*/!cut_only, geometry_order, family, direction); - cutcells::io::write_lagrange_vtk( - filename, - std::span(grid.points.data(), grid.points.size()), - std::span(grid.connectivity.data(), grid.connectivity.size()), - std::span(grid.offsets.data(), grid.offsets.size()), - std::span(grid.vtk_types.data(), grid.vtk_types.size()), - grid.gdim, - std::span(grid.parent_map.data(), grid.parent_map.size()), - std::span(grid.curved_valid.data(), grid.curved_valid.size()), - std::span(grid.subdivision_depth.data(), grid.subdivision_depth.size()), - std::span(grid.curving_status.data(), grid.curving_status.size())); - return; - } - - auto vis = part_visualization_mesh( - part, mode, geometry_order, node_family, projection_direction); + auto vis = part_visualization_mesh(part, mode); cutcells::io::write_vtk(filename, vis); } @@ -2018,87 +1856,6 @@ void declare_ho_cut(nb::module_& m, const std::string& type) }, nb::rv_policy::reference_internal, "Per-level-set domain classification, shape (num_level_sets, num_cells).") - .def( - "graph_check_summary", - [](const HOCutResult& self) - { - const auto& diagnostics = self.cut_cells.graph_diagnostics; - std::vector accepted; - std::vector checked_cells; - std::vector checked_edges; - std::vector checked_faces; - std::vector failed_checks; - std::vector graph_refinements; - std::vector min_true_transversality; - std::vector min_host_normal_alignment; - std::vector max_drift_amplification; - std::vector max_relative_correction_distance; - std::vector max_relative_tangential_shift; - std::vector min_edge_gap_ratio; - std::vector min_face_area_ratio; - std::vector min_level_set_gradient_host_alignment; - - accepted.reserve(diagnostics.size()); - checked_cells.reserve(diagnostics.size()); - checked_edges.reserve(diagnostics.size()); - checked_faces.reserve(diagnostics.size()); - failed_checks.reserve(diagnostics.size()); - graph_refinements.reserve(diagnostics.size()); - min_true_transversality.reserve(diagnostics.size()); - min_host_normal_alignment.reserve(diagnostics.size()); - max_drift_amplification.reserve(diagnostics.size()); - max_relative_correction_distance.reserve(diagnostics.size()); - max_relative_tangential_shift.reserve(diagnostics.size()); - min_edge_gap_ratio.reserve(diagnostics.size()); - min_face_area_ratio.reserve(diagnostics.size()); - min_level_set_gradient_host_alignment.reserve(diagnostics.size()); - - for (const auto& d : diagnostics) - { - accepted.push_back(d.accepted ? 1 : 0); - checked_cells.push_back(d.checked_cells); - checked_edges.push_back(d.checked_edges); - checked_faces.push_back(d.checked_faces); - failed_checks.push_back(d.failed_checks); - graph_refinements.push_back(d.graph_refinements); - min_true_transversality.push_back(d.min_true_transversality); - min_host_normal_alignment.push_back(d.min_host_normal_alignment); - max_drift_amplification.push_back(d.max_drift_amplification); - max_relative_correction_distance.push_back( - d.max_relative_correction_distance); - max_relative_tangential_shift.push_back( - d.max_relative_tangential_shift); - min_edge_gap_ratio.push_back(d.min_edge_gap_ratio); - min_face_area_ratio.push_back(d.min_face_area_ratio); - min_level_set_gradient_host_alignment.push_back( - d.min_level_set_gradient_host_alignment); - } - - nb::dict out; - out["accepted"] = as_nbarray(std::move(accepted)); - out["checked_cells"] = as_nbarray(std::move(checked_cells)); - out["checked_edges"] = as_nbarray(std::move(checked_edges)); - out["checked_faces"] = as_nbarray(std::move(checked_faces)); - out["failed_checks"] = as_nbarray(std::move(failed_checks)); - out["graph_refinements"] = as_nbarray(std::move(graph_refinements)); - out["min_true_transversality"] = - as_nbarray(std::move(min_true_transversality)); - out["min_host_normal_alignment"] = - as_nbarray(std::move(min_host_normal_alignment)); - out["max_drift_amplification"] = - as_nbarray(std::move(max_drift_amplification)); - out["max_relative_correction_distance"] = - as_nbarray(std::move(max_relative_correction_distance)); - out["max_relative_tangential_shift"] = - as_nbarray(std::move(max_relative_tangential_shift)); - out["min_edge_gap_ratio"] = as_nbarray(std::move(min_edge_gap_ratio)); - out["min_face_area_ratio"] = as_nbarray(std::move(min_face_area_ratio)); - out["min_surface_jacobian_ratio"] = out["min_face_area_ratio"]; - out["min_level_set_gradient_host_alignment"] = - as_nbarray(std::move(min_level_set_gradient_host_alignment)); - return out; - }, - "Return per-cut-cell graph preflight diagnostics.") .def("__getitem__", [](const HOCutResult& self, const std::string& expr_str) { return cutcells::select_part( @@ -2122,316 +1879,7 @@ void declare_ho_cut(nb::module_& m, const std::string& type) }, nb::arg("cut_cell_id"), nb::rv_policy::reference_internal, - "Return the AdaptCell for a cut-cell index.") - .def( - "curved_zero_nodes", - [](HOCutResult& self, - int geometry_order, - const std::string& node_family, - T small_entity_tol, - const std::string& projection_direction) - { - cutcells::curving::CurvingOptions options; - options.geometry_order = geometry_order; - options.node_family = - cutcells::curving::node_family_from_string(node_family); - options.direction_mode = - cutcells::curving::direction_mode_from_string(projection_direction); - options.small_entity_tol = small_entity_tol; - - try - { - auto parent_cell_ids = - std::span(self.cut_cells.parent_cell_ids); - auto adapt_cells = - std::span>(self.cut_cells.adapt_cells); - auto level_set_cells = - std::span>( - self.cut_cells.level_set_cells); - auto ls_offsets = std::span(self.cut_cells.ls_offsets); - - if (!self.cut_cells.curving.identity_valid - || self.cut_cells.curving.num_cut_cells - != static_cast(adapt_cells.size())) - { - cutcells::curving::rebuild_identity( - self.cut_cells.curving, parent_cell_ids, adapt_cells); - } - - for (std::size_t i = 0; - i < self.cut_cells.curving.identities.size(); ++i) - { - const auto& ident = self.cut_cells.curving.identities[i]; - bool graph_ok = true; - if (ident.cut_cell_id >= 0 - && ident.cut_cell_id - < static_cast( - self.cut_cells.graph_diagnostics.size())) - { - const auto& graph_diag = - self.cut_cells.graph_diagnostics[ - static_cast(ident.cut_cell_id)]; - for (const auto& record : graph_diag.zero_entities) - { - if (record.local_zero_entity_id - == ident.local_zero_entity_id) - { - graph_ok = record.accepted; - break; - } - } - } - - if (!graph_ok) - { - auto& state = self.cut_cells.curving.states[i]; - state = cutcells::curving::CurvedZeroEntityState{}; - state.status = cutcells::curving::CurvingStatus::failed; - state.geometry_order = options.geometry_order; - state.node_family = options.node_family; - state.direction_mode = options.direction_mode; - state.small_entity_tol = options.small_entity_tol; - state.zero_mask = ident.zero_mask; - state.failure_reason = "graph check failed before curving"; - continue; - } - - (void)cutcells::curving::ensure_curved( - self.cut_cells.curving, - parent_cell_ids, - adapt_cells, - level_set_cells, - ls_offsets, - ident.cut_cell_id, - ident.local_zero_entity_id, - options); - } - } - catch (const std::exception& e) - { - throw std::runtime_error( - std::string("curved_zero_nodes: ensure_curved failed: ") - + e.what()); - } - - std::vector points; - std::vector offsets; - std::vector status; - std::vector cut_cell_id; - std::vector local_zero_entity_id; - std::vector dim; - std::vector parent_dim; - std::vector parent_id; - std::vector direction_mode; - std::vector zero_mask; - std::vector stats_offsets; - std::vector node_iterations; - std::vector node_status; - std::vector node_failure_code; - std::vector node_residual; - std::vector node_active_face_mask; - std::vector node_closest_face_id; - std::vector node_safe_subspace_dim; - std::vector node_projection_mode; - std::vector node_retry_count; - std::vector node_seed; - std::vector node_direction; - std::vector node_clip_lo; - std::vector node_clip_hi; - std::vector node_root_t; - nb::list failure_reason; - std::vector total_iterations; - std::vector max_iterations; - offsets.reserve(self.cut_cells.curving.states.size() + 1); - offsets.push_back(0); - stats_offsets.reserve(self.cut_cells.curving.states.size() + 1); - stats_offsets.push_back(0); - - for (std::size_t i = 0; i < self.cut_cells.curving.states.size(); ++i) - { - try - { - const auto& state = self.cut_cells.curving.states[i]; - const auto& ident = self.cut_cells.curving.identities[i]; - points.insert(points.end(), state.ref_nodes.begin(), state.ref_nodes.end()); - offsets.push_back(static_cast( - points.size() / static_cast(self.cut_cells.tdim))); - status.push_back(static_cast(state.status)); - cut_cell_id.push_back(static_cast(ident.cut_cell_id)); - local_zero_entity_id.push_back(static_cast(ident.local_zero_entity_id)); - dim.push_back(static_cast(ident.dim)); - parent_dim.push_back(ident.parent_dim); - parent_id.push_back(ident.parent_id); - direction_mode.push_back( - static_cast(state.direction_mode)); - zero_mask.push_back(ident.zero_mask); - node_iterations.insert( - node_iterations.end(), - state.node_iterations.begin(), - state.node_iterations.end()); - node_status.insert( - node_status.end(), - state.node_status.begin(), - state.node_status.end()); - node_failure_code.insert( - node_failure_code.end(), - state.node_failure_code.begin(), - state.node_failure_code.end()); - node_residual.insert( - node_residual.end(), - state.node_residual.begin(), - state.node_residual.end()); - node_active_face_mask.insert( - node_active_face_mask.end(), - state.node_active_face_mask.begin(), - state.node_active_face_mask.end()); - node_closest_face_id.insert( - node_closest_face_id.end(), - state.node_closest_face_id.begin(), - state.node_closest_face_id.end()); - node_safe_subspace_dim.insert( - node_safe_subspace_dim.end(), - state.node_safe_subspace_dim.begin(), - state.node_safe_subspace_dim.end()); - node_projection_mode.insert( - node_projection_mode.end(), - state.node_projection_mode.begin(), - state.node_projection_mode.end()); - node_retry_count.insert( - node_retry_count.end(), - state.node_retry_count.begin(), - state.node_retry_count.end()); - node_seed.insert( - node_seed.end(), - state.node_seed.begin(), - state.node_seed.end()); - node_direction.insert( - node_direction.end(), - state.node_direction.begin(), - state.node_direction.end()); - node_clip_lo.insert( - node_clip_lo.end(), - state.node_clip_lo.begin(), - state.node_clip_lo.end()); - node_clip_hi.insert( - node_clip_hi.end(), - state.node_clip_hi.begin(), - state.node_clip_hi.end()); - node_root_t.insert( - node_root_t.end(), - state.node_root_t.begin(), - state.node_root_t.end()); - stats_offsets.push_back(static_cast(node_iterations.size())); - failure_reason.append(state.failure_reason); - int total = 0; - int max_iter = 0; - for (const auto it : state.node_iterations) - { - total += static_cast(it); - max_iter = std::max(max_iter, static_cast(it)); - } - total_iterations.push_back(static_cast(total)); - max_iterations.push_back(static_cast(max_iter)); - } - catch (const std::exception& e) - { - throw std::runtime_error( - std::string("curved_zero_nodes: packaging state ") - + std::to_string(i) - + " failed: " - + e.what()); - } - } - - nb::dict result; - const std::size_t npoints = - self.cut_cells.tdim > 0 - ? points.size() / static_cast(self.cut_cells.tdim) - : 0; - result["points"] = as_nbarray( - std::move(points), - {npoints, static_cast(self.cut_cells.tdim)}); - result["offsets"] = as_nbarray(std::move(offsets)); - result["status"] = as_nbarray(std::move(status)); - result["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); - result["local_zero_entity_id"] = as_nbarray(std::move(local_zero_entity_id)); - result["dim"] = as_nbarray(std::move(dim)); - result["parent_dim"] = as_nbarray(std::move(parent_dim)); - result["parent_id"] = as_nbarray(std::move(parent_id)); - result["direction_mode"] = as_nbarray(std::move(direction_mode)); - result["zero_mask"] = as_nbarray(std::move(zero_mask)); - result["stats_offsets"] = as_nbarray(std::move(stats_offsets)); - result["node_iterations"] = as_nbarray(std::move(node_iterations)); - result["node_status"] = as_nbarray(std::move(node_status)); - result["node_failure_code"] = as_nbarray(std::move(node_failure_code)); - result["node_residual"] = as_nbarray(std::move(node_residual)); - result["node_active_face_mask"] = as_nbarray(std::move(node_active_face_mask)); - result["node_closest_face_id"] = as_nbarray(std::move(node_closest_face_id)); - result["node_safe_subspace_dim"] = as_nbarray(std::move(node_safe_subspace_dim)); - result["node_projection_mode"] = as_nbarray(std::move(node_projection_mode)); - result["node_retry_count"] = as_nbarray(std::move(node_retry_count)); - const std::size_t nstats = node_root_t.size(); - result["node_seed"] = as_nbarray( - std::move(node_seed), - {nstats, static_cast(self.cut_cells.tdim)}); - result["node_direction"] = as_nbarray( - std::move(node_direction), - {nstats, static_cast(self.cut_cells.tdim)}); - result["node_clip_lo"] = as_nbarray(std::move(node_clip_lo)); - result["node_clip_hi"] = as_nbarray(std::move(node_clip_hi)); - result["node_root_t"] = as_nbarray(std::move(node_root_t)); - result["failure_reason"] = std::move(failure_reason); - - nb::list failure_code_names; - for (const char* name : { - "none", - "exact_vertex", - "boundary_from_edge", - "invalid_constraint_count", - "missing_level_set_cell", - "empty_zero_mask", - "unsupported_entity", - "no_host_interval", - "no_sign_changing_bracket", - "brent_failed", - "outside_host_domain", - "singular_gradient_system", - "line_search_failed", - "max_iterations", - "missing_boundary_edge", - "boundary_edge_failed", - "projection_failed", - "closest_face_retry_failed", - "constrained_newton_failed", - "small_entity_kept_straight"}) - failure_code_names.append(name); - result["failure_code_names"] = std::move(failure_code_names); - - nb::list projection_mode_names; - for (const char* name : { - "none", - "safe_line", - "closest_face_retry", - "constrained_newton", - "vector_newton"}) - projection_mode_names.append(name); - result["projection_mode_names"] = std::move(projection_mode_names); - nb::list direction_mode_names; - for (const char* name : { - "straight_zero_entity_normal", - "level_set_gradient"}) - direction_mode_names.append(name); - result["direction_mode_names"] = std::move(direction_mode_names); - result["total_iterations"] = as_nbarray(std::move(total_iterations)); - result["max_iterations"] = as_nbarray(std::move(max_iterations)); - return result; - }, - nb::arg("geometry_order") = 2, - nb::arg("node_family") = "gll", - nb::arg("small_entity_tol") = default_sqrt_epsilon_tol(), - nb::arg("projection_direction") = "level_set_gradient", - "Return cached curved zero-entity nodes in parent reference coordinates."); - + "Return the AdaptCell for a cut-cell index."); // --- HOMeshPart --- std::string part_name = "HOMeshPart_" + type; nb::class_(m, part_name.c_str(), "Mesh part selected by expression") @@ -2466,637 +1914,80 @@ void declare_ho_cut(nb::module_& m, const std::string& type) .def( "visualization_mesh", [](const PartT& self, - const std::string& mode, - int geometry_order, - const std::string& node_family, - const std::string& projection_direction) { + const std::string& mode) { nb::gil_scoped_release release; - return part_visualization_mesh( - self, mode, geometry_order, node_family, projection_direction); + return part_visualization_mesh(self, mode); }, nb::arg("mode") = "full", - nb::arg("geometry_order") = -1, - nb::arg("node_family") = "gll", - nb::arg("projection_direction") = "level_set_gradient", "Return a visualization mesh for an HOMeshPart, preserving AdaptCell topology.") .def( "quadrature", [](const PartT& self, int order, - const std::string& mode, - int geometry_order, - const std::string& node_family, - const std::string& projection_direction) { + const std::string& mode) { nb::gil_scoped_release release; - return part_quadrature( - self, order, mode, geometry_order, node_family, projection_direction); + return part_quadrature(self, order, mode); }, nb::arg("order") = 3, nb::arg("mode") = "full", - nb::arg("geometry_order") = -1, - nb::arg("node_family") = "gll", - nb::arg("projection_direction") = "level_set_gradient", - "Return quadrature rules for an HOMeshPart, preserving AdaptCell topology. For curved geometry this is the construction node family.") - .def( - "graph_check_zero_entity_data", - [](const PartT& self) - { - if (self.cut_cells == nullptr) - throw std::runtime_error( - "HOMeshPart is not attached to cut-cell storage"); - - const auto infos = - cutcells::output::selected_zero_entity_infos(self); - const T nan = std::numeric_limits::quiet_NaN(); - - std::vector cut_cell_id; - std::vector parent_cell_id; - std::vector local_zero_entity_id; - std::vector level_set_id; - std::vector zero_entity_dim; - std::vector host_cell_id; - std::vector host_cell_type; - std::vector host_face_id; - std::vector source_level_set; - std::vector graph_accepted; - std::vector graph_failed_checks; - std::vector graph_checked_edges; - std::vector graph_checked_faces; - std::vector graph_failure_reason_code; - nb::list graph_failure_reason; - std::vector min_true_transversality; - std::vector min_host_normal_alignment; - std::vector max_drift_amplification; - std::vector max_relative_correction_distance; - std::vector max_relative_tangential_shift; - std::vector min_edge_gap_ratio; - std::vector min_face_area_ratio; - std::vector min_level_set_gradient_host_alignment; - std::vector failed_face_triangle_index; - std::vector failed_face_area_ratio; - std::vector graph_failed_projection_seed; - std::vector graph_failed_projection_direction; - std::vector graph_failed_projection_clip_lo; - std::vector graph_failed_projection_clip_hi; - std::vector graph_failed_projection_root_t; - std::vector requested_refinement_entity_dim; - std::vector requested_refinement_entity_id; - - cut_cell_id.reserve(infos.size()); - parent_cell_id.reserve(infos.size()); - local_zero_entity_id.reserve(infos.size()); - level_set_id.reserve(infos.size()); - zero_entity_dim.reserve(infos.size()); - host_cell_id.reserve(infos.size()); - host_cell_type.reserve(infos.size()); - host_face_id.reserve(infos.size()); - source_level_set.reserve(infos.size()); - graph_accepted.reserve(infos.size()); - graph_failed_checks.reserve(infos.size()); - graph_checked_edges.reserve(infos.size()); - graph_checked_faces.reserve(infos.size()); - graph_failure_reason_code.reserve(infos.size()); - min_true_transversality.reserve(infos.size()); - min_host_normal_alignment.reserve(infos.size()); - max_drift_amplification.reserve(infos.size()); - max_relative_correction_distance.reserve(infos.size()); - max_relative_tangential_shift.reserve(infos.size()); - min_edge_gap_ratio.reserve(infos.size()); - min_face_area_ratio.reserve(infos.size()); - min_level_set_gradient_host_alignment.reserve(infos.size()); - failed_face_triangle_index.reserve(infos.size()); - failed_face_area_ratio.reserve(infos.size()); - graph_failed_projection_seed.reserve( - infos.size() * static_cast(self.cut_cells->tdim)); - graph_failed_projection_direction.reserve( - infos.size() * static_cast(self.cut_cells->tdim)); - graph_failed_projection_clip_lo.reserve(infos.size()); - graph_failed_projection_clip_hi.reserve(infos.size()); - graph_failed_projection_root_t.reserve(infos.size()); - requested_refinement_entity_dim.reserve(infos.size()); - requested_refinement_entity_id.reserve(infos.size()); - - auto append_projection_vector = - [&](std::vector& out, const std::vector& values) - { - for (int d = 0; d < self.cut_cells->tdim; ++d) - { - out.push_back( - d < static_cast(values.size()) - ? values[static_cast(d)] - : nan); - } - }; - - for (const auto& info : infos) - { - cut_cell_id.push_back(info.cut_cell_id); - parent_cell_id.push_back(info.parent_cell_id); - local_zero_entity_id.push_back(info.local_zero_entity_id); - zero_entity_dim.push_back(info.dimension); - - const cutcells::ZeroEntityGraphDiagnostics* record = nullptr; - if (info.cut_cell_id >= 0 - && info.cut_cell_id - < static_cast( - self.cut_cells->graph_diagnostics.size())) - { - const auto& diag = - self.cut_cells->graph_diagnostics[ - static_cast(info.cut_cell_id)]; - for (const auto& candidate : diag.zero_entities) - { - const std::uint64_t candidate_bit = - candidate.level_set_id >= 0 - ? (std::uint64_t(1) << candidate.level_set_id) - : std::uint64_t(0); - if (candidate.local_zero_entity_id - == info.local_zero_entity_id - && (self.expr.zero_required == 0 - || (self.expr.zero_required & candidate_bit) != 0)) - { - record = &candidate; - break; - } - } - } - - if (record == nullptr) - { - level_set_id.push_back(-1); - host_cell_id.push_back(-1); - host_cell_type.push_back(-1); - host_face_id.push_back(-1); - source_level_set.push_back(-1); - graph_accepted.push_back(-1); - graph_failed_checks.push_back(-1); - graph_checked_edges.push_back(-1); - graph_checked_faces.push_back(-1); - graph_failure_reason_code.push_back(-1); - graph_failure_reason.append("missing"); - min_true_transversality.push_back(nan); - min_host_normal_alignment.push_back(nan); - max_drift_amplification.push_back(nan); - max_relative_correction_distance.push_back(nan); - max_relative_tangential_shift.push_back(nan); - min_edge_gap_ratio.push_back(nan); - min_face_area_ratio.push_back(nan); - min_level_set_gradient_host_alignment.push_back(nan); - failed_face_triangle_index.push_back(-1); - failed_face_area_ratio.push_back(nan); - append_projection_vector( - graph_failed_projection_seed, std::vector{}); - append_projection_vector( - graph_failed_projection_direction, std::vector{}); - graph_failed_projection_clip_lo.push_back(nan); - graph_failed_projection_clip_hi.push_back(nan); - graph_failed_projection_root_t.push_back(nan); - requested_refinement_entity_dim.push_back(-1); - requested_refinement_entity_id.push_back(-1); - continue; - } - - level_set_id.push_back(record->level_set_id); - host_cell_id.push_back(record->host_cell_id); - host_cell_type.push_back( - static_cast(record->host_cell_type)); - host_face_id.push_back(record->host_face_id); - source_level_set.push_back(record->source_level_set); - graph_accepted.push_back(record->accepted ? 1 : 0); - graph_failed_checks.push_back(record->failed_checks); - graph_checked_edges.push_back(record->checked_edges); - graph_checked_faces.push_back(record->checked_faces); - graph_failure_reason_code.push_back( - static_cast(record->failure_reason)); - graph_failure_reason.append(std::string( - cutcells::graph_criteria::failure_reason_name( - record->failure_reason))); - min_true_transversality.push_back( - record->min_true_transversality); - min_host_normal_alignment.push_back( - record->min_host_normal_alignment); - max_drift_amplification.push_back( - record->max_drift_amplification); - max_relative_correction_distance.push_back( - record->max_relative_correction_distance); - max_relative_tangential_shift.push_back( - record->max_relative_tangential_shift); - min_edge_gap_ratio.push_back(record->min_edge_gap_ratio); - min_face_area_ratio.push_back(record->min_face_area_ratio); - min_level_set_gradient_host_alignment.push_back( - record->min_level_set_gradient_host_alignment); - failed_face_triangle_index.push_back( - record->failed_face_triangle_index); - failed_face_area_ratio.push_back( - record->failed_face_area_ratio); - append_projection_vector( - graph_failed_projection_seed, - record->failed_projection_seed); - append_projection_vector( - graph_failed_projection_direction, - record->failed_projection_direction); - graph_failed_projection_clip_lo.push_back( - record->failed_projection_clip_lo); - graph_failed_projection_clip_hi.push_back( - record->failed_projection_clip_hi); - graph_failed_projection_root_t.push_back( - record->failed_projection_root_t); - requested_refinement_entity_dim.push_back( - record->requested_refinement_entity_dim); - requested_refinement_entity_id.push_back( - record->requested_refinement_entity_id); - } - - nb::dict out; - out["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); - out["parent_cell_id"] = as_nbarray(std::move(parent_cell_id)); - out["local_zero_entity_id"] = - as_nbarray(std::move(local_zero_entity_id)); - out["level_set_id"] = as_nbarray(std::move(level_set_id)); - out["zero_entity_dim"] = as_nbarray(std::move(zero_entity_dim)); - out["zero_entity_host_cell_id"] = - as_nbarray(std::move(host_cell_id)); - out["zero_entity_host_cell_type"] = - as_nbarray(std::move(host_cell_type)); - out["zero_entity_host_face_id"] = - as_nbarray(std::move(host_face_id)); - out["zero_entity_source_level_set"] = - as_nbarray(std::move(source_level_set)); - out["graph_accepted"] = as_nbarray(std::move(graph_accepted)); - out["graph_failed_checks"] = - as_nbarray(std::move(graph_failed_checks)); - out["graph_checked_edges"] = - as_nbarray(std::move(graph_checked_edges)); - out["graph_checked_faces"] = - as_nbarray(std::move(graph_checked_faces)); - out["graph_failure_reason_code"] = - as_nbarray(std::move(graph_failure_reason_code)); - out["graph_failure_reason"] = graph_failure_reason; - out["graph_min_transversality"] = - as_nbarray(std::move(min_true_transversality)); - out["graph_min_host_alignment"] = - as_nbarray(std::move(min_host_normal_alignment)); - out["graph_max_drift"] = - as_nbarray(std::move(max_drift_amplification)); - out["graph_max_correction"] = - as_nbarray(std::move(max_relative_correction_distance)); - out["graph_max_tangential_shift"] = - as_nbarray(std::move(max_relative_tangential_shift)); - out["graph_min_edge_gap"] = - as_nbarray(std::move(min_edge_gap_ratio)); - out["graph_min_face_area"] = - as_nbarray(std::move(min_face_area_ratio)); - out["graph_min_surface_jacobian_ratio"] = - out["graph_min_face_area"]; - out["graph_min_level_set_gradient_host_alignment"] = - as_nbarray(std::move(min_level_set_gradient_host_alignment)); - out["graph_failed_face_triangle_index"] = - as_nbarray(std::move(failed_face_triangle_index)); - out["graph_failed_face_area_ratio"] = - as_nbarray(std::move(failed_face_area_ratio)); - out["graph_failed_surface_jacobian_ratio"] = - out["graph_failed_face_area_ratio"]; - out["graph_failed_projection_seed"] = as_nbarray( - std::move(graph_failed_projection_seed), - {infos.size(), static_cast(self.cut_cells->tdim)}); - out["graph_failed_projection_direction"] = as_nbarray( - std::move(graph_failed_projection_direction), - {infos.size(), static_cast(self.cut_cells->tdim)}); - out["graph_failed_projection_clip_lo"] = - as_nbarray(std::move(graph_failed_projection_clip_lo)); - out["graph_failed_projection_clip_hi"] = - as_nbarray(std::move(graph_failed_projection_clip_hi)); - out["graph_failed_projection_root_t"] = - as_nbarray(std::move(graph_failed_projection_root_t)); - out["graph_requested_refinement_entity_dim"] = - as_nbarray(std::move(requested_refinement_entity_dim)); - out["graph_requested_refinement_entity_id"] = - as_nbarray(std::move(requested_refinement_entity_id)); - return out; - }, - "Return graph-check values aligned with the cells of the straight phi = 0 visualization mesh.") - .def( - "graph_check_node_data", - [](const PartT& self) - { - if (self.cut_cells == nullptr) - throw std::runtime_error( - "HOMeshPart is not attached to cut-cell storage"); - - const auto infos = - cutcells::output::selected_zero_entity_infos(self); - const int tdim = self.cut_cells->tdim; - const T nan = std::numeric_limits::quiet_NaN(); - - std::vector cut_cell_id; - std::vector parent_cell_id; - std::vector local_zero_entity_id; - std::vector zero_entity_dim; - std::vector node_index; - std::vector node_kind; - std::vector node_accepted; - std::vector selected_direction_kind; - std::vector fallback_used; - std::vector failure_reason_code; - nb::list failure_reason; - std::vector parent_entity_dim; - std::vector parent_entity_id; - std::vector requested_refinement_entity_dim; - std::vector requested_refinement_entity_id; - std::vector gradient_host_alignment; - std::vector gradient_angle_to_tangent_deg; - std::vector selected_host_alignment; - std::vector drift_amplification; - std::vector relative_correction_distance; - std::vector relative_tangential_shift; - std::vector true_transversality; - std::vector seed; - std::vector corrected; - std::vector selected_direction; - std::vector level_set_gradient_direction; - std::vector straight_helper_normal; - - auto append_vector = [&](std::vector& out, const std::vector& values) - { - for (int d = 0; d < tdim; ++d) - { - out.push_back( - d < static_cast(values.size()) - ? values[static_cast(d)] - : nan); - } - }; - - for (const auto& info : infos) - { - const cutcells::ZeroEntityGraphDiagnostics* record = nullptr; - if (info.cut_cell_id >= 0 - && info.cut_cell_id - < static_cast( - self.cut_cells->graph_diagnostics.size())) - { - const auto& diag = - self.cut_cells->graph_diagnostics[ - static_cast(info.cut_cell_id)]; - for (const auto& candidate : diag.zero_entities) - { - const std::uint64_t candidate_bit = - candidate.level_set_id >= 0 - ? (std::uint64_t(1) << candidate.level_set_id) - : std::uint64_t(0); - if (candidate.local_zero_entity_id - == info.local_zero_entity_id - && (self.expr.zero_required == 0 - || (self.expr.zero_required & candidate_bit) != 0)) - { - record = &candidate; - break; - } - } - } - if (record == nullptr) - continue; - - for (const auto& node : record->nodes) - { - cut_cell_id.push_back(info.cut_cell_id); - parent_cell_id.push_back(info.parent_cell_id); - local_zero_entity_id.push_back(info.local_zero_entity_id); - zero_entity_dim.push_back(info.dimension); - node_index.push_back(node.node_index); - node_kind.push_back(static_cast(node.node_kind)); - node_accepted.push_back(node.accepted ? 1 : 0); - selected_direction_kind.push_back( - static_cast(node.selected_direction_kind)); - fallback_used.push_back(node.fallback_used ? 1 : 0); - failure_reason_code.push_back( - static_cast(node.failure_reason)); - failure_reason.append(std::string( - cutcells::graph_criteria::failure_reason_name( - node.failure_reason))); - parent_entity_dim.push_back(node.parent_entity_dim); - parent_entity_id.push_back(node.parent_entity_id); - requested_refinement_entity_dim.push_back( - node.requested_refinement_entity_dim); - requested_refinement_entity_id.push_back( - node.requested_refinement_entity_id); - gradient_host_alignment.push_back( - node.level_set_gradient_host_alignment); - gradient_angle_to_tangent_deg.push_back( - node.level_set_gradient_angle_to_tangent_deg); - selected_host_alignment.push_back( - node.selected_host_alignment); - drift_amplification.push_back(node.drift_amplification); - relative_correction_distance.push_back( - node.relative_correction_distance); - relative_tangential_shift.push_back( - node.relative_tangential_shift); - true_transversality.push_back(node.true_transversality); - append_vector(seed, node.seed); - append_vector(corrected, node.corrected); - append_vector(selected_direction, node.selected_direction); - append_vector( - level_set_gradient_direction, - node.level_set_gradient_direction); - append_vector(straight_helper_normal, node.straight_helper_normal); - } - } - - const std::size_t n = node_index.size(); - nb::dict out; - out["cut_cell_id"] = as_nbarray(std::move(cut_cell_id)); - out["parent_cell_id"] = as_nbarray(std::move(parent_cell_id)); - out["local_zero_entity_id"] = - as_nbarray(std::move(local_zero_entity_id)); - out["zero_entity_dim"] = as_nbarray(std::move(zero_entity_dim)); - out["node_index"] = as_nbarray(std::move(node_index)); - out["node_kind"] = as_nbarray(std::move(node_kind)); - out["node_accepted"] = as_nbarray(std::move(node_accepted)); - out["selected_direction_kind"] = - as_nbarray(std::move(selected_direction_kind)); - out["fallback_used"] = as_nbarray(std::move(fallback_used)); - out["failure_reason_code"] = - as_nbarray(std::move(failure_reason_code)); - out["failure_reason"] = failure_reason; - out["parent_entity_dim"] = - as_nbarray(std::move(parent_entity_dim)); - out["parent_entity_id"] = as_nbarray(std::move(parent_entity_id)); - out["requested_refinement_entity_dim"] = - as_nbarray(std::move(requested_refinement_entity_dim)); - out["requested_refinement_entity_id"] = - as_nbarray(std::move(requested_refinement_entity_id)); - out["level_set_gradient_host_alignment"] = - as_nbarray(std::move(gradient_host_alignment)); - out["level_set_gradient_angle_to_tangent_deg"] = - as_nbarray(std::move(gradient_angle_to_tangent_deg)); - out["selected_host_alignment"] = - as_nbarray(std::move(selected_host_alignment)); - out["drift_amplification"] = - as_nbarray(std::move(drift_amplification)); - out["relative_correction_distance"] = - as_nbarray(std::move(relative_correction_distance)); - out["relative_tangential_shift"] = - as_nbarray(std::move(relative_tangential_shift)); - out["true_transversality"] = - as_nbarray(std::move(true_transversality)); - out["seed"] = as_nbarray( - std::move(seed), {n, static_cast(tdim)}); - out["corrected"] = as_nbarray( - std::move(corrected), {n, static_cast(tdim)}); - out["selected_direction"] = as_nbarray( - std::move(selected_direction), - {n, static_cast(tdim)}); - out["level_set_gradient_direction"] = as_nbarray( - std::move(level_set_gradient_direction), - {n, static_cast(tdim)}); - out["straight_helper_normal"] = as_nbarray( - std::move(straight_helper_normal), - {n, static_cast(tdim)}); - return out; - }, - "Return flat per-node graph-check diagnostics for the selected zero interface.") + "Return quadrature rules for an HOMeshPart, preserving AdaptCell topology.") .def( "write_vtu", [](const PartT& self, const std::string& filename, - const std::string& mode, - int geometry_order, - const std::string& node_family, - const std::string& projection_direction) { + const std::string& mode) { nb::gil_scoped_release release; - part_write_vtu( - self, filename, mode, geometry_order, node_family, projection_direction); + part_write_vtu(self, filename, mode); }, nb::arg("filename"), nb::arg("mode") = "full", - nb::arg("geometry_order") = -1, - nb::arg("node_family") = "gll", - nb::arg("projection_direction") = "level_set_gradient", - "Write a VTU file for an HOMeshPart. geometry_order > 1 samples the requested construction node family into curved VTK Lagrange cells."); + "Write a straight VTU file for an HOMeshPart."); // --- ho_cut() factory --- m.def("ho_cut", - [](const MeshViewT& mesh, const LevelSetT& ls, - bool triangulate, int graph_max_refinements, - const std::string& graph_projection_direction, - const std::string& graph_refinement_mode, - T min_level_set_gradient_host_alignment, - bool graph_enabled) { + [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { nb::gil_scoped_release release; - cutcells::ReadyCellGraphOptions graph_options; - graph_options.enabled = graph_enabled; - graph_options.max_refinements = graph_max_refinements; - graph_options.projection_mode = - graph_projection_mode_from_string(graph_projection_direction); - graph_options.refinement_mode = - graph_refinement_mode_from_string(graph_refinement_mode); - graph_options.min_level_set_gradient_host_alignment = - min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut( - mesh, *owned_ls, triangulate, graph_options); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, - nb::arg("graph_max_refinements") = 5, - nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_midpoint_residual", - nb::arg("min_level_set_gradient_host_alignment") = T(0.9), - nb::arg("graph_enabled") = true, - "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" + "Cut a MeshView with a single LevelSetFunction.\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("ho_cut", [](const MeshViewT& mesh, const std::vector& level_sets, - bool triangulate, int graph_max_refinements, - const std::string& graph_projection_direction, - const std::string& graph_refinement_mode, - T min_level_set_gradient_host_alignment, - bool graph_enabled) { + bool triangulate) { nb::gil_scoped_release release; - cutcells::ReadyCellGraphOptions graph_options; - graph_options.enabled = graph_enabled; - graph_options.max_refinements = graph_max_refinements; - graph_options.projection_mode = - graph_projection_mode_from_string(graph_projection_direction); - graph_options.refinement_mode = - graph_refinement_mode_from_string(graph_refinement_mode); - graph_options.min_level_set_gradient_host_alignment = - min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut( - mesh, *owned_ls, triangulate, graph_options); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, - nb::arg("graph_max_refinements") = 5, - nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_midpoint_residual", - nb::arg("min_level_set_gradient_host_alignment") = T(0.9), - nb::arg("graph_enabled") = true, - "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" + "Cut a MeshView with multiple LevelSetFunctions.\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); m.def("cut", - [](const MeshViewT& mesh, const LevelSetT& ls, - bool triangulate, int graph_max_refinements, - const std::string& graph_projection_direction, - const std::string& graph_refinement_mode, - T min_level_set_gradient_host_alignment, - bool graph_enabled) { + [](const MeshViewT& mesh, const LevelSetT& ls, bool triangulate) { nb::gil_scoped_release release; - cutcells::ReadyCellGraphOptions graph_options; - graph_options.enabled = graph_enabled; - graph_options.max_refinements = graph_max_refinements; - graph_options.projection_mode = - graph_projection_mode_from_string(graph_projection_direction); - graph_options.refinement_mode = - graph_refinement_mode_from_string(graph_refinement_mode); - graph_options.min_level_set_gradient_host_alignment = - min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared(ls); - auto [hc, bg] = cutcells::cut( - mesh, *owned_ls, triangulate, graph_options); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_set"), nb::arg("triangulate") = false, - nb::arg("graph_max_refinements") = 5, - nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_midpoint_residual", - nb::arg("min_level_set_gradient_host_alignment") = T(0.9), - nb::arg("graph_enabled") = true, - "Cut a MeshView with a single LevelSetFunction (HO pipeline).\n" + "Cut a MeshView with a single LevelSetFunction.\n" "Returns an HOCutResult; use result[\"phi < 0\"] to select parts."); m.def("cut", [](const MeshViewT& mesh, const std::vector& level_sets, - bool triangulate, int graph_max_refinements, - const std::string& graph_projection_direction, - const std::string& graph_refinement_mode, - T min_level_set_gradient_host_alignment, - bool graph_enabled) { + bool triangulate) { nb::gil_scoped_release release; - cutcells::ReadyCellGraphOptions graph_options; - graph_options.enabled = graph_enabled; - graph_options.max_refinements = graph_max_refinements; - graph_options.projection_mode = - graph_projection_mode_from_string(graph_projection_direction); - graph_options.refinement_mode = - graph_refinement_mode_from_string(graph_refinement_mode); - graph_options.min_level_set_gradient_host_alignment = - min_level_set_gradient_host_alignment; auto owned_ls = std::make_shared>(level_sets); - auto [hc, bg] = cutcells::cut( - mesh, *owned_ls, triangulate, graph_options); + auto [hc, bg] = cutcells::cut(mesh, *owned_ls, triangulate); return HOCutResult{std::move(hc), std::move(bg), owned_ls}; }, nb::arg("mesh"), nb::arg("level_sets"), nb::arg("triangulate") = true, - nb::arg("graph_max_refinements") = 5, - nb::arg("graph_projection_direction") = "level_set_gradient", - nb::arg("graph_refinement_mode") = "green_midpoint_residual", - nb::arg("min_level_set_gradient_host_alignment") = T(0.9), - nb::arg("graph_enabled") = true, - "Cut a MeshView with multiple LevelSetFunctions (HO pipeline).\n" + "Cut a MeshView with multiple LevelSetFunctions.\n" "Returns an HOCutResult; use result[\"phi1 < 0 and phi2 = 0\"] to select parts."); // Simple aliases for Python @@ -3701,48 +2592,6 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("edge_max_depth") = 20, nb::arg("triangulate_cut_parts") = false); - m.def( - "check_ready_to_cut_cell_graphs", - [](const AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, - const std::string& projection_direction, - const std::string& refinement_mode, - int graph_max_refinements, - T max_relative_correction_distance, - T max_relative_tangential_shift, - T max_drift_amplification, - T min_host_normal_alignment, - T min_level_set_gradient_host_alignment) - { - auto options = make_graph_options( - projection_direction, - refinement_mode, - graph_max_refinements, - max_relative_correction_distance, - max_relative_tangential_shift, - max_drift_amplification, - min_host_normal_alignment, - min_level_set_gradient_host_alignment, - true); - cutcells::ReadyCellGraphDiagnostics diagnostics; - { - nb::gil_scoped_release release; - diagnostics = cutcells::check_ready_to_cut_cell_graphs( - adapt_cell, ls_cell, level_set_id, options); - } - return graph_diagnostics_to_dict(diagnostics); - }, - nb::arg("adapt_cell"), - nb::arg("level_set_cell"), - nb::arg("level_set_id"), - nb::arg("projection_direction") = "level_set_gradient", - nb::arg("refinement_mode") = "green_midpoint_residual", - nb::arg("graph_max_refinements") = 5, - nb::arg("max_relative_correction_distance") = T(0.5), - nb::arg("max_relative_tangential_shift") = T(0.25), - nb::arg("max_drift_amplification") = T(4), - nb::arg("min_host_normal_alignment") = T(0.25), - nb::arg("min_level_set_gradient_host_alignment") = T(0.9)); - m.def( "refine_ready_cell_on_largest_midpoint_value", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, @@ -3798,59 +2647,6 @@ void declare_certification(nb::module_& m, const std::string& suffix) nb::arg("edge_max_depth") = 20, nb::arg("triangulate_cut_parts") = false); - m.def( - "certify_refine_graph_check_and_process_ready_cells", - [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, - int max_iterations, T zero_tol, T sign_tol, int edge_max_depth, - bool triangulate_cut_parts, const std::string& projection_direction, - const std::string& refinement_mode, - int graph_max_refinements, - T max_relative_correction_distance, - T max_relative_tangential_shift, - T max_drift_amplification, - T min_host_normal_alignment, - T min_level_set_gradient_host_alignment, - bool graph_enabled) - { - auto options = make_graph_options( - projection_direction, - refinement_mode, - graph_max_refinements, - max_relative_correction_distance, - max_relative_tangential_shift, - max_drift_amplification, - min_host_normal_alignment, - min_level_set_gradient_host_alignment, - graph_enabled); - cutcells::ReadyCellGraphDiagnostics diagnostics; - { - nb::gil_scoped_release release; - diagnostics = - cutcells::certify_refine_graph_check_and_process_ready_cells( - adapt_cell, ls_cell, level_set_id, - max_iterations, zero_tol, sign_tol, edge_max_depth, - triangulate_cut_parts, options); - } - return graph_diagnostics_to_dict(diagnostics); - }, - nb::arg("adapt_cell"), - nb::arg("level_set_cell"), - nb::arg("level_set_id"), - nb::arg("max_iterations") = 8, - nb::arg("zero_tol") = T(1e-12), - nb::arg("sign_tol") = T(1e-12), - nb::arg("edge_max_depth") = 20, - nb::arg("triangulate_cut_parts") = false, - nb::arg("projection_direction") = "level_set_gradient", - nb::arg("refinement_mode") = "green_midpoint_residual", - nb::arg("graph_max_refinements") = 5, - nb::arg("max_relative_correction_distance") = T(0.5), - nb::arg("max_relative_tangential_shift") = T(0.25), - nb::arg("max_drift_amplification") = T(4), - nb::arg("min_host_normal_alignment") = T(0.25), - nb::arg("min_level_set_gradient_host_alignment") = T(0.9), - nb::arg("graph_enabled") = true); - m.def( "certify_and_refine", [](AdaptCellT& adapt_cell, const LevelSetCellT& ls_cell, int level_set_id, diff --git a/python/demo/demo_level_set_ho_vtk.py b/python/demo/demo_level_set_ho_vtk.py deleted file mode 100644 index 5c0a942..0000000 --- a/python/demo/demo_level_set_ho_vtk.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT - -import argparse -from pathlib import Path - -import numpy as np - -import cutcells -from cutcells import box_tetrahedron_mesh, mesh_from_pyvista, rectangle_triangle_mesh - - -def _parse_degrees(arg: str) -> list[int]: - if arg == "all": - return [2, 3, 4] - return [int(arg)] - - -def _phi(X: np.ndarray) -> np.ndarray: - z = X[2] if X.shape[1] > 2 else 0.0 - return np.sqrt((X[0] - 0.15) ** 2 + (X[1] + 0.1) ** 2 + (z - 0.05) ** 2) - 0.55 - - -def _run_case(mesh, family: str, degree: int, output_dir: Path) -> None: - callback_info: dict[str, tuple[int, int]] = {} - - def phi(X: np.ndarray) -> np.ndarray: - callback_info["shape"] = tuple(X.shape) - return _phi(X) - - ls = cutcells.create_level_set(mesh, phi, degree=degree) - output_path = output_dir / f"{family}_p{degree}.vtu" - cutcells.write_level_set_vtu(str(output_path), ls, field_name="phi") - - print(f"[{family} p{degree}] cells={mesh.num_cells()}") - print(f"[{family} p{degree}] dofs={ls.mesh_data.num_dofs()}") - print(f"[{family} p{degree}] callback batch shape={callback_info.get('shape')}") - print(f"[{family} p{degree}] wrote {output_path}") - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Interpolate a higher-order level set and write Lagrange VTU files." - ) - parser.add_argument( - "--cell-family", - choices=["triangle", "tetra", "both"], - default="both", - help="Cell family to run.", - ) - parser.add_argument( - "--degree", - choices=["2", "3", "4", "all"], - default="all", - help="Polynomial degree.", - ) - parser.add_argument( - "--output-dir", - type=Path, - default=Path("demo_level_set_ho_vtu"), - help="Output directory for VTU files.", - ) - parser.add_argument( - "--n", - type=int, - default=8, - help="Structured mesh resolution parameter.", - ) - args = parser.parse_args() - - args.output_dir.mkdir(parents=True, exist_ok=True) - degrees = _parse_degrees(args.degree) - families = ( - ["triangle", "tetra"] if args.cell_family == "both" else [args.cell_family] - ) - - for family in families: - if family == "triangle": - grid = rectangle_triangle_mesh(-1.0, -1.0, 1.0, 1.0, args.n, args.n) - mesh = mesh_from_pyvista(grid, tdim=2) - else: - grid = box_tetrahedron_mesh( - -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, args.n, args.n, args.n - ) - mesh = mesh_from_pyvista(grid, tdim=3) - - for degree in degrees: - _run_case(mesh, family, degree, args.output_dir) - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_meshview_ho_cut.py b/python/demo/demo_mesh_parts.py similarity index 100% rename from python/demo/demo_meshview_ho_cut.py rename to python/demo/demo_mesh_parts.py diff --git a/python/demo/demo_meshview.py b/python/demo/demo_meshview.py deleted file mode 100644 index 85c766e..0000000 --- a/python/demo/demo_meshview.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT -""" -Demo: MeshView + LevelSetFunction Python wrappers -================================================== - -Demonstrates: - * Building a ``cutcells.MeshView`` from a pyvista ``UnstructuredGrid``. - * Building a ``cutcells.LevelSetFunction`` from a plain Python callable. - * Calling ``cutcells.cut_mesh_view`` to obtain cut meshes for - – the interface (phi = 0) - – the inside (phi < 0) - * Visualising the results with pyvista. - -The background mesh is a 2-D Delaunay triangulation of a square [-1, 1]^2. -The level-set is a circle of radius 0.7 centred at the origin: - phi(x) = sqrt(x0^2 + x1^2) - 0.7 -""" - -import argparse - -import numpy as np - -try: - import pyvista as pv -except ImportError as exc: - raise SystemExit( - "pyvista is required for this demo. " - "Install with: python -m pip install pyvista\n" - f"Import error: {exc}" - ) - -import cutcells -from cutcells import ( - mesh_from_pyvista, - rectangle_triangle_mesh, -) - - -# --------------------------------------------------------------------------- -# Level-set definition -# --------------------------------------------------------------------------- - -RADIUS = 0.7 -CENTER = np.array([0.0, 0.0, 0.0]) - - -def circle_phi(x: np.ndarray) -> float: - """Signed-distance to a circle of radius RADIUS centred at CENTER. - - ``x`` is a 1-D NumPy array of length 3 (pyvista always stores 3-D coords). - Returns a float: negative inside the circle, positive outside. - """ - return float(np.sqrt(np.sum((x - CENTER) ** 2)) - RADIUS) - - -def main(): - parser = argparse.ArgumentParser( - description="Demo: cut_mesh_view with MeshView and LevelSetFunction" - ) - parser.add_argument( - "--n", - type=int, - default=30, - help="Grid points per axis (default: 30)", - ) - parser.add_argument( - "--no-plot", - action="store_true", - help="Skip interactive visualisation (useful for CI / off-screen runs)", - ) - args = parser.parse_args() - - N = int(args.n) - - # ------------------------------------------------------------------ - # 1. Build the background pyvista grid and wrap it in a MeshView - # ------------------------------------------------------------------ - print(f"Creating {N}×{N} Delaunay triangle mesh …") - grid = rectangle_triangle_mesh(-1.0, -1.0, 1.0, 1.0, N, N) - print(f" nodes: {grid.n_points}, cells: {grid.n_cells}") - - mesh_view = mesh_from_pyvista(grid, tdim=2) - print( - f" MeshView gdim={mesh_view.gdim}, tdim={mesh_view.tdim}, " - f"num_nodes={mesh_view.num_nodes()}, num_cells={mesh_view.num_cells()}" - ) - - # ------------------------------------------------------------------ - # 2. Build a LevelSetFunction from a Python callable - # ------------------------------------------------------------------ - # The callable receives a length-3 NumPy array (x) and may optionally - # accept a second argument cell_id (ignored here). - level_set = cutcells.LevelSetFunction( - value=circle_phi, - gdim=mesh_view.gdim, # 3 (pyvista always stores 3-D coords) - ) - print( - f"\nLevelSetFunction has_value={level_set.has_value()}, " - f"has_nodal_values={level_set.has_nodal_values()}" - ) - - # ------------------------------------------------------------------ - # 3. Cut the mesh – interface (phi = 0) - # ------------------------------------------------------------------ - print("\nCutting mesh for phi=0 (interface) …") - cut_interface = cutcells.cut_mesh_view( - mesh_view, level_set, "phi=0", triangulate=True - ) - print( - f" Interface cut mesh: {len(cut_interface.vtk_types)} cells, " - f"{len(cut_interface.vertex_coords)} vertices" - ) - - # ------------------------------------------------------------------ - # 4. Cut the mesh – interior (phi < 0) - # ------------------------------------------------------------------ - print("Cutting mesh for phi<0 (inside) …") - cut_inside = cutcells.cut_mesh_view(mesh_view, level_set, "phi<0", triangulate=True) - print( - f" Inside cut mesh: {len(cut_inside.vtk_types)} cells, " - f"{len(cut_inside.vertex_coords)} vertices" - ) - - # ------------------------------------------------------------------ - # 5. (Optional) also build nodal values and use LevelSetFunction - # with nodal_values instead of value callable – same result - # ------------------------------------------------------------------ - print("\nBuilding nodal level-set values manually …") - ls_nodal = np.array([circle_phi(p) for p in grid.points], dtype=np.float64) - level_set_nodal = cutcells.LevelSetFunction( - nodal_values=ls_nodal, - gdim=mesh_view.gdim, - ) - print( - f" LevelSetFunction (nodal) has_nodal_values={level_set_nodal.has_nodal_values()}" - ) - - cut_inside_nodal = cutcells.cut_mesh_view( - mesh_view, level_set_nodal, "phi<0", triangulate=True - ) - assert len(cut_inside_nodal.vtk_types) == len(cut_inside.vtk_types), ( - "Nodal and callable level-set should give identical cut meshes" - ) - print(" Nodal and callable results agree ✓") - - # ------------------------------------------------------------------ - # 6. Runtime Quadrature and Physical Mapping - # ------------------------------------------------------------------ - print("\nComputing runtime quadrature for phi<0 …") - order = 2 - # Use the flat arrays from the grid - points_flat = np.asarray(grid.points, dtype=np.float64).ravel() - connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) - offsets = np.asarray(grid.offset, dtype=np.int32) - celltypes = np.asarray(grid.celltypes, dtype=np.int32) - - # Calculate quadrature rules in reference space for phi<0 - q_rules_inside = cutcells.runtime_quadrature( - ls_nodal, - points_flat, - connectivity, - offsets, - celltypes, - "phi<0", - triangulate=True, - order=order, - ) - print(f" Inside: Generated {len(q_rules_inside.weights)} total quadrature points.") - - # Calculate quadrature rules in reference space for phi=0 - print("Computing runtime quadrature for phi=0 …") - q_rules_interface = cutcells.runtime_quadrature( - ls_nodal, - points_flat, - connectivity, - offsets, - celltypes, - "phi=0", - triangulate=True, - order=order, - ) - print( - f" Interface: Generated {len(q_rules_interface.weights)} total quadrature points." - ) - - # Map to physical space - print(" Mapping quadrature points to physical space …") - q_points_inside_phys = cutcells.physical_points( - q_rules_inside, points_flat, connectivity, offsets, celltypes - ).reshape(-1, 3) - - q_points_interface_phys = cutcells.physical_points( - q_rules_interface, points_flat, connectivity, offsets, celltypes - ).reshape(-1, 3) - - # ------------------------------------------------------------------ - # 7. Visualise - # ------------------------------------------------------------------ - if args.no_plot: - print("\n--no-plot specified, skipping visualisation.") - return - - # -- wrap cut results as pyvista grids -- - pv_interface = pv.UnstructuredGrid( - cut_interface.cells, - cut_interface.vtk_types, - np.asarray(cut_interface.vertex_coords, dtype=np.float64), - ) - pv_inside = pv.UnstructuredGrid( - cut_inside.cells, - cut_inside.vtk_types, - np.asarray(cut_inside.vertex_coords, dtype=np.float64), - ) - - # -- inside background cells (entirely inside the circle) -- - points_flat = np.asarray(grid.points, dtype=np.float64).ravel() - connectivity = np.asarray(grid.cell_connectivity, dtype=np.int32) - offsets = np.asarray(grid.offset, dtype=np.int32) - celltypes = np.asarray(grid.celltypes, dtype=np.int32) - inside_ids = cutcells.locate_cells( - ls_nodal, points_flat, connectivity, offsets, celltypes, "phi<0" - ) - pv_inside_bg = grid.extract_cells(inside_ids) - - # -- combined inside view: background inside cells + cut cells -- - pv_combined = pv_inside_bg.merge(pv_inside) - - # -- wrap quadrature points as pyvista points -- - pv_q_points_inside = pv.PolyData(q_points_inside_phys) - pv_q_points_interface = pv.PolyData(q_points_interface_phys) - - # Plot - pl = pv.Plotter(shape=(1, 2), title="CutCells – MeshView + LevelSetFunction demo") - - pl.subplot(0, 0) - pl.add_title("Interface (phi = 0) + Quad Points") - pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) - pl.add_mesh(pv_interface, show_edges=True, color="red") - pl.add_mesh( - pv_q_points_interface, - color="yellow", - point_size=10, - render_points_as_spheres=True, - label=f"Interface Points ({len(q_rules_interface.weights)})", - ) - pl.add_legend() - pl.view_xy() - - pl.subplot(0, 1) - pl.add_title("Inside domain (phi < 0) + Quad Points") - pl.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.4) - pl.add_mesh(pv_combined, show_edges=True, color="steelblue", opacity=0.8) - pl.add_mesh( - pv_q_points_inside, - color="orange", - point_size=10, - render_points_as_spheres=True, - label=f"Inside Points ({len(q_rules_inside.weights)})", - ) - pl.add_legend() - pl.view_xy() - - pl.show() - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_meshview_ho_cut_triangulated.py b/python/demo/demo_meshview_ho_cut_triangulated.py deleted file mode 100644 index ab4c96b..0000000 --- a/python/demo/demo_meshview_ho_cut_triangulated.py +++ /dev/null @@ -1,526 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT -""" -Demo: MeshView + create_level_set + cut(mesh, ls) triangulated straight output. - -This is the triangulated counterpart of demo_meshview_ho_cut.py: -1. build a pyvista tetrahedral mesh, -2. convert it to a MeshView, -3. build one polynomial level set with create_level_set(...), -4. call cut(mesh, ls), -5. select inside/interface/outside HOMeshPart objects, -6. triangulate the straight interface and straight cut-volume output, -7. test whether the triangulation-created edges carry a root of the level set, -8. write VTU files and plot the triangulated result in pyvista. - -The edge check is important because a triangulation diagonal that crosses the -zero level set can later matter for quadrature mappings and Jacobian signs. -""" - -from __future__ import annotations - -import argparse -from collections import Counter -from pathlib import Path - -import numpy as np - -import cutcells - - -CENTER = np.array([0.1, -0.05, 0.0], dtype=np.float64) -RADIUS = 0.55 -VTK_TETRA = 10 - -EDGE_PATTERNS = { - 5: ((0, 1), (1, 2), (2, 0)), # triangle - 9: ((0, 1), (1, 2), (2, 3), (3, 0)), # quadrilateral - 10: ((0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)), # tetrahedron - 13: ( - (0, 1), - (1, 2), - (2, 0), - (3, 4), - (4, 5), - (5, 3), - (0, 3), - (1, 4), - (2, 5), - ), # prism / wedge -} - -TAG_LABELS = { - int(cutcells.EdgeRootTag.no_root.value): "no_root", - int(cutcells.EdgeRootTag.one_root.value): "one_root", - int(cutcells.EdgeRootTag.multiple_roots.value): "multiple_roots", - int(cutcells.EdgeRootTag.zero.value): "zero", -} - - -def phi_batch(X: np.ndarray) -> np.ndarray: - return np.sqrt( - (X[0] - CENTER[0]) ** 2 - + (X[1] - CENTER[1]) ** 2 - + (X[2] - CENTER[2]) ** 2 - ) - RADIUS - - -def structured_tetra_mesh(x0, y0, z0, x1, y1, z1, nx, ny, nz): - xs = np.linspace(x0, x1, num=nx) - ys = np.linspace(y0, y1, num=ny) - zs = np.linspace(z0, z1, num=nz) - xx, yy, zz = np.meshgrid(xs, ys, zs, indexing="ij") - points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()].astype(np.float64, copy=False) - - def vid(i: int, j: int, k: int) -> int: - return i + nx * (j + ny * k) - - tet_list = [] - for k in range(nz - 1): - for j in range(ny - 1): - for i in range(nx - 1): - v000 = vid(i, j, k) - v100 = vid(i + 1, j, k) - v010 = vid(i, j + 1, k) - v110 = vid(i + 1, j + 1, k) - v001 = vid(i, j, k + 1) - v101 = vid(i + 1, j, k + 1) - v011 = vid(i, j + 1, k + 1) - v111 = vid(i + 1, j + 1, k + 1) - - tet_list.extend( - ( - [v000, v100, v110, v111], - [v000, v110, v010, v111], - [v000, v010, v011, v111], - [v000, v011, v001, v111], - [v000, v001, v101, v111], - [v000, v101, v100, v111], - ) - ) - - connectivity = np.asarray(tet_list, dtype=np.int32).reshape(-1) - offsets = np.arange(0, connectivity.size + 1, 4, dtype=np.int32) - cell_types = np.full(offsets.size - 1, VTK_TETRA, dtype=np.int32) - - # Ensure positive tetra orientation for the affine maps used later. - for cell_id in range(cell_types.size): - start = offsets[cell_id] - tet = connectivity[start : start + 4].copy() - verts = points[tet] - J = np.column_stack((verts[1] - verts[0], verts[2] - verts[0], verts[3] - verts[0])) - if np.linalg.det(J) < 0.0: - connectivity[start + 1], connectivity[start + 2] = ( - connectivity[start + 2], - connectivity[start + 1], - ) - - mesh = cutcells.MeshView(points, connectivity, offsets, cell_types, tdim=3) - return mesh, points, connectivity, offsets, cell_types - - -def mesh_to_pyvista(points: np.ndarray, connectivity: np.ndarray, offsets: np.ndarray, cell_types: np.ndarray, pv): - cells = np.empty(cell_types.size * 5, dtype=np.int64) - cells[0::5] = 4 - cells.reshape(-1, 5)[:, 1:] = connectivity.reshape(-1, 4) - return pv.UnstructuredGrid(cells, np.asarray(cell_types, dtype=np.uint8), points) - - -def cutmesh_to_pyvista(cut_mesh, pv): - return pv.UnstructuredGrid( - np.array(cut_mesh.cells, dtype=np.int64, copy=True), - np.array(cut_mesh.vtk_types, dtype=np.uint8, copy=True), - np.array(cut_mesh.vertex_coords, dtype=np.float64, copy=True), - ) - - -def tetra_parent_vertices(mesh, cell_id: int) -> np.ndarray: - connectivity = np.asarray(mesh.connectivity, dtype=np.int32) - offsets = np.asarray(mesh.offsets, dtype=np.int32) - coordinates = np.asarray(mesh.coordinates, dtype=np.float64) - start = int(offsets[cell_id]) - end = int(offsets[cell_id + 1]) - vertex_ids = connectivity[start:end] - return np.array(coordinates[vertex_ids], dtype=np.float64, copy=True) - - -def physical_to_parent_reference_tetra(parent_vertices: np.ndarray, x_phys: np.ndarray) -> np.ndarray: - x0 = parent_vertices[0] - J = np.column_stack( - ( - parent_vertices[1] - x0, - parent_vertices[2] - x0, - parent_vertices[3] - x0, - ) - ) - xi = np.linalg.solve(J, np.asarray(x_phys, dtype=np.float64) - x0) - return np.array(xi, dtype=np.float64, copy=True) - - -def edge_key(parent_cell_id: int, xa: np.ndarray, xb: np.ndarray, digits: int = 12): - pa = tuple(np.round(np.asarray(xa, dtype=np.float64), digits)) - pb = tuple(np.round(np.asarray(xb, dtype=np.float64), digits)) - return (int(parent_cell_id), tuple(sorted((pa, pb)))) - - -def collect_edges_by_parent(cut_mesh) -> dict: - points = np.asarray(cut_mesh.vertex_coords, dtype=np.float64) - cells = np.asarray(cut_mesh.cells, dtype=np.int64) - vtk_types = np.asarray(cut_mesh.vtk_types, dtype=np.int64) - parent_map = np.asarray(cut_mesh.parent_map, dtype=np.int32) - - if parent_map.size != vtk_types.size: - raise RuntimeError("cut_mesh.parent_map must contain one entry per output cell.") - - edges = {} - cursor = 0 - for cell_id, vtk_type in enumerate(vtk_types): - num_verts = int(cells[cursor]) - local_ids = cells[cursor + 1 : cursor + 1 + num_verts] - cursor += num_verts + 1 - parent_cell_id = int(parent_map[cell_id]) - - for local_a, local_b in EDGE_PATTERNS[int(vtk_type)]: - xa = np.array(points[int(local_ids[local_a])], dtype=np.float64, copy=True) - xb = np.array(points[int(local_ids[local_b])], dtype=np.float64, copy=True) - edges[edge_key(parent_cell_id, xa, xb)] = { - "parent_cell_id": parent_cell_id, - "xa_phys": xa, - "xb_phys": xb, - } - - return edges - - -def classify_new_triangulation_edges(mesh, ls, base_mesh, triangulated_mesh) -> list[dict]: - base_edges = collect_edges_by_parent(base_mesh) - tri_edges = collect_edges_by_parent(triangulated_mesh) - ls_cell_cache: dict[int, object] = {} - parent_vertex_cache: dict[int, np.ndarray] = {} - records = [] - - for key in sorted(set(tri_edges) - set(base_edges)): - record = dict(tri_edges[key]) - parent_cell_id = record["parent_cell_id"] - - if parent_cell_id not in ls_cell_cache: - ls_cell_cache[parent_cell_id] = cutcells.make_cell_level_set(ls, parent_cell_id) - parent_vertex_cache[parent_cell_id] = tetra_parent_vertices(mesh, parent_cell_id) - - ls_cell = ls_cell_cache[parent_cell_id] - parent_vertices = parent_vertex_cache[parent_cell_id] - xa_ref = physical_to_parent_reference_tetra(parent_vertices, record["xa_phys"]) - xb_ref = physical_to_parent_reference_tetra(parent_vertices, record["xb_phys"]) - - edge_coeffs = np.asarray( - cutcells.restrict_edge_bernstein_exact( - ls_cell.cell_type, - ls_cell.bernstein_order, - np.asarray(ls_cell.bernstein_coeffs), - xa_ref, - xb_ref, - ), - dtype=np.float64, - ) - tag, split_t = cutcells.classify_edge_roots( - edge_coeffs, - zero_tol=1.0e-12, - sign_tol=1.0e-12, - max_depth=20, - ) - tag_value = int(tag.value) - phi_a = float( - cutcells.evaluate_bernstein( - ls_cell.cell_type, - ls_cell.bernstein_order, - np.asarray(ls_cell.bernstein_coeffs), - xa_ref, - ) - ) - phi_b = float( - cutcells.evaluate_bernstein( - ls_cell.cell_type, - ls_cell.bernstein_order, - np.asarray(ls_cell.bernstein_coeffs), - xb_ref, - ) - ) - has_zero_endpoint = abs(phi_a) <= 1.0e-12 or abs(phi_b) <= 1.0e-12 - has_root = tag_value in { - int(cutcells.EdgeRootTag.one_root.value), - int(cutcells.EdgeRootTag.multiple_roots.value), - int(cutcells.EdgeRootTag.zero.value), - } - has_interior_root = ( - tag_value in { - int(cutcells.EdgeRootTag.one_root.value), - int(cutcells.EdgeRootTag.multiple_roots.value), - } - and not has_zero_endpoint - ) - - record["xa_ref"] = xa_ref - record["xb_ref"] = xb_ref - record["phi_a"] = phi_a - record["phi_b"] = phi_b - record["edge_root_tag"] = tag_value - record["edge_has_root"] = int(has_root) - record["edge_has_zero_endpoint"] = int(has_zero_endpoint) - record["edge_has_interior_root"] = int(has_interior_root) - record["green_split_t"] = float(split_t) if split_t is not None else np.nan - records.append(record) - - return records - - -def write_line_vtu(path: Path, records: list[dict]) -> None: - if not records: - return - - points = [] - connectivity = [] - offsets = [] - root_tags = [] - has_root = [] - parent_ids = [] - split_t = [] - - for edge_id, record in enumerate(records): - points.extend(record["xa_phys"].tolist()) - points.extend(record["xb_phys"].tolist()) - connectivity.extend((2 * edge_id, 2 * edge_id + 1)) - offsets.append(2 * edge_id + 2) - root_tags.append(record["edge_root_tag"]) - has_root.append(record["edge_has_root"]) - parent_ids.append(record["parent_cell_id"]) - split_t.append(record["green_split_t"]) - - point_text = " ".join(f"{value:.16g}" for value in points) - connectivity_text = " ".join(str(value) for value in connectivity) - offsets_text = " ".join(str(value) for value in offsets) - types_text = " ".join("3" for _ in records) # VTK_LINE - root_tags_text = " ".join(str(value) for value in root_tags) - has_root_text = " ".join(str(value) for value in has_root) - parent_ids_text = " ".join(str(value) for value in parent_ids) - split_t_text = " ".join( - "nan" if np.isnan(value) else f"{value:.16g}" for value in split_t - ) - - xml = f""" - - - - - - {point_text} - - - - - {connectivity_text} - - - {offsets_text} - - - {types_text} - - - - - {root_tags_text} - - - {has_root_text} - - - {parent_ids_text} - - - {split_t_text} - - - - - -""" - path.write_text(xml) - - -def summarize_records(records: list[dict]) -> dict[str, int]: - counts = Counter(TAG_LABELS[record["edge_root_tag"]] for record in records) - return dict(sorted(counts.items())) - - -def count_interior_root_edges(records: list[dict]) -> int: - return int(sum(record["edge_has_interior_root"] for record in records)) - - -def count_zero_endpoint_edges(records: list[dict]) -> int: - return int(sum(record["edge_has_zero_endpoint"] for record in records)) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Triangulated HO straight-output demo with triangulation-edge root checks." - ) - parser.add_argument( - "--n", - type=int, - default=8, - help="Structured tetrahedral mesh resolution parameter.", - ) - parser.add_argument( - "--degree", - type=int, - default=2, - help="Polynomial degree for create_level_set(...).", - ) - parser.add_argument( - "--output-dir", - type=Path, - default=Path("demo_meshview_ho_cut_triangulated_output"), - help="Directory for VTU files.", - ) - parser.add_argument( - "--no-plot", - action="store_true", - help="Skip interactive pyvista plotting.", - ) - args = parser.parse_args() - - args.output_dir.mkdir(parents=True, exist_ok=True) - - print(f"Creating tetrahedral pyvista mesh with n={args.n} ...") - mesh, points, connectivity, offsets, cell_types = structured_tetra_mesh( - -1.0, -1.0, -1.0, - 1.0, 1.0, 1.0, - args.n, args.n, args.n, - ) - print(f" tetra cells={mesh.num_cells()}, points={mesh.num_nodes()}") - print(f" MeshView gdim={mesh.gdim}, tdim={mesh.tdim}") - - print(f"Building polynomial level set with degree={args.degree} ...") - ls = cutcells.create_level_set(mesh, phi_batch, degree=args.degree, name="phi") - print(f" level-set dofs={ls.mesh_data.num_dofs()}") - - print("Running cut(mesh, ls) ...") - result = cutcells.cut(mesh, ls) - negative = result["phi < 0"] - interface = result["phi = 0"] - positive = result["phi > 0"] - - print(f" num_cut_cells={result.num_cut_cells}") - print(f" phi < 0 : cut={negative.num_cut_cells}, uncut={negative.num_uncut_cells}") - print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") - print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") - - print("Building straight visualization meshes from HOMeshPart ...") - inside_full = negative.visualization_mesh(mode="full") - inside_cut = negative.visualization_mesh(mode="cut_only") - outside_full = positive.visualization_mesh(mode="full") - outside_cut = positive.visualization_mesh(mode="cut_only") - interface_mesh = interface.visualization_mesh(mode="cut_only") - - print(f" inside full cells={len(np.asarray(inside_full.types))}") - print(f" inside cut-only cells={len(np.asarray(inside_cut.types))}") - print(f" interface cells={len(np.asarray(interface_mesh.types))}") - print(f" outside full cells={len(np.asarray(outside_full.types))}") - print(f" outside cut-only cells={len(np.asarray(outside_cut.types))}") - - print("Building reference meshes for the new-edge check ...") - inside_full_base = negative.visualization_mesh(mode="full") - inside_cut_base = negative.visualization_mesh(mode="cut_only") - outside_full_base = positive.visualization_mesh(mode="full") - outside_cut_base = positive.visualization_mesh(mode="cut_only") - interface_base = interface.visualization_mesh(mode="cut_only") - - print("Checking whether triangulation-created edges carry level-set roots ...") - edge_checks = { - "phi_negative_full": classify_new_triangulation_edges( - mesh, ls, inside_full_base, inside_full - ), - "phi_negative_cut_only": classify_new_triangulation_edges( - mesh, ls, inside_cut_base, inside_cut - ), - "phi_interface": classify_new_triangulation_edges( - mesh, ls, interface_base, interface_mesh - ), - "phi_positive_full": classify_new_triangulation_edges( - mesh, ls, outside_full_base, outside_full - ), - "phi_positive_cut_only": classify_new_triangulation_edges( - mesh, ls, outside_cut_base, outside_cut - ), - } - - for name, records in edge_checks.items(): - print( - f" {name}: new_edges={len(records)}, " - f"interior_root_edges={count_interior_root_edges(records)}, " - f"zero_endpoint_edges={count_zero_endpoint_edges(records)}, " - f"tags={summarize_records(records)}" - ) - - for stem, records in edge_checks.items(): - if not records: - continue - path = args.output_dir / f"{stem}_triangulation_new_edges.vtu" - write_line_vtu(path, records) - print(f" wrote {path}") - - mesh_outputs = [ - (negative, args.output_dir / "phi_negative_full_triangulated.vtu", "full"), - (negative, args.output_dir / "phi_negative_cut_only_triangulated.vtu", "cut_only"), - (interface, args.output_dir / "phi_interface_triangulated.vtu", "cut_only"), - (positive, args.output_dir / "phi_positive_full_triangulated.vtu", "full"), - (positive, args.output_dir / "phi_positive_cut_only_triangulated.vtu", "cut_only"), - ] - - for part, path, mode in mesh_outputs: - part.write_vtu(str(path), mode=mode) - print(f" wrote {path}") - - if args.no_plot: - return - - try: - import pyvista as pv - except Exception as exc: - raise SystemExit( - "pyvista is required for plotting this demo. " - "The VTU files were still written successfully.\n" - f"Import error: {exc}" - ) - - pv_inside_full = cutmesh_to_pyvista(inside_full, pv) - pv_interface = cutmesh_to_pyvista(interface_mesh, pv) - pv_outside_full = cutmesh_to_pyvista(outside_full, pv) - grid = mesh_to_pyvista(points, connectivity, offsets, cell_types, pv) - - plotter = pv.Plotter(shape=(1, 3), title="CutCells HO triangulated straight-output demo") - plotter.subplot(0, 0) - plotter.add_title("phi < 0, mode='full', triangulated") - plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.18) - plotter.add_mesh(pv_inside_full, color="steelblue", opacity=0.78, show_edges=True) - - plotter.subplot(0, 1) - plotter.add_title("phi = 0, mode='cut_only', triangulated") - plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.12) - plotter.add_mesh(pv_interface, color="crimson", opacity=0.95, show_edges=True) - - plotter.subplot(0, 2) - plotter.add_title("phi > 0, mode='full', triangulated") - plotter.add_mesh(grid, style="wireframe", color="lightgrey", opacity=0.18) - plotter.add_mesh(pv_outside_full, color="burlywood", opacity=0.78, show_edges=True) - - plotter.link_views() - plotter.show() - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_sphere_p2_cut_vtu.py b/python/demo/demo_sphere_p2_cut_vtu.py deleted file mode 100644 index 98322b8..0000000 --- a/python/demo/demo_sphere_p2_cut_vtu.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2026 ONERA -# Authors: Susanne Claus -# This file is part of CutCells -# -# SPDX-License-Identifier: MIT -""" -Demo: cut one sphere level set and write a P2 Lagrange VTU. - -The demo builds a structured tetrahedral MeshView, interpolates one sphere -level set with P=2 nodes, cuts the mesh with CutCells, and writes the curved -P2 zero interface as a higher-order VTU file. The cut path uses -triangulation=true by default. -""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -import numpy as np - -import cutcells - - -P = 2 -VTK_TETRA = 10 -DEFAULT_CENTER = np.array([0.15, -0.1, 0.05], dtype=np.float64) -DEFAULT_RADIUS = 0.55 - - -def sphere_phi(X: np.ndarray, center: np.ndarray, radius: float) -> np.ndarray: - return np.sqrt( - (X[0] - center[0]) ** 2 - + (X[1] - center[1]) ** 2 - + (X[2] - center[2]) ** 2 - ) - radius - - -def structured_tetra_mesh( - x0: float, - y0: float, - z0: float, - x1: float, - y1: float, - z1: float, - nx: int, - ny: int, - nz: int, -) -> cutcells.MeshView: - xs = np.linspace(x0, x1, num=nx) - ys = np.linspace(y0, y1, num=ny) - zs = np.linspace(z0, z1, num=nz) - xx, yy, zz = np.meshgrid(xs, ys, zs, indexing="ij") - points = np.c_[xx.ravel(), yy.ravel(), zz.ravel()].astype(np.float64, copy=False) - - def vertex_id(i: int, j: int, k: int) -> int: - return i + nx * (j + ny * k) - - tets: list[list[int]] = [] - for k in range(nz - 1): - for j in range(ny - 1): - for i in range(nx - 1): - v000 = vertex_id(i, j, k) - v100 = vertex_id(i + 1, j, k) - v010 = vertex_id(i, j + 1, k) - v110 = vertex_id(i + 1, j + 1, k) - v001 = vertex_id(i, j, k + 1) - v101 = vertex_id(i + 1, j, k + 1) - v011 = vertex_id(i, j + 1, k + 1) - v111 = vertex_id(i + 1, j + 1, k + 1) - - tets.extend( - ( - [v000, v100, v110, v111], - [v000, v110, v010, v111], - [v000, v010, v011, v111], - [v000, v011, v001, v111], - [v000, v001, v101, v111], - [v000, v101, v100, v111], - ) - ) - - connectivity = np.asarray(tets, dtype=np.int32).reshape(-1) - offsets = np.arange(0, connectivity.size + 1, 4, dtype=np.int32) - cell_types = np.full(offsets.size - 1, VTK_TETRA, dtype=np.int32) - - for cell_id in range(cell_types.size): - start = int(offsets[cell_id]) - tet = connectivity[start : start + 4].copy() - vertices = points[tet] - jacobian = np.column_stack( - ( - vertices[1] - vertices[0], - vertices[2] - vertices[0], - vertices[3] - vertices[0], - ) - ) - if np.linalg.det(jacobian) < 0.0: - connectivity[start + 1], connectivity[start + 2] = ( - connectivity[start + 2], - connectivity[start + 1], - ) - - return cutcells.MeshView(points, connectivity, offsets, cell_types, tdim=3) - - -def write_background_mesh(path: Path, mesh: cutcells.MeshView) -> None: - coordinates = np.ascontiguousarray(np.asarray(mesh.coordinates).reshape(-1)) - connectivity = np.ascontiguousarray(np.asarray(mesh.connectivity), dtype=np.int32) - offsets = np.ascontiguousarray(np.asarray(mesh.offsets), dtype=np.int32) - cell_types = np.ascontiguousarray(np.asarray(mesh.cell_types), dtype=np.int32) - - cutcells.write_vtk( - str(path), - coordinates, - connectivity, - offsets, - cell_types, - gdim=mesh.gdim, - ) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Cut one sphere level set and write a P2 Lagrange VTU." - ) - parser.add_argument( - "--n", - type=int, - default=7, - help="Number of grid points per coordinate direction.", - ) - parser.add_argument( - "--center", - type=float, - nargs=3, - default=DEFAULT_CENTER.tolist(), - metavar=("X", "Y", "Z"), - help="Sphere center.", - ) - parser.add_argument( - "--radius", - type=float, - default=DEFAULT_RADIUS, - help="Sphere radius.", - ) - parser.add_argument( - "--output-dir", - type=Path, - default=Path("demo_sphere_p2_cut_vtu_output"), - help="Directory for generated VTU files.", - ) - parser.add_argument( - "--node-family", - choices=["lagrange", "gll"], - default="gll", - help="Construction/output nodes for the P2 geometry.", - ) - parser.add_argument( - "--write-domains", - action="store_true", - help="Also write P2 VTUs for phi < 0 and phi > 0 on cut cells.", - ) - parser.add_argument( - "--triangulate", - action=argparse.BooleanOptionalAction, - default=True, - help="Triangulate cut parts before extracting VTU output.", - ) - args = parser.parse_args() - - if args.n < 2: - raise SystemExit("--n must be at least 2") - if args.radius <= 0.0: - raise SystemExit("--radius must be positive") - - center = np.asarray(args.center, dtype=np.float64) - args.output_dir.mkdir(parents=True, exist_ok=True) - - print(f"Building structured tetrahedral mesh with n={args.n} ...") - mesh = structured_tetra_mesh( - -1.0, - -1.0, - -1.0, - 1.0, - 1.0, - 1.0, - args.n, - args.n, - args.n, - ) - print(f" cells={mesh.num_cells()}, points={mesh.num_nodes()}") - - background_path = args.output_dir / "sphere_background_mesh.vtu" - write_background_mesh(background_path, mesh) - print(f" wrote {background_path}") - - print("Interpolating one P2 sphere level set ...") - ls = cutcells.create_level_set( - mesh, - lambda X: sphere_phi(X, center, args.radius), - degree=P, - name="phi", - ) - print(f" level-set dofs={ls.mesh_data.num_dofs()}") - - print(f"Running CutCells cut(mesh, ls, triangulate={args.triangulate}) ...") - result = cutcells.cut(mesh, ls, triangulate=args.triangulate) - negative = result["phi < 0"] - interface = result["phi = 0"] - positive = result["phi > 0"] - - print(f" num_cut_cells={result.num_cut_cells}") - print(f" phi < 0 : cut={negative.num_cut_cells}, uncut={negative.num_uncut_cells}") - print(f" phi = 0 : cut={interface.num_cut_cells}, uncut={interface.num_uncut_cells}") - print(f" phi > 0 : cut={positive.num_cut_cells}, uncut={positive.num_uncut_cells}") - - if interface.num_cut_cells == 0: - raise RuntimeError( - "The sphere does not cut the mesh. Adjust --center, --radius, or --n." - ) - - suffix = "_triangulated" if args.triangulate else "" - interface_path = args.output_dir / f"sphere_interface{suffix}_p2.vtu" - interface.write_vtu( - str(interface_path), - mode="cut_only", - geometry_order=P, - node_family=args.node_family, - ) - print(f" wrote {interface_path}") - - if args.write_domains: - inside_path = args.output_dir / f"sphere_inside_cut_only{suffix}_p2.vtu" - outside_path = args.output_dir / f"sphere_outside_cut_only{suffix}_p2.vtu" - negative.write_vtu( - str(inside_path), - mode="cut_only", - geometry_order=P, - node_family=args.node_family, - ) - positive.write_vtu( - str(outside_path), - mode="cut_only", - geometry_order=P, - node_family=args.node_family, - ) - print(f" wrote {inside_path}") - print(f" wrote {outside_path}") - - -if __name__ == "__main__": - main() diff --git a/python/demo/demo_two_level_sets.py b/python/demo/demo_two_level_sets.py new file mode 100644 index 0000000..cd85955 --- /dev/null +++ b/python/demo/demo_two_level_sets.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 ONERA +# Authors: Susanne Claus +# This file is part of CutCells +# +# SPDX-License-Identifier: MIT +""" +Demo: cut a tetrahedral mesh with two level sets. + +The example uses a sphere and a plane: + sphere(x) = |x - c| - r + plane(x) = n . x - offset + +It writes straight VTU output for several mesh-part selections, including +single-interface parts, a codimension-two intersection curve, and volume +regions selected by both level sets. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np + +import cutcells +from cutcells import box_tetrahedron_mesh, mesh_from_pyvista + + +DEFAULT_CENTER = np.array([0.1, -0.08, 0.05], dtype=np.float64) +DEFAULT_RADIUS = 0.58 +DEFAULT_PLANE_NORMAL = np.array([1.0, -0.35, 0.2], dtype=np.float64) +DEFAULT_PLANE_OFFSET = 0.07 +TRIANGULATE = True +OUTPUT_DIR = Path("demo_two_level_sets_output") + + +def sphere_phi(X: np.ndarray, center: np.ndarray, radius: float) -> np.ndarray: + return ( + np.sqrt( + (X[0] - center[0]) ** 2 + (X[1] - center[1]) ** 2 + (X[2] - center[2]) ** 2 + ) + - radius + ) + + +def plane_phi(X: np.ndarray, normal: np.ndarray, offset: float) -> np.ndarray: + return normal[0] * X[0] + normal[1] * X[1] + normal[2] * X[2] - offset + + +def write_part(part, path: Path, mode: str = "cut_only") -> None: + part.write_vtu(str(path), mode=mode) + print( + f" {path.name:<38} dim={part.dim}, " + f"cut={part.num_cut_cells}, uncut={part.num_uncut_cells}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Cut a tetrahedral mesh with a sphere and a plane level set." + ) + parser.add_argument( + "--n", + type=int, + default=7, + help="Number of grid points per coordinate direction.", + ) + args = parser.parse_args() + + if args.n < 2: + raise SystemExit("--n must be at least 2") + + center = DEFAULT_CENTER + normal = DEFAULT_PLANE_NORMAL.copy() + norm = float(np.linalg.norm(normal)) + if norm == 0.0: + raise RuntimeError("DEFAULT_PLANE_NORMAL must be nonzero") + normal /= norm + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + print(f"Building tetrahedral box mesh with n={args.n} ...") + grid = box_tetrahedron_mesh( + -1.0, + -1.0, + -1.0, + 1.0, + 1.0, + 1.0, + args.n, + args.n, + args.n, + ) + mesh = mesh_from_pyvista(grid, tdim=3) + print(f" cells={mesh.num_cells()}, points={mesh.num_nodes()}") + + background_path = OUTPUT_DIR / "background_mesh.vtu" + grid.save(background_path) + print(f" wrote {background_path}") + + print("Interpolating level sets ...") + sphere = cutcells.create_level_set( + mesh, + lambda X: sphere_phi(X, center, DEFAULT_RADIUS), + degree=1, + name="sphere", + ) + plane = cutcells.create_level_set( + mesh, + lambda X: plane_phi(X, normal, DEFAULT_PLANE_OFFSET), + degree=1, + name="plane", + ) + print(f" sphere dofs={sphere.mesh_data.num_dofs()}") + print(f" plane dofs={plane.mesh_data.num_dofs()}") + + print(f"Running cut(mesh, [sphere, plane], triangulate={TRIANGULATE}) ...") + result = cutcells.cut(mesh, [sphere, plane], triangulate=TRIANGULATE) + print(f" level sets={list(result.level_set_names)}") + print(f" num_cut_cells={result.num_cut_cells}") + + outputs = [ + ("sphere = 0 and plane < 0", "sphere_interface_plane_negative.vtu", "cut_only"), + ("sphere = 0 and plane > 0", "sphere_interface_plane_positive.vtu", "cut_only"), + ("plane = 0 and sphere < 0", "plane_disk_inside_sphere.vtu", "cut_only"), + ("sphere = 0 and plane = 0", "sphere_plane_intersection_curve.vtu", "cut_only"), + ("sphere < 0 and plane < 0", "inside_sphere_plane_negative.vtu", "cut_only"), + ("sphere < 0 and plane > 0", "inside_sphere_plane_positive.vtu", "cut_only"), + ] + + print("Writing selected mesh parts ...") + for expr, filename, mode in outputs: + part = result[expr] + write_part(part, OUTPUT_DIR / filename, mode=mode) + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index da02724..4c537fd 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -27,28 +27,6 @@ def _single_tetra_mesh(): return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) -def _tetra_line_interval(seed, direction): - lo = -np.inf - hi = np.inf - constraints = [ - (np.array([1.0, 0.0, 0.0]), 0.0), - (np.array([0.0, 1.0, 0.0]), 0.0), - (np.array([0.0, 0.0, 1.0]), 0.0), - (np.array([-1.0, -1.0, -1.0]), -1.0), - ] - for normal, lower in constraints: - value = float(np.dot(normal, seed) - lower) - slope = float(np.dot(normal, direction)) - if abs(slope) <= 1.0e-14: - if value < -1.0e-14: - return np.nan, np.nan - continue - bound = -value / slope - if slope > 0.0: - lo = max(lo, bound) - else: - hi = min(hi, bound) - return lo, hi class CertificationRefinementTests(unittest.TestCase): @@ -247,7 +225,7 @@ def test_triangle_ready_to_cut_cell_is_replaced_by_polygonal_lut_cells(self): ]), ) - def test_ready_to_cut_triangle_uses_exact_one_root_vertices(self): + def test_ready_to_cut_triangle_uses_linear_one_root_vertices(self): mesh = _single_triangle_mesh() ls = cutcells.create_level_set( mesh, @@ -263,61 +241,20 @@ def test_ready_to_cut_triangle_uses_exact_one_root_vertices(self): coords = np.asarray(adapt.vertex_coords).reshape(-1, 2) source_edges = np.asarray(adapt.vertex_source_edge_id) - expected_curved_root = np.array([0.5, 0.0]) expected_linear_root = np.array([0.25, 0.0]) expected_second_root = np.array([0.0, 0.25]) - d_curved = np.linalg.norm(coords - expected_curved_root, axis=1) - d_second = np.linalg.norm(coords - expected_second_root, axis=1) d_linear = np.linalg.norm(coords - expected_linear_root, axis=1) + d_second = np.linalg.norm(coords - expected_second_root, axis=1) - curved_id = int(np.argmin(d_curved)) + linear_id = int(np.argmin(d_linear)) second_id = int(np.argmin(d_second)) - self.assertLess(d_curved[curved_id], 1e-10) + self.assertLess(d_linear[linear_id], 1e-10) self.assertLess(d_second[second_id], 1e-10) - self.assertGreater(np.min(d_linear), 1e-2) - self.assertGreaterEqual(int(source_edges[curved_id]), 0) + self.assertGreaterEqual(int(source_edges[linear_id]), 0) self.assertGreaterEqual(int(source_edges[second_id]), 0) - def test_graph_check_failure_refines_original_uncut_ready_triangle(self): - mesh = _single_triangle_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] - 0.25, - degree=2, - ) - adapt = cutcells.make_adapt_cell(mesh, 0) - cutcells.build_edges(adapt) - ls_cell = cutcells.make_cell_level_set(ls, 0) - - diagnostics = cutcells.certify_refine_graph_check_and_process_ready_cells( - adapt, - ls_cell, - 0, - projection_direction="straight_zero_entity_normal", - graph_max_refinements=2, - max_relative_correction_distance=0.01, - ) - - self.assertFalse(bool(diagnostics["accepted"])) - self.assertEqual(int(diagnostics["graph_refinements"]), 2) - self.assertEqual( - str(diagnostics["first_failure_reason"]), - "excessive_correction_distance", - ) - self.assertGreater(adapt.num_cells(), 2) - - tags = np.asarray(adapt.cell_cert_tags(0)) - self.assertEqual( - sorted(np.unique(tags).tolist()), - sorted([ - cutcells.CellCertTag.negative.value, - cutcells.CellCertTag.positive.value, - cutcells.CellCertTag.cut.value, - ]), - ) - def test_ho_cut_smoke_uses_certified_triangle_pipeline(self): mesh = _single_triangle_mesh() ls = cutcells.create_level_set( @@ -327,7 +264,7 @@ def test_ho_cut_smoke_uses_certified_triangle_pipeline(self): name="phi", ) - result = cutcells.ho_cut(mesh, ls, graph_enabled=False) + result = cutcells.ho_cut(mesh, ls) self.assertEqual(result.num_cut_cells, 1) np.testing.assert_array_equal(np.asarray(result.parent_cell_ids), np.array([0])) @@ -354,270 +291,6 @@ def test_cut_overload_supports_mesh_part_selection(self): self.assertGreater(adapt.num_cells(), 0) self.assertGreater(adapt.num_vertices(), 3) - def test_graph_check_data_is_attached_to_zero_interface_cells(self): - mesh = _single_triangle_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] - 0.25, - degree=2, - name="phi", - ) - - result = cutcells.cut( - mesh, - ls, - graph_max_refinements=0, - min_level_set_gradient_host_alignment=0.0, - ) - interface = result["phi = 0"] - zero_mesh = interface.visualization_mesh(mode="cut_only", geometry_order=1) - data = interface.graph_check_zero_entity_data() - summary = result.graph_check_summary() - - num_zero_cells = np.asarray(zero_mesh.offset).size - 1 - self.assertGreater(num_zero_cells, 0) - self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) == 0)) - self.assertEqual(np.asarray(data["local_zero_entity_id"]).shape, (num_zero_cells,)) - self.assertEqual(np.asarray(data["graph_accepted"]).shape, (num_zero_cells,)) - self.assertTrue(np.all(np.asarray(data["zero_entity_dim"]) == interface.dim)) - self.assertTrue(np.all(np.asarray(data["graph_accepted"]) >= 0)) - self.assertTrue(np.all(np.isfinite(np.asarray(data["graph_max_correction"])))) - for key in ( - "graph_failed_projection_seed", - "graph_failed_projection_direction", - "graph_failed_projection_clip_lo", - "graph_failed_projection_clip_hi", - "graph_failed_projection_root_t", - ): - self.assertIn(key, data) - self.assertEqual( - np.asarray(data["graph_failed_projection_seed"]).shape, - (num_zero_cells, 2), - ) - self.assertEqual( - np.asarray(data["graph_failed_projection_direction"]).shape, - (num_zero_cells, 2), - ) - self.assertEqual( - np.asarray(data["graph_failed_projection_clip_lo"]).shape, - (num_zero_cells,), - ) - - def test_sphere_graph_check_reports_surface_jacobian_and_node_data(self): - grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) - mesh = cutcells.mesh_from_pyvista(grid) - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - result = cutcells.cut(mesh, ls, graph_max_refinements=0) - interface = result["phi = 0"] - data = interface.graph_check_zero_entity_data() - node_data = interface.graph_check_node_data() - - self.assertIn("graph_min_level_set_gradient_host_alignment", data) - self.assertIn("graph_min_surface_jacobian_ratio", data) - self.assertIn("graph_failed_surface_jacobian_ratio", data) - - node_accepted = np.asarray(node_data["node_accepted"], dtype=np.int32) - self.assertGreater(node_accepted.size, 0) - gradient_alignment = np.asarray( - node_data["level_set_gradient_host_alignment"], dtype=np.float64) - finite = np.isfinite(gradient_alignment) - self.assertTrue(np.any(finite)) - self.assertTrue(np.all(gradient_alignment[finite] >= 0.0)) - - def test_cut_graph_check_accepts_level_set_gradient_mode(self): - mesh = _single_triangle_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] - 0.25, - degree=2, - name="phi", - ) - - result = cutcells.cut( - mesh, - ls, - graph_max_refinements=0, - graph_projection_direction="level_set_gradient", - ) - data = result["phi = 0"].graph_check_zero_entity_data() - - self.assertTrue(np.all(np.asarray(result.graph_check_summary()["graph_refinements"]) == 0)) - self.assertGreater(np.asarray(data["graph_accepted"]).size, 0) - - def test_cut_graph_check_accepts_red_failed_cell_refinement_mode(self): - grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) - mesh = cutcells.mesh_from_pyvista(grid) - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - result = cutcells.cut( - mesh, - ls, - graph_max_refinements=1, - graph_refinement_mode="red_failed_cell", - ) - summary = result.graph_check_summary() - - self.assertGreater(np.asarray(summary["accepted"]).size, 0) - self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) - - def test_cut_graph_check_accepts_orthogonal_surface_edge_refinement_mode(self): - grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) - mesh = cutcells.mesh_from_pyvista(grid) - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - result = cutcells.cut( - mesh, - ls, - graph_max_refinements=1, - graph_refinement_mode="green_orthogonal_surface_edge", - ) - summary = result.graph_check_summary() - - self.assertGreater(np.asarray(summary["accepted"]).size, 0) - self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) - - def test_cut_graph_check_accepts_surface_error_refinement_modes(self): - grid = cutcells.box_tetrahedron_mesh(-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 3, 3, 3) - mesh = cutcells.mesh_from_pyvista(grid) - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - for mode in ("green_midpoint_residual", "green_normal_variation"): - result = cutcells.cut( - mesh, - ls, - graph_max_refinements=1, - graph_refinement_mode=mode, - ) - summary = result.graph_check_summary() - self.assertGreater(np.asarray(summary["accepted"]).size, 0) - self.assertTrue(np.all(np.asarray(summary["graph_refinements"]) >= 0)) - - def test_scalar_projection_records_parent_clipped_tetra_bracket(self): - mesh = _single_tetra_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - result = cutcells.ho_cut( - mesh, - ls, - min_level_set_gradient_host_alignment=0.0, - ) - curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") - - seeds = np.asarray(curved["node_seed"], dtype=np.float64) - directions = np.asarray(curved["node_direction"], dtype=np.float64) - clip_lo = np.asarray(curved["node_clip_lo"], dtype=np.float64) - clip_hi = np.asarray(curved["node_clip_hi"], dtype=np.float64) - root_t = np.asarray(curved["node_root_t"], dtype=np.float64) - - projected = np.flatnonzero(np.isfinite(root_t)) - self.assertGreater(projected.size, 0) - for idx in projected: - seed = seeds[idx] - direction = directions[idx] - lo, hi = _tetra_line_interval(seed, direction) - np.testing.assert_allclose([clip_lo[idx], clip_hi[idx]], [lo, hi], atol=1e-12) - self.assertFalse(np.isclose(clip_lo[idx], -2.0)) - self.assertFalse(np.isclose(clip_hi[idx], 2.0)) - self.assertLessEqual(clip_lo[idx] - 1e-12, root_t[idx]) - self.assertLessEqual(root_t[idx], clip_hi[idx] + 1e-12) - - phi_seed = float(np.dot(seed, seed) - 0.36) - grad_seed = 2.0 * seed - self.assertGreaterEqual(float(np.dot(grad_seed, direction)), -1e-12) - if phi_seed > 0.0: - self.assertLessEqual(root_t[idx], 1e-12) - elif phi_seed < 0.0: - self.assertGreaterEqual(root_t[idx], -1e-12) - - def test_straight_normal_projection_is_gradient_oriented(self): - mesh = _single_tetra_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] * X[0] + X[1] * X[1] + X[2] * X[2] - 0.36, - degree=2, - name="phi", - ) - - result = cutcells.ho_cut( - mesh, - ls, - min_level_set_gradient_host_alignment=0.0, - ) - curved = result.curved_zero_nodes( - geometry_order=2, - node_family="gll", - projection_direction="straight_zero_entity_normal", - ) - - seeds = np.asarray(curved["node_seed"], dtype=np.float64) - directions = np.asarray(curved["node_direction"], dtype=np.float64) - root_t = np.asarray(curved["node_root_t"], dtype=np.float64) - projected = np.flatnonzero(np.isfinite(root_t)) - self.assertGreater(projected.size, 0) - for idx in projected: - self.assertGreaterEqual(float(np.dot(2.0 * seeds[idx], directions[idx])), -1e-12) - - def test_triangulated_zero_quad_faces_curve_across_artificial_diagonal(self): - mesh = _single_tetra_mesh() - ls = cutcells.create_level_set( - mesh, - lambda X: X[0] + X[1] - 0.5, - degree=1, - name="phi", - ) - - result = cutcells.ho_cut( - mesh, - ls, - triangulate=True, - graph_max_refinements=0, - min_level_set_gradient_host_alignment=0.0, - ) - curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") - graph_nodes = result["phi = 0"].graph_check_node_data() - - dim = np.asarray(curved["dim"], dtype=np.int32) - status = np.asarray(curved["status"], dtype=np.uint8) - edge_states = np.flatnonzero(dim == 1) - face_states = np.flatnonzero(dim == 2) - - self.assertGreaterEqual(edge_states.size, 5) - self.assertTrue(np.all(status[edge_states] == 2)) # CurvingStatus::curved - self.assertGreaterEqual(face_states.size, 2) - self.assertTrue(np.all(status[face_states] == 2)) # CurvingStatus::curved - face_interior_nodes = ( - np.asarray(graph_nodes["node_kind"], dtype=np.int32) == 3 - ) - self.assertTrue(np.any( - np.asarray(graph_nodes["node_index"], dtype=np.int32)[face_interior_nodes] > 0 - )) - def test_zero_face_final_incidence_uses_face_to_cell_connectivity(self): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( @@ -630,8 +303,6 @@ def test_zero_face_final_incidence_uses_face_to_cell_connectivity(self): result = cutcells.ho_cut( mesh, ls, - graph_max_refinements=0, - min_level_set_gradient_host_alignment=0.0, ) adapt = result.adapt_cell(0) @@ -649,72 +320,6 @@ def test_zero_face_final_incidence_uses_face_to_cell_connectivity(self): cell_types = np.asarray(adapt.cell_types, dtype=np.int32) self.assertEqual(cell_types[incident_cells].tolist(), [6, 6]) - def test_curving_accepts_near_zero_multi_level_set_node_without_projection(self): - mesh = _single_triangle_mesh() - - def phi(X): - return X[0] + X[1] - 0.25 + 5.12e-13 * X[0] * X[1] - - ls0 = cutcells.create_level_set(mesh, phi, degree=2, name="phi") - ls1 = cutcells.create_level_set(mesh, phi, degree=2, name="psi") - - result = cutcells.ho_cut(mesh, [ls0, ls1]) - curved = result.curved_zero_nodes(geometry_order=2, node_family="gll") - - status = np.asarray(curved["status"], dtype=np.uint8) - dim = np.asarray(curved["dim"], dtype=np.int32) - zero_mask = np.asarray(curved["zero_mask"], dtype=np.uint64) - offsets = np.asarray(curved["offsets"], dtype=np.int32) - stats_offsets = np.asarray(curved["stats_offsets"], dtype=np.int32) - node_iterations = np.asarray(curved["node_iterations"], dtype=np.int32) - node_residual = np.asarray(curved["node_residual"], dtype=np.float64) - node_projection_mode = np.asarray(curved["node_projection_mode"], dtype=np.uint8) - points = np.asarray(curved["points"], dtype=np.float64) - - self.assertTrue(np.all(status == 2)) # CurvingStatus::curved - edge_state = int(np.flatnonzero((dim == 1) & (zero_mask == 3))[0]) - node_begin = int(offsets[edge_state]) - node_end = int(offsets[edge_state + 1]) - stat_begin = int(stats_offsets[edge_state]) - stat_end = int(stats_offsets[edge_state + 1]) - - self.assertEqual(node_end - node_begin, 3) - self.assertEqual(stat_end - stat_begin, 3) - mid_node = node_begin + 1 - mid_stat = stat_begin + 1 - - np.testing.assert_allclose(points[mid_node], [0.125, 0.125], atol=1e-15) - self.assertLess(node_residual[mid_stat], 1.0e-12) - self.assertEqual(int(node_iterations[mid_stat]), 0) - self.assertEqual(int(node_projection_mode[mid_stat]), 0) # CurvingProjectionMode::none - - def test_curving_keeps_only_short_edge_faces_straight(self): - mesh = _single_tetra_mesh() - eps = 1.0e-4 - - def phi(X): - return X[0] + X[1] - eps - - ls = cutcells.create_level_set(mesh, phi, degree=1, name="phi") - result = cutcells.ho_cut(mesh, ls, graph_enabled=False) - curved = result.curved_zero_nodes( - geometry_order=2, - node_family="gll", - small_entity_tol=2.0e-2, - ) - - dim = np.asarray(curved["dim"], dtype=np.int32) - stats_offsets = np.asarray(curved["stats_offsets"], dtype=np.int32) - node_failure_code = np.asarray(curved["node_failure_code"], dtype=np.uint8) - small_code = list(curved["failure_code_names"]).index("small_entity_kept_straight") - - face_states = np.flatnonzero(dim == 2) - self.assertGreater(len(face_states), 0) - for face_state in face_states: - begin = int(stats_offsets[face_state]) - end = int(stats_offsets[face_state + 1]) - self.assertFalse(np.all(node_failure_code[begin:end] == small_code)) - def test_cut_detects_quadratic_interior_tetra_intersection(self): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( diff --git a/python/tests/test_geometric_quantity_header.py b/python/tests/test_geometric_quantity_header.py deleted file mode 100644 index f5ec6eb..0000000 --- a/python/tests/test_geometric_quantity_header.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -import shlex -import shutil -import subprocess -import textwrap -from pathlib import Path - -import pytest - - -def test_geometric_quantity_header_primitives(tmp_path): - cxx = os.environ.get("CXX") - if cxx: - compiler = shlex.split(cxx) - else: - path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") - if path is None: - pytest.skip("C++ compiler not available") - compiler = [path] - - repo = Path(__file__).resolve().parents[2] - source = tmp_path / "geometric_quantity_check.cpp" - exe = tmp_path / "geometric_quantity_check" - source.write_text( - textwrap.dedent( - r""" - #include "geometric_quantity.h" - - #include - #include - #include - #include - - namespace geom = cutcells::geom; - namespace cell = cutcells::cell; - - bool close(double a, double b) - { - return std::fabs(a - b) < 1.0e-12; - } - - int main() - { - const auto tet = cell::type::tetrahedron; - - // 1. Projecting a 3D gradient into a face plane. - // Tetrahedron face 3 is the z = 0 face with normal (0, 0, 1). - std::array gradient = {1.0, 2.0, 3.0}; - const auto face_gradient = - geom::project_into_parent_face_tangent( - tet, 3, std::span(gradient)); - assert(!face_gradient.degenerate()); - assert(close(face_gradient.value[0], 1.0)); - assert(close(face_gradient.value[1], 2.0)); - assert(close(face_gradient.value[2], 0.0)); - - // 2. Computing an in-face normal to a segment. - std::array a = {0.0, 0.0, 0.0}; - std::array b = {1.0, 0.0, 0.0}; - std::array face_normal = {0.0, 0.0, 1.0}; - const auto in_face_normal = - geom::in_face_segment_normal( - std::span(a), - std::span(b), - std::span(face_normal)); - assert(!in_face_normal.degenerate()); - assert(close(in_face_normal.value[0], 0.0)); - assert(close(in_face_normal.value[1], 1.0)); - assert(close(in_face_normal.value[2], 0.0)); - - // 3. Projecting a direction onto a parent edge. - // Tetrahedron edge 5 is the x-axis edge from vertex 0 to 1. - std::array direction = {3.0, 4.0, 5.0}; - const auto edge_direction = - geom::project_onto_parent_edge( - tet, 5, std::span(direction)); - assert(!edge_direction.degenerate()); - assert(close(edge_direction.value[0], 3.0)); - assert(close(edge_direction.value[1], 0.0)); - assert(close(edge_direction.value[2], 0.0)); - - // 4. Detecting a degenerate projected direction. - std::array vertical = {0.0, 0.0, 7.0}; - const auto admissible = - geom::admissible_direction_in_parent_frame( - tet, - geom::ParentEntity{2, 3}, - std::span(vertical)); - assert(admissible.degenerate()); - assert(admissible.degeneracy == geom::Degeneracy::zero_projection); - - return 0; - } - """ - ) - ) - - compile_result = subprocess.run( - [ - *compiler, - "-std=c++20", - "-I", - str(repo / "cpp/src"), - str(source), - "-o", - str(exe), - ], - capture_output=True, - text=True, - ) - assert compile_result.returncode == 0, compile_result.stderr - - run_result = subprocess.run([str(exe)], capture_output=True, text=True) - assert run_result.returncode == 0, run_result.stderr diff --git a/python/tests/test_graph_criteria_header.py b/python/tests/test_graph_criteria_header.py deleted file mode 100644 index 54f5b06..0000000 --- a/python/tests/test_graph_criteria_header.py +++ /dev/null @@ -1,217 +0,0 @@ -import os -import shlex -import shutil -import subprocess -import textwrap -from pathlib import Path - -import pytest - - -def test_graph_criteria_header_reports(tmp_path): - cxx = os.environ.get("CXX") - if cxx: - compiler = shlex.split(cxx) - else: - path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") - if path is None: - pytest.skip("C++ compiler not available") - compiler = [path] - - repo = Path(__file__).resolve().parents[2] - source = tmp_path / "graph_criteria_check.cpp" - exe = tmp_path / "graph_criteria_check" - source.write_text( - textwrap.dedent( - r""" - #include "graph_criteria.h" - - #include - #include - #include - #include - - namespace cell = cutcells::cell; - namespace geom = cutcells::geom; - namespace graph = cutcells::graph_criteria; - - template - std::span sp(const std::array& a) - { - return std::span(a.data(), a.size()); - } - - int main() - { - const auto tet = cell::type::tetrahedron; - const geom::ParentEntity z_face{2, 3}; - - graph::Options opts; - opts.min_restricted_gradient_strength = 1.0e-12; - opts.min_transversality = 1.0e-6; - opts.max_relative_correction_distance = 1.0; - opts.max_relative_tangential_shift = 0.25; - - graph::HostFrame edge_host; - edge_host.dimension = graph::HostDimension::edge; - edge_host.normal = {1.0, 0.0, 0.0}; - edge_host.tangent = {0.0, 1.0, 0.0}; - edge_host.h = 1.0; - - // 1. Candidate direction tangent to the true zero set. - { - const std::array xh = {0.2, 0.2, 0.0}; - const std::array grad = {0.0, 1.0, 0.0}; - const std::array d = {1.0, 0.0, 0.0}; - const std::array xc = {0.25, 0.2, 0.0}; - const auto report = graph::evaluate_direction( - tet, z_face, edge_host, sp(xh), sp(grad), sp(d), sp(xc), - graph::DirectionKind::projected_level_set_gradient, opts); - assert(!report.accepted); - assert(report.failure_reason == graph::FailureReason::tangent_to_zero_set); - } - - // 2. Candidate direction too tangential to the straight host normal. - { - graph::Options drift_opts = opts; - drift_opts.min_host_normal_alignment = 0.0; - drift_opts.max_drift_amplification = 4.0; - const std::array xh = {0.2, 0.2, 0.0}; - const std::array grad = {1.0, 0.01, 0.0}; - const std::array d = {1.0, 0.01, 0.0}; - const std::array xc = {0.25, 0.2005, 0.0}; - graph::HostFrame host = edge_host; - host.normal = {0.0, 1.0, 0.0}; - host.tangent = {1.0, 0.0, 0.0}; - const auto report = graph::evaluate_direction( - tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), - graph::DirectionKind::projected_level_set_gradient, drift_opts); - assert(!report.accepted); - assert(report.failure_reason - == graph::FailureReason::excessive_drift_amplification); - } - - // 3. Root search segment leaves the admissible parent face. - { - const std::array xh = {0.9, 0.05, 0.0}; - const std::array grad = {1.0, 0.0, 0.0}; - const std::array d = {1.0, 0.0, 0.0}; - const std::array xc = {1.1, 0.05, 0.0}; - const auto report = graph::evaluate_direction( - tet, z_face, edge_host, sp(xh), sp(grad), sp(d), sp(xc), - graph::DirectionKind::projected_straight_host_normal, opts); - assert(!report.accepted); - assert(report.failure_reason - == graph::FailureReason::root_segment_leaves_parent_entity); - } - - // 4. Parent-face edge using the in-face segment normal is valid. - { - const std::array a = {0.25, 0.25, 0.0}; - const std::array b = {0.75, 0.25, 0.0}; - const auto face_normal = - geom::parent_face_normal(tet, 3, true); - const auto in_face_normal = - geom::in_face_segment_normal( - sp(a), sp(b), - std::span( - face_normal.value.data(), face_normal.value.size())); - const auto tangent = - geom::segment_tangent(sp(a), sp(b), true); - graph::HostFrame host; - host.dimension = graph::HostDimension::edge; - host.normal = in_face_normal.value; - host.tangent = tangent.value; - host.h = 0.5; - const std::array xh = {0.4, 0.25, 0.0}; - const std::array grad = {0.0, 1.0, 0.0}; - const std::array d = {0.0, 1.0, 0.0}; - const std::array xc = {0.4, 0.30, 0.0}; - const auto report = graph::evaluate_direction( - tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), - graph::DirectionKind::projected_straight_host_normal, opts); - assert(report.accepted); - assert(report.failure_reason == graph::FailureReason::none); - } - - // 5. Excessive tangential shift is rejected even when the root lies on the ray. - { - graph::Options shift_opts = opts; - shift_opts.max_drift_amplification = 10.0; - shift_opts.max_relative_tangential_shift = 0.1; - graph::HostFrame host = edge_host; - host.normal = {0.0, 1.0, 0.0}; - host.tangent = {1.0, 0.0, 0.0}; - const std::array xh = {0.1, 0.1, 0.0}; - const std::array grad = {0.8, 1.0, 0.0}; - const std::array d = {0.8, 1.0, 0.0}; - const std::array xc = {0.26, 0.3, 0.0}; - const auto report = graph::evaluate_direction( - tet, z_face, host, sp(xh), sp(grad), sp(d), sp(xc), - graph::DirectionKind::projected_level_set_gradient, shift_opts); - assert(!report.accepted); - assert(report.failure_reason - == graph::FailureReason::excessive_tangential_shift); - } - - // Preferred direction rule: accept the straight-host normal first. - { - graph::HostFrame host = edge_host; - host.normal = {0.0, 1.0, 0.0}; - host.tangent = {1.0, 0.0, 0.0}; - const std::array xh = {0.2, 0.2, 0.0}; - const std::array grad = {0.0, 1.0, 0.0}; - const std::array grad_d = {0.2, 1.0, 0.0}; - const std::array grad_root = {0.21, 0.25, 0.0}; - const std::array normal_d = {0.0, 1.0, 0.0}; - const std::array normal_root = {0.2, 0.25, 0.0}; - const auto selected = graph::select_preferred_direction( - tet, z_face, host, sp(xh), sp(grad), sp(grad_d), sp(grad_root), - sp(normal_d), sp(normal_root), opts); - assert(selected.accepted); - assert(selected.selected_kind - == graph::DirectionKind::projected_straight_host_normal); - } - - // Ordering and face quality helpers remain pure accept/reject checks. - { - const std::array host_pts = {0.0, 0.0, 0.5, 0.0, 1.0, 0.0}; - const std::array ok_pts = {0.0, 0.0, 0.5, 0.0, 1.0, 0.0}; - const std::array folded_pts = {0.0, 0.0, 0.6, 0.0, 0.55, 0.0}; - const std::array tangent = {1.0, 0.0}; - const auto ok = graph::evaluate_projected_edge_ordering( - sp(host_pts), sp(ok_pts), 2, sp(tangent), 1.0, opts); - const auto folded = graph::evaluate_projected_edge_ordering( - sp(host_pts), sp(folded_pts), 2, sp(tangent), 1.0, opts); - assert(ok.accepted); - assert(!folded.accepted); - assert(folded.failure_reason == graph::FailureReason::edge_ordering_fold); - - assert(graph::failure_reason_name( - graph::FailureReason::surface_jacobian_not_positive) - == "surface_jacobian_not_positive"); - } - - return 0; - } - """ - ) - ) - - compile_result = subprocess.run( - [ - *compiler, - "-std=c++20", - "-I", - str(repo / "cpp/src"), - str(source), - "-o", - str(exe), - ], - capture_output=True, - text=True, - ) - assert compile_result.returncode == 0, compile_result.stderr - - run_result = subprocess.run([str(exe)], capture_output=True, text=True) - assert run_result.returncode == 0, run_result.stderr diff --git a/python/tests/test_tangent_shift_header.py b/python/tests/test_tangent_shift_header.py deleted file mode 100644 index 211bd95..0000000 --- a/python/tests/test_tangent_shift_header.py +++ /dev/null @@ -1,207 +0,0 @@ -import os -import shlex -import shutil -import subprocess -import textwrap -from pathlib import Path - -import pytest - - -def test_tangent_shift_header_reports(tmp_path): - cxx = os.environ.get("CXX") - if cxx: - compiler = shlex.split(cxx) - else: - path = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") - if path is None: - pytest.skip("C++ compiler not available") - compiler = [path] - - repo = Path(__file__).resolve().parents[2] - source = tmp_path / "tangent_shift_check.cpp" - exe = tmp_path / "tangent_shift_check" - source.write_text( - textwrap.dedent( - r""" - #include "tangent_shift.h" - - #include - #include - #include - #include - #include - - namespace cell = cutcells::cell; - namespace geom = cutcells::geom; - namespace ts = cutcells::tangent_shift; - - bool close(double a, double b, double tol = 1.0e-8) - { - return std::fabs(a - b) <= tol; - } - - std::span sp(const std::vector& v) - { - return std::span(v.data(), v.size()); - } - - std::array circle_point(double fraction) - { - const double x = 0.2 + 0.6 * fraction; - const double dx = x - 0.5; - const double y = -0.5 + std::sqrt(0.36 - dx * dx); - return {x, y, 0.0}; - } - - std::vector make_points( - std::initializer_list> points) - { - std::vector out; - for (const auto& point : points) - out.insert(out.end(), point.begin(), point.end()); - return out; - } - - int main() - { - const auto tet = cell::type::tetrahedron; - const geom::ParentEntity z_face{2, 3}; - const auto a = circle_point(0.0); - const auto b = circle_point(1.0); - const auto midpoint = circle_point(0.5); - const auto drifted = circle_point(0.70); - - const std::vector provisional = make_points({a, b}); - const std::vector desired_mid = {0.0, 0.5, 1.0}; - - ts::Options opts; - opts.max_relative_arclength_drift = 0.02; - opts.max_absolute_arclength_drift = 1.0e-12; - opts.max_relative_final_arclength_error = 0.03; - opts.max_absolute_final_arclength_error = 1.0e-10; - opts.min_relative_spacing = 1.0e-8; - opts.phi_tolerance = 1.0e-10; - opts.max_correction_iterations = 24; - - auto circle_phi = [](std::span x) -> double - { - const double dx = x[0] - 0.5; - const double dy = x[1] + 0.5; - return dx * dx + dy * dy - 0.36; - }; - auto circle_grad = [](std::span x, - std::span g) - { - g[0] = 2.0 * (x[0] - 0.5); - g[1] = 2.0 * (x[1] + 0.5); - g[2] = 0.0; - }; - - // 1. Projected midpoint already has acceptable arclength drift. - { - const std::vector edge = make_points({a, midpoint, b}); - const auto report = - ts::correct_projected_edge_node( - tet, z_face, sp(provisional), sp(edge), 3, - sp(desired_mid), 1, circle_phi, circle_grad, opts); - assert(report.accepted); - assert(!report.corrected); - assert(report.failure_reason == ts::FailureReason::none); - assert(report.metrics.initial_arclength_error < 1.0e-12); - } - - // 2. Excessive midpoint drift is shifted and re-corrected to phi = 0. - { - const std::vector edge = make_points({a, drifted, b}); - const auto report = - ts::correct_projected_edge_node( - tet, z_face, sp(provisional), sp(edge), 3, - sp(desired_mid), 1, circle_phi, circle_grad, opts); - assert(report.accepted); - assert(report.corrected); - assert(close(report.corrected_node[0], midpoint[0])); - assert(close(report.corrected_node[1], midpoint[1])); - assert(report.metrics.phi_residual <= opts.phi_tolerance); - } - - // 3. If the correction would leave the admissible parent face, reject. - { - auto outside_phi = [](std::span x) -> double - { - return x[0] + x[1] - 1.2; - }; - auto outside_grad = [](std::span, - std::span g) - { - g[0] = 1.0; - g[1] = 1.0; - g[2] = 0.0; - }; - const std::vector edge = make_points({a, drifted, b}); - auto fail_opts = opts; - fail_opts.max_correction_iterations = 8; - const auto report = - ts::correct_projected_edge_node( - tet, z_face, sp(provisional), sp(edge), 3, - sp(desired_mid), 1, outside_phi, outside_grad, fail_opts); - assert(!report.accepted); - assert(report.request_refinement); - assert(report.failure_reason - == ts::FailureReason::correction_left_parent_entity); - } - - // 4. A correction that crosses the next edge node destroys ordering. - { - const auto before = circle_point(0.35); - const auto next = circle_point(0.45); - const std::vector edge = make_points({a, before, next, b}); - const std::vector desired = {0.0, 0.5, 0.75, 1.0}; - const auto report = - ts::correct_projected_edge_node( - tet, z_face, sp(provisional), sp(edge), 3, - sp(desired), 1, circle_phi, circle_grad, opts); - assert(!report.accepted); - assert(report.request_refinement); - assert(report.failure_reason - == ts::FailureReason::ordering_destroyed); - } - - // 5. If re-correction cannot satisfy phi = 0, reject. - { - const std::vector edge = make_points({a, drifted, b}); - auto no_iter = opts; - no_iter.max_correction_iterations = 0; - const auto report = - ts::correct_projected_edge_node( - tet, z_face, sp(provisional), sp(edge), 3, - sp(desired_mid), 1, circle_phi, circle_grad, no_iter); - assert(!report.accepted); - assert(report.request_refinement); - assert(report.failure_reason - == ts::FailureReason::phi_residual_too_large); - } - - return 0; - } - """ - ) - ) - - compile_result = subprocess.run( - [ - *compiler, - "-std=c++20", - "-I", - str(repo / "cpp/src"), - str(source), - "-o", - str(exe), - ], - capture_output=True, - text=True, - ) - assert compile_result.returncode == 0, compile_result.stderr - - run_result = subprocess.run([str(exe)], capture_output=True, text=True) - assert run_result.returncode == 0, run_result.stderr diff --git a/python/tests/test_tetra_triangulation_analysis.py b/python/tests/test_tetra_triangulation_analysis.py index c6d9e88..f22286d 100644 --- a/python/tests/test_tetra_triangulation_analysis.py +++ b/python/tests/test_tetra_triangulation_analysis.py @@ -1,9 +1,20 @@ import sys from pathlib import Path +import pytest + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from cutcells import CellType, analyze_all_cases, summarize_analysis +import cutcells + +pytestmark = pytest.mark.skipif( + not hasattr(cutcells, "analyze_all_cases"), + reason="triangulation_analysis helpers are not packaged", +) + +CellType = cutcells.CellType +analyze_all_cases = getattr(cutcells, "analyze_all_cases", None) +summarize_analysis = getattr(cutcells, "summarize_analysis", None) def test_tetra_parent_summary_excludes_zero_only_edges_from_interior_root_count(): From e392bf0b0a430e2af7c0ab57e5a10150e86d1574 Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 16 May 2026 11:24:45 +0200 Subject: [PATCH 22/23] changes to demo and test --- python/demo/demo_two_level_sets.py | 2 +- python/tests/test_certification_refinement.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/python/demo/demo_two_level_sets.py b/python/demo/demo_two_level_sets.py index cd85955..09a6aa8 100644 --- a/python/demo/demo_two_level_sets.py +++ b/python/demo/demo_two_level_sets.py @@ -30,7 +30,7 @@ DEFAULT_CENTER = np.array([0.1, -0.08, 0.05], dtype=np.float64) DEFAULT_RADIUS = 0.58 DEFAULT_PLANE_NORMAL = np.array([1.0, -0.35, 0.2], dtype=np.float64) -DEFAULT_PLANE_OFFSET = 0.07 +DEFAULT_PLANE_OFFSET = 0.0 TRIANGULATE = True OUTPUT_DIR = Path("demo_two_level_sets_output") diff --git a/python/tests/test_certification_refinement.py b/python/tests/test_certification_refinement.py index 4c537fd..0693702 100644 --- a/python/tests/test_certification_refinement.py +++ b/python/tests/test_certification_refinement.py @@ -500,6 +500,40 @@ def test_tetra_ready_to_cut_cell_is_replaced_without_tetra_triangulation(self): ]), ) + def test_zero_vertex_tetra_ready_to_cut_is_replaced_by_valid_tetrahedra(self): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - X[2], + degree=1, + ) + adapt = cutcells.make_adapt_cell(mesh, 0) + cutcells.build_edges(adapt) + ls_cell = cutcells.make_cell_level_set(ls, 0) + + cutcells.certify_refine_and_process_ready_cells(adapt, ls_cell, 0) + + self.assertEqual(adapt.num_cells(), 3) + np.testing.assert_array_equal( + np.asarray(adapt.cell_types), + np.full(3, cutcells.CellType.tetrahedron.value, dtype=np.int32), + ) + + tags = np.asarray(adapt.cell_cert_tags(0)) + self.assertEqual( + sorted(np.unique(tags).tolist()), + sorted([ + cutcells.CellCertTag.negative.value, + cutcells.CellCertTag.positive.value, + ]), + ) + + connectivity = np.asarray(adapt.cell_connectivity, dtype=np.int32) + offsets = np.asarray(adapt.cell_offsets, dtype=np.int32) + for c in range(adapt.num_cells()): + cell_vertices = connectivity[offsets[c]:offsets[c + 1]] + self.assertEqual(len(set(cell_vertices.tolist())), 4) + def test_certify_and_refine_recursively_splits_tetra_multiple_root_edges(self): mesh = _single_tetra_mesh() ls = cutcells.create_level_set( From 6d45fd35a5ce7c886390835de41dca2e880016eb Mon Sep 17 00:00:00 2001 From: Susanne Claus Date: Sat, 16 May 2026 20:57:51 +0200 Subject: [PATCH 23/23] include or --- cpp/src/ho_cut_mesh.cpp | 107 ++++++++--- cpp/src/ho_mesh_part_output.cpp | 65 ++++++- cpp/src/selection_expr.cpp | 188 ++++++++++++++----- cpp/src/selection_expr.h | 34 +++- python/demo/demo_two_level_sets.py | 1 + python/tests/test_ho_part_straight_output.py | 89 +++++++++ 6 files changed, 392 insertions(+), 92 deletions(-) diff --git a/cpp/src/ho_cut_mesh.cpp b/cpp/src/ho_cut_mesh.cpp index 59090b1..2dd46d2 100644 --- a/cpp/src/ho_cut_mesh.cpp +++ b/cpp/src/ho_cut_mesh.cpp @@ -156,7 +156,7 @@ template bool leaf_cell_matches_sign_requirements( const AdaptCell& ac, std::span cell_verts, - const SelectionExpr& expr, + const SelectionTerm& term, std::uint64_t cut_cell_active_mask, const BackgroundMeshData& bg, I parent_cell_id) @@ -165,8 +165,8 @@ bool leaf_cell_matches_sign_requirements( for (int li = 0; li < nls; ++li) { const std::uint64_t bit = std::uint64_t(1) << li; - const bool require_neg = (expr.negative_required & bit) != 0; - const bool require_pos = (expr.positive_required & bit) != 0; + const bool require_neg = (term.negative_required & bit) != 0; + const bool require_pos = (term.positive_required & bit) != 0; if (!require_neg && !require_pos) continue; @@ -208,12 +208,32 @@ bool leaf_cell_matches_sign_requirements( return true; } +template +bool leaf_cell_matches_selection_expr( + const AdaptCell& ac, + std::span cell_verts, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + for (const auto& term : expr.terms) + { + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, term, cut_cell_active_mask, bg, parent_cell_id)) + { + return true; + } + } + return false; +} + template bool zero_entity_matches( const AdaptCell& ac, int zero_entity_index, int target_dim, - const SelectionExpr& expr, + const SelectionTerm& term, std::uint64_t cut_cell_active_mask, const BackgroundMeshData& bg, I parent_cell_id) @@ -222,10 +242,13 @@ bool zero_entity_matches( return false; const auto zero_mask = ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; - if ((zero_mask & expr.zero_required) != expr.zero_required) + if ((cut_cell_active_mask & term.zero_required) != term.zero_required) + return false; + + if ((zero_mask & term.zero_required) != term.zero_required) return false; - if (expr.negative_required == 0 && expr.positive_required == 0) + if (term.negative_required == 0 && term.positive_required == 0) return true; std::vector zero_verts; @@ -252,7 +275,7 @@ bool zero_entity_matches( continue; if (leaf_cell_matches_sign_requirements( - ac, cell_verts, expr, cut_cell_active_mask, bg, parent_cell_id)) + ac, cell_verts, term, cut_cell_active_mask, bg, parent_cell_id)) { return true; } @@ -261,6 +284,28 @@ bool zero_entity_matches( return false; } +template +bool zero_entity_matches_selection_expr( + const AdaptCell& ac, + int zero_entity_index, + int target_dim, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + for (const auto& term : expr.terms) + { + if (zero_entity_matches( + ac, zero_entity_index, target_dim, term, + cut_cell_active_mask, bg, parent_cell_id)) + { + return true; + } + } + return false; +} + template void refresh_adapt_cell_semantics( AdaptCell& ac, @@ -557,26 +602,35 @@ HOMeshPart select_part(const HOCutCells& cut_cells, // For an uncut cell, every vertex has the same sign for each LS. // Check if the cell domain is compatible with the expression. - bool match = true; - for (const auto& clause : part.expr.clauses) + bool match = false; + for (const auto& term : part.expr.terms) { - const int li = clause.level_set_index; - const cell::domain dom = bg.domain(li, ci); + bool term_match = true; + for (const auto& clause : term.clauses) + { + const int li = clause.level_set_index; + const cell::domain dom = bg.domain(li, ci); - switch (clause.relation) + switch (clause.relation) + { + case Relation::LessThan: + if (dom != cell::domain::inside) term_match = false; + break; + case Relation::GreaterThan: + if (dom != cell::domain::outside) term_match = false; + break; + case Relation::EqualTo: + // An uncut (non-intersected) cell has no zero interface. + term_match = false; + break; + } + if (!term_match) break; + } + if (term_match) { - case Relation::LessThan: - if (dom != cell::domain::inside) match = false; - break; - case Relation::GreaterThan: - if (dom != cell::domain::outside) match = false; - break; - case Relation::EqualTo: - // An uncut (non-intersected) cell has no zero interface. - match = false; - break; + match = true; + break; } - if (!match) break; } if (match) @@ -600,7 +654,7 @@ HOMeshPart select_part(const HOCutCells& cut_cells, for (int c = 0; c < n_leaf; ++c) { auto cell_verts = ac.entity_to_vertex[tdim][static_cast(c)]; - if (leaf_cell_matches_sign_requirements( + if (leaf_cell_matches_selection_expr( ac, cell_verts, part.expr, cut_active_mask, bg, bg_cell)) { has_matching_leaf = true; @@ -613,14 +667,11 @@ HOMeshPart select_part(const HOCutCells& cut_cells, } else { - if ((cut_active_mask & part.expr.zero_required) != part.expr.zero_required) - continue; - bool has_matching_zero_entity = false; const int n_zero = ac.n_zero_entities(); for (int z = 0; z < n_zero; ++z) { - if (zero_entity_matches( + if (zero_entity_matches_selection_expr( ac, z, part.dim, part.expr, cut_active_mask, bg, bg_cell)) { has_matching_zero_entity = true; diff --git a/cpp/src/ho_mesh_part_output.cpp b/cpp/src/ho_mesh_part_output.cpp index 385ed2b..77a2780 100644 --- a/cpp/src/ho_mesh_part_output.cpp +++ b/cpp/src/ho_mesh_part_output.cpp @@ -116,7 +116,7 @@ template bool leaf_cell_matches_sign_requirements( const AdaptCell& ac, std::span cell_verts, - const SelectionExpr& expr, + const SelectionTerm& term, std::uint64_t cut_cell_active_mask, const BackgroundMeshData& bg, I parent_cell_id) @@ -125,8 +125,8 @@ bool leaf_cell_matches_sign_requirements( for (int li = 0; li < nls; ++li) { const std::uint64_t bit = std::uint64_t(1) << li; - const bool require_neg = (expr.negative_required & bit) != 0; - const bool require_pos = (expr.positive_required & bit) != 0; + const bool require_neg = (term.negative_required & bit) != 0; + const bool require_pos = (term.positive_required & bit) != 0; if (!require_neg && !require_pos) continue; @@ -163,12 +163,32 @@ bool leaf_cell_matches_sign_requirements( return true; } +template +bool leaf_cell_matches_selection_expr( + const AdaptCell& ac, + std::span cell_verts, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + for (const auto& term : expr.terms) + { + if (leaf_cell_matches_sign_requirements( + ac, cell_verts, term, cut_cell_active_mask, bg, parent_cell_id)) + { + return true; + } + } + return false; +} + template bool zero_entity_matches( const AdaptCell& ac, int zero_entity_index, int target_dim, - const SelectionExpr& expr, + const SelectionTerm& term, std::uint64_t cut_cell_active_mask, const BackgroundMeshData& bg, I parent_cell_id) @@ -181,10 +201,13 @@ bool zero_entity_matches( const auto zero_mask = ac.zero_entity_zero_mask[static_cast(zero_entity_index)]; - if ((zero_mask & expr.zero_required) != expr.zero_required) + if ((cut_cell_active_mask & term.zero_required) != term.zero_required) return false; - if (expr.negative_required == 0 && expr.positive_required == 0) + if ((zero_mask & term.zero_required) != term.zero_required) + return false; + + if (term.negative_required == 0 && term.positive_required == 0) return true; std::vector zero_verts; @@ -215,7 +238,7 @@ bool zero_entity_matches( } if (leaf_cell_matches_sign_requirements( - ac, cell_verts, expr, cut_cell_active_mask, bg, parent_cell_id)) + ac, cell_verts, term, cut_cell_active_mask, bg, parent_cell_id)) { return true; } @@ -224,6 +247,28 @@ bool zero_entity_matches( return false; } +template +bool zero_entity_matches_selection_expr( + const AdaptCell& ac, + int zero_entity_index, + int target_dim, + const SelectionExpr& expr, + std::uint64_t cut_cell_active_mask, + const BackgroundMeshData& bg, + I parent_cell_id) +{ + for (const auto& term : expr.terms) + { + if (zero_entity_matches( + ac, zero_entity_index, target_dim, term, + cut_cell_active_mask, bg, parent_cell_id)) + { + return true; + } + } + return false; +} + struct SelectedEntity { int local_zero_entity_id = -1; @@ -250,7 +295,7 @@ std::vector selected_entities(const HOMeshPart& part, { auto verts = adapt_cell.entity_to_vertex[adapt_cell.tdim][ static_cast(c)]; - if (!leaf_cell_matches_sign_requirements( + if (!leaf_cell_matches_selection_expr( adapt_cell, verts, part.expr, cut_active_mask, *part.bg, parent_cell_id)) { @@ -270,7 +315,7 @@ std::vector selected_entities(const HOMeshPart& part, entities.reserve(static_cast(n_zero)); for (int z = 0; z < n_zero; ++z) { - if (!zero_entity_matches( + if (!zero_entity_matches_selection_expr( adapt_cell, z, part.dim, part.expr, cut_active_mask, *part.bg, parent_cell_id)) { @@ -683,7 +728,7 @@ std::vector selected_zero_entity_infos( const int n_zero = ac.n_zero_entities(); for (int z = 0; z < n_zero; ++z) { - if (!zero_entity_matches( + if (!zero_entity_matches_selection_expr( ac, z, part.dim, part.expr, cut_active_mask, *part.bg, parent_cell_id)) { diff --git a/cpp/src/selection_expr.cpp b/cpp/src/selection_expr.cpp index ebd0bab..b6dc2eb 100644 --- a/cpp/src/selection_expr.cpp +++ b/cpp/src/selection_expr.cpp @@ -83,18 +83,34 @@ Clause parse_clause(std::string_view text) return c; } -/// Case-insensitive check for " and " delimiter between clauses. -/// Returns the position of the 'a' in " and ", or npos. -std::size_t find_and_delimiter(std::string_view text, std::size_t start = 0) +/// Case-insensitive check for a space-delimited keyword between clauses/terms. +/// Returns the position of the leading whitespace before the keyword, or npos. +std::size_t find_keyword_delimiter(std::string_view text, + std::string_view keyword, + std::size_t start = 0) { - // Search for " and " (space-delimited, case-insensitive) - for (std::size_t i = start; i + 4 < text.size(); ++i) + for (std::size_t i = start; i + keyword.size() + 1 < text.size(); ++i) { - if (std::isspace(static_cast(text[i])) - && (text[i + 1] == 'a' || text[i + 1] == 'A') - && (text[i + 2] == 'n' || text[i + 2] == 'N') - && (text[i + 3] == 'd' || text[i + 3] == 'D') - && std::isspace(static_cast(text[i + 4]))) + if (!std::isspace(static_cast(text[i]))) + continue; + + bool match = true; + for (std::size_t k = 0; k < keyword.size(); ++k) + { + const char a = static_cast( + std::tolower(static_cast(text[i + 1 + k]))); + const char b = static_cast( + std::tolower(static_cast(keyword[k]))); + if (a != b) + { + match = false; + break; + } + } + + if (match + && std::isspace(static_cast( + text[i + 1 + keyword.size()]))) { return i; } @@ -102,6 +118,54 @@ std::size_t find_and_delimiter(std::string_view text, std::size_t start = 0) return std::string_view::npos; } +SelectionTerm parse_term(std::string_view text) +{ + text = trim(text); + if (text.empty()) + throw std::runtime_error("parse_selection_expr: empty term"); + + SelectionTerm term; + std::size_t pos = 0; + while (pos < text.size()) + { + const std::size_t and_pos = find_keyword_delimiter(text, "and", pos); + std::string_view clause_text; + if (and_pos == std::string_view::npos) + { + clause_text = text.substr(pos); + pos = text.size(); + } + else + { + clause_text = text.substr(pos, and_pos - pos); + pos = and_pos + 5; // skip " and " + } + + term.clauses.push_back(parse_clause(clause_text)); + } + + if (term.clauses.empty()) + throw std::runtime_error("parse_selection_expr: no clauses found in term"); + return term; +} + +void mirror_first_term(SelectionExpr& expr) +{ + if (expr.terms.empty()) + { + expr.clauses.clear(); + expr.zero_required = 0; + expr.negative_required = 0; + expr.positive_required = 0; + return; + } + + expr.clauses = expr.terms.front().clauses; + expr.zero_required = expr.terms.front().zero_required; + expr.negative_required = expr.terms.front().negative_required; + expr.positive_required = expr.terms.front().positive_required; +} + } // anonymous namespace // --------------------------------------------------------------------------- @@ -119,24 +183,26 @@ SelectionExpr parse_selection_expr(std::string_view text) std::size_t pos = 0; while (pos < text.size()) { - std::size_t and_pos = find_and_delimiter(text, pos); - std::string_view clause_text; - if (and_pos == std::string_view::npos) + const std::size_t or_pos = find_keyword_delimiter(text, "or", pos); + std::string_view term_text; + if (or_pos == std::string_view::npos) { - clause_text = text.substr(pos); + term_text = text.substr(pos); pos = text.size(); } else { - clause_text = text.substr(pos, and_pos - pos); - pos = and_pos + 5; // skip " and " + term_text = text.substr(pos, or_pos - pos); + pos = or_pos + 4; // skip " or " } - expr.clauses.push_back(parse_clause(clause_text)); + expr.terms.push_back(parse_term(term_text)); } - if (expr.clauses.empty()) - throw std::runtime_error("parse_selection_expr: no clauses found"); + if (expr.terms.empty()) + throw std::runtime_error("parse_selection_expr: no terms found"); + + mirror_first_term(expr); return expr; } @@ -148,36 +214,47 @@ SelectionExpr parse_selection_expr(std::string_view text) void compile_selection_expr(SelectionExpr& expr, const std::vector& level_set_names) { - expr.zero_required = 0; - expr.negative_required = 0; - expr.positive_required = 0; - - for (auto& clause : expr.clauses) + for (auto& term : expr.terms) { - auto it = std::find(level_set_names.begin(), level_set_names.end(), - clause.name); - if (it == level_set_names.end()) + term.zero_required = 0; + term.negative_required = 0; + term.positive_required = 0; + + for (auto& clause : term.clauses) { - throw std::runtime_error( - "compile_selection_expr: unknown level-set name '" - + clause.name + "'"); - } + auto it = std::find(level_set_names.begin(), level_set_names.end(), + clause.name); + if (it == level_set_names.end()) + { + throw std::runtime_error( + "compile_selection_expr: unknown level-set name '" + + clause.name + "'"); + } - int idx = static_cast(std::distance(level_set_names.begin(), it)); - if (idx >= 64) - throw std::runtime_error( - "compile_selection_expr: level-set index >= 64 not supported"); + int idx = static_cast(std::distance(level_set_names.begin(), it)); + if (idx >= 64) + throw std::runtime_error( + "compile_selection_expr: level-set index >= 64 not supported"); - clause.level_set_index = idx; - const std::uint64_t bit = std::uint64_t(1) << idx; + clause.level_set_index = idx; + const std::uint64_t bit = std::uint64_t(1) << idx; - switch (clause.relation) - { - case Relation::LessThan: expr.negative_required |= bit; break; - case Relation::GreaterThan: expr.positive_required |= bit; break; - case Relation::EqualTo: expr.zero_required |= bit; break; + switch (clause.relation) + { + case Relation::LessThan: + term.negative_required |= bit; + break; + case Relation::GreaterThan: + term.positive_required |= bit; + break; + case Relation::EqualTo: + term.zero_required |= bit; + break; + } } } + + mirror_first_term(expr); } // --------------------------------------------------------------------------- @@ -186,11 +263,28 @@ void compile_selection_expr(SelectionExpr& expr, int infer_selection_dim(const SelectionExpr& expr, int tdim) { - int n_eq = 0; - for (const auto& clause : expr.clauses) - if (clause.relation == Relation::EqualTo) - ++n_eq; - return std::max(0, tdim - n_eq); + if (expr.terms.empty()) + throw std::runtime_error("infer_selection_dim: empty selection expression"); + + auto infer_term_dim = [tdim](const SelectionTerm& term) + { + int n_eq = 0; + for (const auto& clause : term.clauses) + if (clause.relation == Relation::EqualTo) + ++n_eq; + return std::max(0, tdim - n_eq); + }; + + const int dim = infer_term_dim(expr.terms.front()); + for (std::size_t i = 1; i < expr.terms.size(); ++i) + { + if (infer_term_dim(expr.terms[i]) != dim) + { + throw std::runtime_error( + "infer_selection_dim: 'or' terms must select the same entity dimension"); + } + } + return dim; } } // namespace cutcells diff --git a/cpp/src/selection_expr.h b/cpp/src/selection_expr.h index ab4a06b..fc4f966 100644 --- a/cpp/src/selection_expr.h +++ b/cpp/src/selection_expr.h @@ -29,16 +29,16 @@ struct Clause Relation relation; ///< the comparison operator }; -/// A compiled selection expression. +/// One conjunction term in a selection expression. /// -/// The expression is a conjunction (AND) of clauses: -/// "phi1 < 0 and phi2 = 0" → two clauses. +/// A term is an AND of clauses: +/// "phi1 < 0 and phi2 = 0" -> two clauses in one term. /// /// After compilation against a name registry, bitmasks are populated: /// - bit i set in zero_required → φ_i = 0 /// - bit i set in negative_required → φ_i < 0 /// - bit i set in positive_required → φ_i > 0 -struct SelectionExpr +struct SelectionTerm { std::vector clauses; ///< implicitly AND-connected @@ -48,9 +48,28 @@ struct SelectionExpr std::uint64_t positive_required = 0; }; +/// A compiled selection expression. +/// +/// The expression is a union (OR) of terms: +/// "phi1 < 0 or phi2 < 0" -> two terms. +/// "phi1 < 0 and phi2 > 0" -> one term with two clauses. +struct SelectionExpr +{ + std::vector terms; + + // Legacy single-term view. These fields mirror terms.front() after + // parsing/compilation so existing single-term code can be migrated + // incrementally. New selection code should use terms. + std::vector clauses; + std::uint64_t zero_required = 0; + std::uint64_t negative_required = 0; + std::uint64_t positive_required = 0; +}; + /// Parse a selection expression string. /// -/// Grammar: clause ('and' clause)* +/// Grammar: term ('or' term)* +/// term: clause ('and' clause)* /// clause: name ('<'|'>'|'=') '0' /// /// @throws std::runtime_error on syntax error. @@ -67,8 +86,9 @@ void compile_selection_expr(SelectionExpr& expr, /// Infer the entity dimension selected by a compiled expression. /// -/// If no clause is EqualTo → volume (tdim). -/// Each EqualTo clause reduces the dimension by one. +/// Each OR term must select the same dimension. Within a term, each EqualTo +/// clause reduces the dimension by one; a term with no EqualTo clauses selects +/// volume (tdim). /// /// @param tdim topological dimension of the background cells. /// @return inferred entity dimension. diff --git a/python/demo/demo_two_level_sets.py b/python/demo/demo_two_level_sets.py index 09a6aa8..043ec02 100644 --- a/python/demo/demo_two_level_sets.py +++ b/python/demo/demo_two_level_sets.py @@ -127,6 +127,7 @@ def main() -> None: ("sphere = 0 and plane = 0", "sphere_plane_intersection_curve.vtu", "cut_only"), ("sphere < 0 and plane < 0", "inside_sphere_plane_negative.vtu", "cut_only"), ("sphere < 0 and plane > 0", "inside_sphere_plane_positive.vtu", "cut_only"), + ("sphere < 0 or plane < 0", "inside_sphere_or_plane_negative.vtu", "cut_only"), ] print("Writing selected mesh parts ...") diff --git a/python/tests/test_ho_part_straight_output.py b/python/tests/test_ho_part_straight_output.py index 3214828..ea6e5d3 100644 --- a/python/tests/test_ho_part_straight_output.py +++ b/python/tests/test_ho_part_straight_output.py @@ -1,6 +1,7 @@ from pathlib import Path import numpy as np +import pytest import cutcells @@ -36,6 +37,26 @@ def _single_triangle_mesh(): return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=2) +def _two_tetra_mesh(): + coords = np.array( + [ + [0.0, 0.0, 0.0], + [0.2, 0.0, 0.0], + [0.0, 0.2, 0.0], + [0.0, 0.0, 0.2], + [1.0, 0.0, 0.0], + [1.2, 0.0, 0.0], + [1.0, 0.2, 0.0], + [1.0, 0.0, 0.2], + ], + dtype=np.float64, + ) + connectivity = np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.int32) + offsets = np.array([0, 4, 8], dtype=np.int32) + cell_types = np.array([10, 10], dtype=np.int32) + return cutcells.MeshView(coords, connectivity, offsets, cell_types, tdim=3) + + def _edge_key(xa: np.ndarray, xb: np.ndarray, digits: int = 12): pa = tuple(np.round(np.asarray(xa, dtype=np.float64), digits)) pb = tuple(np.round(np.asarray(xb, dtype=np.float64), digits)) @@ -186,6 +207,74 @@ def test_homeshpart_straight_output_bridge(tmp_path: Path): assert "Name=\"types\" format=\"ascii\">9 " in interface_path.read_text() +def test_mesh_part_string_selection_supports_volume_or(): + mesh = _two_tetra_mesh() + left = cutcells.create_level_set( + mesh, + lambda X: X[0] - 0.5, + degree=1, + name="left", + ) + right = cutcells.create_level_set( + mesh, + lambda X: 0.5 - X[0], + degree=1, + name="right", + ) + + result = cutcells.cut(mesh, [left, right]) + + left_part = result["left < 0"] + right_part = result["right < 0"] + union_part = result["left < 0 or right < 0"] + + np.testing.assert_array_equal(np.asarray(left_part.uncut_cell_ids), np.array([0])) + np.testing.assert_array_equal(np.asarray(right_part.uncut_cell_ids), np.array([1])) + np.testing.assert_array_equal( + np.asarray(union_part.uncut_cell_ids), + np.array([0, 1]), + ) + assert union_part.num_cut_cells == 0 + + +def test_mesh_part_string_selection_rejects_mixed_dimension_or(): + mesh = _single_tetra_mesh() + ls = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.6, + degree=1, + name="phi", + ) + + result = cutcells.cut(mesh, ls) + with pytest.raises(RuntimeError, match="same entity dimension"): + result["phi < 0 or phi = 0"] + + +def test_mesh_part_string_selection_supports_surface_or(): + mesh = _single_tetra_mesh() + phi = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[1] - 0.6, + degree=1, + name="phi", + ) + psi = cutcells.create_level_set( + mesh, + lambda X: X[0] + X[2] - 0.6, + degree=1, + name="psi", + ) + + result = cutcells.cut(mesh, [phi, psi]) + union_part = result["phi = 0 or psi = 0"] + union_mesh = union_part.visualization_mesh(mode="cut_only") + + assert union_part.dim == 2 + np.testing.assert_array_equal(np.asarray(union_part.cut_cell_ids), np.array([0])) + assert len(np.asarray(union_mesh.types)) > 0 + + def test_triangulated_tetra_prism_midpoints_keep_masks_and_source_edges(): mesh = _single_tetra_mesh() ls = cutcells.create_level_set(