diff --git a/Code/CMake/SimVascularExternals.cmake b/Code/CMake/SimVascularExternals.cmake index 50772c5b9..880882e3e 100644 --- a/Code/CMake/SimVascularExternals.cmake +++ b/Code/CMake/SimVascularExternals.cmake @@ -9,8 +9,10 @@ if(DOXYGEN_FOUND) configure_file(${SV_SOURCE_DIR}/../Documentation/Doxyfile ${SV_BINARY_DIR}/Doxyfile @ONLY) add_custom_target(doc - ${DOXYGEN_EXECUTABLE} ${SV_BINARY_DIR}/Doxyfile - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMAND "${CMAKE_COMMAND}" -E remove_directory + "${SV_SOURCE_DIR}/../Documentation/build" + COMMAND "${DOXYGEN_EXECUTABLE}" "${SV_BINARY_DIR}/Doxyfile" + WORKING_DIRECTORY "${SV_SOURCE_DIR}/.." COMMENT "Generating API documentation with Doxygen" VERBATIM ) endif(DOXYGEN_FOUND) diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index c5ab81146..30baa33b3 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -266,11 +266,17 @@ file(GLOB SOLVER_FE_MATH_SRCS CONFIGURE_DEPENDS FE/Math/*.h ) +file(GLOB SOLVER_FE_QUADRATURE_SRCS CONFIGURE_DEPENDS + FE/Quadrature/*.cpp + FE/Quadrature/*.h +) + list(APPEND CSRCS ${SOLVER_CORE_SRCS} ${SOLVER_FE_COMMON_SRCS} ${SOLVER_FE_BASIS_SRCS} ${SOLVER_FE_MATH_SRCS} + ${SOLVER_FE_QUADRATURE_SRCS} ) # Set PETSc interace code. diff --git a/Code/Source/solver/FE/Common/Types.h b/Code/Source/solver/FE/Common/Types.h index f443d0225..74669f010 100644 --- a/Code/Source/solver/FE/Common/Types.h +++ b/Code/Source/solver/FE/Common/Types.h @@ -69,10 +69,9 @@ enum class CellFamily { * @brief Shared vocabulary types, constants, and exception infrastructure used by every FE module. * * @details The Common module collects the foundational definitions that the - * rest of the FE library builds on: index and scalar type aliases; element, - * basis, quadrature, and field enumerations; sentinel constants and strong - * type wrappers; and the FE exception hierarchy together with its - * argument-checking helpers. + * rest of the FE library builds on: index and scalar type aliases; shared + * enumerations and strong types; sentinel constants; and the FE exception + * hierarchy together with its argument-checking helpers. */ namespace svmp::FE { @@ -83,10 +82,10 @@ namespace svmp::FE { * @brief Core type aliases, enumerations, constants, geometric types, and compile-time traits. * * @details This group documents the index and identifier types used for - * element-local and global numbering, the element/basis/quadrature/field - * enumerations shared across modules, sentinel constants, reference- and - * physical-space geometric aliases, and the strong-type utilities that - * prevent accidental mixing of conceptually distinct values. + * element-local and global numbering, the enumerations shared across modules, + * sentinel constants, reference- and physical-space geometric aliases, and + * the strong-type utilities that prevent accidental mixing of conceptually + * distinct values. * @{ */ @@ -242,19 +241,6 @@ enum class ElementType : std::uint8_t { Unknown ///< Unrecognized or uninitialized element type }; -/** - * @brief Quadrature rule types - */ -enum class QuadratureType : std::uint8_t { - GaussLegendre, ///< Standard Gaussian quadrature - GaussLobatto, ///< Includes endpoints (for spectral elements) - Newton, ///< Newton-Cotes rules - Reduced, ///< Order-based reduced integration for locking - PositionBased, ///< Position-based reduced integration (legacy compatible) - Composite, ///< Composite rules for adaptivity - Custom ///< User-defined quadrature points -}; - /** * @brief Basis function families */ diff --git a/Code/Source/solver/FE/FE.h b/Code/Source/solver/FE/FE.h index 125660942..c51c4a787 100644 --- a/Code/Source/solver/FE/FE.h +++ b/Code/Source/solver/FE/FE.h @@ -11,7 +11,8 @@ * This header intentionally contains no declarations. It gives Doxygen a * header-based home for the top-level FE group; submodule groups attach to it * from their own headers, including FE_Basis (Basis/BasisFunction.h), - * FE_Common (Common/Types.h), and FE_Math (Math/Vector.h). + * FE_Common (Common/Types.h), FE_Math (Math/Vector.h), and FE_Quadrature + * (Quadrature/QuadratureRule.h). */ /** diff --git a/Code/Source/solver/FE/Quadrature/QuadratureRule.cpp b/Code/Source/solver/FE/Quadrature/QuadratureRule.cpp new file mode 100644 index 000000000..b33853a56 --- /dev/null +++ b/Code/Source/solver/FE/Quadrature/QuadratureRule.cpp @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +/** + * @file QuadratureRule.cpp + * @brief Internal construction and structural validation for quadrature rules. + * @ingroup FE_Quadrature + */ + +#include "FE/Quadrature/QuadratureRule.h" + +#include "FE/Common/FEException.h" + +#include +#include +#include +#include + +namespace svmp::FE::quadrature { +namespace { + +constexpr int reference_dimension(svmp::CellFamily family) noexcept +{ + switch (family) { + case svmp::CellFamily::Point: + return 0; + case svmp::CellFamily::Line: + return 1; + case svmp::CellFamily::Triangle: + case svmp::CellFamily::Quad: + return 2; + case svmp::CellFamily::Tetra: + case svmp::CellFamily::Hex: + case svmp::CellFamily::Wedge: + return 3; + default: + return -1; + } +} + +constexpr double reference_measure(svmp::CellFamily family) noexcept +{ + switch (family) { + case svmp::CellFamily::Point: + return 1.0; + case svmp::CellFamily::Line: + return 2.0; + case svmp::CellFamily::Triangle: + return 0.5; + case svmp::CellFamily::Quad: + return 4.0; + case svmp::CellFamily::Tetra: + return 1.0 / 6.0; + case svmp::CellFamily::Hex: + return 8.0; + case svmp::CellFamily::Wedge: + return 1.0; + default: + return -1.0; + } +} + +void validate_point( + const QuadPoint& point, + int dimension, + std::size_t point_index) +{ + for (std::size_t component = 0; component < 3u; ++component) { + if (!std::isfinite(point[component])) { + svmp::raise( + std::string{ + "QuadratureRule: quadrature point contains a non-finite " + "coordinate at point index "} + + std::to_string(point_index)); + } + if (component >= + static_cast(dimension) && + point[component] != 0.0) { + svmp::raise( + std::string{ + "QuadratureRule: quadrature point has a nonzero inactive " + "coordinate at point index "} + + std::to_string(point_index)); + } + } +} + +void validate_weights(const std::vector& weights) +{ + for (std::size_t point_index = 0; + point_index < weights.size(); + ++point_index) { + if (!std::isfinite(weights[point_index])) { + svmp::raise( + std::string{ + "QuadratureRule: quadrature weight must be finite at point " + "index "} + + std::to_string(point_index)); + } + } +} + +} // namespace + +QuadratureRule::~QuadratureRule() = default; + +int QuadratureRule::dimension() const noexcept +{ + const int dimension = reference_dimension(cell_family_); + assert(dimension >= 0); + return dimension; +} + +double QuadratureRule::reference_cell_measure() const noexcept +{ + const double measure = reference_measure(cell_family_); + assert(measure > 0.0); + return measure; +} + +QuadratureRule::QuadratureRule( + svmp::CellFamily family, + int polynomial_exactness, + std::vector points, + std::vector weights) + : cell_family_(family), + polynomial_exactness_(polynomial_exactness), + points_(std::move(points)), + weights_(std::move(weights)) +{ + const int dimension = reference_dimension(cell_family_); + svmp::check( + dimension >= 0, + "QuadratureRule: unsupported reference-cell family"); + + svmp::check( + polynomial_exactness_ >= 0, + "QuadratureRule: polynomial exactness must be non-negative"); + svmp::check( + !points_.empty(), + "QuadratureRule: a rule must contain at least one point"); + svmp::check( + points_.size() == weights_.size(), + "QuadratureRule: points/weights size mismatch"); + + for (std::size_t point_index = 0; + point_index < points_.size(); + ++point_index) { + validate_point( + points_[point_index], + dimension, + point_index); + } + + validate_weights(weights_); +} + +} // namespace svmp::FE::quadrature diff --git a/Code/Source/solver/FE/Quadrature/QuadratureRule.h b/Code/Source/solver/FE/Quadrature/QuadratureRule.h new file mode 100644 index 000000000..0f5ace65e --- /dev/null +++ b/Code/Source/solver/FE/Quadrature/QuadratureRule.h @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_QUADRATURE_RULE_H +#define SVMP_FE_QUADRATURE_RULE_H + +/** + * @file QuadratureRule.h + * @brief Abstract base for reference-space quadrature rules. + * @ingroup FE_Quadrature + */ + +/** + * @defgroup FE_Quadrature Quadrature + * @ingroup FE + * @brief Integration rules on canonical reference cells. + * + * @details + * A QuadratureRule owns ordered reference coordinates and weights for + * @f[ + * \int_{\hat K} f(\hat x)\,d\hat x + * \approx \sum_q w_q f(\hat x_q). + * @f] + * Supported families use these canonical reference cells: + * + * | Family | Reference cell | Measure | + * |----------|-----------------------------------------------------|---------| + * | Point | @f$(0,0,0)@f$ | @f$1@f$ | + * | Line | @f$[-1,1]@f$ | @f$2@f$ | + * | Triangle | @f$\{(x,y):x,y\geq0,\ x+y\leq1\}@f$ | @f$1/2@f$ | + * | Quad | @f$[-1,1]^2@f$ | @f$4@f$ | + * | Tetra | @f$\{(x,y,z):x,y,z\geq0,\ x+y+z\leq1\}@f$ | @f$1/6@f$ | + * | Hex | @f$[-1,1]^3@f$ | @f$8@f$ | + * | Wedge | unit triangle @f$\times[-1,1]@f$ | @f$1@f$ | + * + * The family therefore determines both reference dimension and cell measure. + * Quadrature points are not required to lie inside the reference cell. + * Generating code is responsible for verifying weight normalization and + * declared polynomial exactness through analytic moment tests. + */ + +#include "FE/Common/Types.h" +#include "FE/Math/Vector.h" + +#include +#include + +namespace svmp::FE::quadrature { + +/** @addtogroup FE_Quadrature + * @{ + */ + +/** + * @brief Three-component coordinate used for every reference quadrature point. + * + * Only the first QuadratureRule::dimension() components are active; remaining + * components must be zero. + */ +using QuadPoint = math::Vector; + +/** + * @brief Owning abstract base for quadrature rules on canonical reference cells. + * + * Construction requires: + * + * - a supported cell family and non-negative polynomial exactness; + * - at least one point and the same number of points and weights; + * - finite coordinates and weights; and + * - inactive coordinates equal to zero. + * + * Points may be duplicate or outside the reference cell, and weights may be + * zero or negative. Construction does not verify weight normalization or + * polynomial exactness. + */ +class QuadratureRule { +public: + /** @brief Enable polymorphic destruction of concrete quadrature rules. */ + virtual ~QuadratureRule() = 0; + + /** @brief Return the number of point/weight pairs. */ + std::size_t num_points() const noexcept { return points_.size(); } + + /** @brief Return the declared total-degree polynomial exactness. */ + int polynomial_exactness() const noexcept { return polynomial_exactness_; } + + /** + * @brief Return the reference dimension and active QuadPoint component count. + */ + int dimension() const noexcept; + + /** @brief Return the canonical reference-cell family. */ + svmp::CellFamily cell_family() const noexcept { return cell_family_; } + + /** + * @brief Return point @p i without bounds checking. + * @pre @p i is less than num_points(). + */ + const QuadPoint& point(std::size_t i) const noexcept { return points_[i]; } + + /** + * @brief Return the weight paired with point @p i without bounds checking. + * @pre @p i is less than num_points(). + */ + double weight(std::size_t i) const noexcept { return weights_[i]; } + + /** @brief Return all points in integration order. */ + const std::vector& points() const noexcept { return points_; } + + /** @brief Return all weights in point order. */ + const std::vector& weights() const noexcept { return weights_; } + + /** @brief Return the reference-cell measure derived from cell_family(). */ + double reference_cell_measure() const noexcept; + +protected: + /** + * @brief Initialize a concrete rule from complete point and weight data. + * @param family Reference-cell family; also determines dimension and measure. + * @param polynomial_exactness Declared total-degree polynomial exactness. + * @param points Ordered reference coordinates. + * @param weights Weights paired with @p points. + * @throws InvalidArgumentException If a construction requirement is violated. + */ + explicit QuadratureRule( + svmp::CellFamily family, + int polynomial_exactness, + std::vector points, + std::vector weights); + + QuadratureRule(const QuadratureRule&) = default; + QuadratureRule(QuadratureRule&&) noexcept = default; + QuadratureRule& operator=(const QuadratureRule&) = default; + QuadratureRule& operator=(QuadratureRule&&) noexcept = default; + +private: + svmp::CellFamily cell_family_; ///< Canonical reference topology. + int polynomial_exactness_; ///< Exactness declared by the generator. + std::vector points_; ///< Ordered reference coordinates. + std::vector weights_; ///< Weights paired with points_. +}; + +/** @} */ + +} // namespace svmp::FE::quadrature + +#endif // SVMP_FE_QUADRATURE_RULE_H diff --git a/tests/unitTests/FE/Quadrature/test_QuadratureRules.cpp b/tests/unitTests/FE/Quadrature/test_QuadratureRules.cpp new file mode 100644 index 000000000..6e2f57dcd --- /dev/null +++ b/tests/unitTests/FE/Quadrature/test_QuadratureRules.cpp @@ -0,0 +1,296 @@ +/** + * @file test_QuadratureRules.cpp + * @brief Unit tests for the core quadrature rule infrastructure. + */ + +#include + +#include "FE/Common/FEException.h" +#include "FE/Quadrature/QuadratureRule.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::quadrature; + +namespace { + +using ExpectedPoint = std::array; + +class TestQuadratureRule final : public QuadratureRule { +public: + TestQuadratureRule( + svmp::CellFamily family, + int polynomial_exactness, + std::vector points, + std::vector weights) + : QuadratureRule( + family, + polynomial_exactness, + std::move(points), + std::move(weights)) + { + } +}; + +template +void expect_invalid_argument_with_message( + Function&& function, + const std::string& expected_message) +{ + try { + std::forward(function)(); + FAIL() << "Expected InvalidArgumentException containing: " + << expected_message; + } catch (const InvalidArgumentException& exception) { + const std::string actual_message = exception.what(); + EXPECT_NE(actual_message.find(expected_message), std::string::npos) + << "actual message: " << actual_message; + } catch (const std::exception& exception) { + FAIL() << "Expected InvalidArgumentException, received: " + << exception.what(); + } catch (...) { + FAIL() << "Expected InvalidArgumentException, received an unknown exception"; + } +} + +} // namespace + +TEST(QuadPointContract, UsesFixedSizeFEVectorRepresentation) +{ + static_assert(std::is_same_v>); + static_assert(QuadPoint::RowsAtCompileTime == 3); + static_assert(QuadPoint::ColsAtCompileTime == 1); + + const QuadPoint origin = QuadPoint::Zero(); + for (std::size_t component = 0; component < 3u; ++component) { + EXPECT_DOUBLE_EQ(origin[component], 0.0); + } + + const QuadPoint line_point{0.25, 0.0, 0.0}; + EXPECT_DOUBLE_EQ(line_point[0], 0.25); + EXPECT_DOUBLE_EQ(line_point[1], 0.0); + EXPECT_DOUBLE_EQ(line_point[2], 0.0); + + const QuadPoint surface_point{0.25, 0.5, 0.0}; + EXPECT_DOUBLE_EQ(surface_point[0], 0.25); + EXPECT_DOUBLE_EQ(surface_point[1], 0.5); + EXPECT_DOUBLE_EQ(surface_point[2], 0.0); + + QuadPoint mutable_point = QuadPoint::Zero(); + mutable_point[2] = 0.75; + EXPECT_DOUBLE_EQ(mutable_point[2], 0.75); + + const std::vector points(2, QuadPoint::Zero()); + for (const auto& point : points) { + for (std::size_t component = 0; component < 3u; ++component) { + EXPECT_DOUBLE_EQ(point[component], 0.0); + } + } +} + +TEST(QuadratureRuleValidation, AcceptsEverySupportedReferenceCell) +{ + struct Case { + svmp::CellFamily family; + int expected_dimension; + double expected_measure; + ExpectedPoint point; + }; + + const std::vector cases = { + {svmp::CellFamily::Point, 0, 1.0, {0.0, 0.0, 0.0}}, + {svmp::CellFamily::Line, 1, 2.0, {0.0, 0.0, 0.0}}, + {svmp::CellFamily::Triangle, 2, 0.5, {0.25, 0.25, 0.0}}, + {svmp::CellFamily::Quad, 2, 4.0, {0.0, 0.0, 0.0}}, + {svmp::CellFamily::Tetra, 3, 1.0 / 6.0, {0.25, 0.25, 0.25}}, + {svmp::CellFamily::Hex, 3, 8.0, {0.0, 0.0, 0.0}}, + {svmp::CellFamily::Wedge, 3, 1.0, {0.25, 0.25, 0.0}}, + }; + + for (const auto& c : cases) { + const TestQuadratureRule rule( + c.family, + 0, + {{c.point[0], c.point[1], c.point[2]}}, + {c.expected_measure}); + EXPECT_EQ(rule.dimension(), c.expected_dimension); + EXPECT_DOUBLE_EQ( + rule.reference_cell_measure(), + c.expected_measure); + } +} + +TEST(QuadratureRuleValidation, RejectsInvalidMetadata) +{ + expect_invalid_argument_with_message( + [] { + (void)TestQuadratureRule( + svmp::CellFamily::Triangle, -1, {{0.0, 0.0, 0.0}}, {0.5}); + }, + "polynomial exactness must be non-negative"); + + const std::array unsupported_families = { + svmp::CellFamily::Pyramid, + svmp::CellFamily::Polygon, + svmp::CellFamily::Polyhedron, + }; + for (const auto family : unsupported_families) { + SCOPED_TRACE(static_cast(family)); + expect_invalid_argument_with_message( + [family] { + (void)TestQuadratureRule( + family, 1, {{0.0, 0.0, 0.0}}, {1.0}); + }, + "unsupported reference-cell family"); + } + + expect_invalid_argument_with_message( + [] { + (void)TestQuadratureRule( + static_cast(255), + 1, + {{0.0, 0.0, 0.0}}, + {1.0}); + }, + "unsupported reference-cell family"); +} + +TEST(QuadratureRuleValidation, RejectsMalformedStorageAndNonfiniteValues) +{ + const double nan = std::numeric_limits::quiet_NaN(); + const double inf = std::numeric_limits::infinity(); + + expect_invalid_argument_with_message( + [] { (void)TestQuadratureRule(svmp::CellFamily::Line, 1, {}, {}); }, + "at least one point"); + expect_invalid_argument_with_message( + [] { + (void)TestQuadratureRule( + svmp::CellFamily::Line, 1, {{0.0, 0.0, 0.0}}, {}); + }, + "points/weights size mismatch"); + expect_invalid_argument_with_message( + [nan] { + (void)TestQuadratureRule( + svmp::CellFamily::Line, 1, {{nan, 0.0, 0.0}}, {2.0}); + }, + "non-finite coordinate at point index 0"); + expect_invalid_argument_with_message( + [inf] { + (void)TestQuadratureRule( + svmp::CellFamily::Line, 1, {{0.0, 0.0, 0.0}}, {inf}); + }, + "quadrature weight must be finite at point index 0"); + expect_invalid_argument_with_message( + [nan] { + (void)TestQuadratureRule( + svmp::CellFamily::Line, 1, {{0.0, 0.0, 0.0}}, {nan}); + }, + "quadrature weight must be finite at point index 0"); +} + +TEST(QuadratureRuleValidation, RejectsAnyNonzeroInactiveCoordinate) +{ + constexpr double nonzero = std::numeric_limits::epsilon(); + + expect_invalid_argument_with_message( + [nonzero] { + (void)TestQuadratureRule( + svmp::CellFamily::Point, 0, {{nonzero, 0.0, 0.0}}, {1.0}); + }, + "nonzero inactive coordinate at point index 0"); + expect_invalid_argument_with_message( + [nonzero] { + (void)TestQuadratureRule( + svmp::CellFamily::Line, 1, {{0.0, -nonzero, 0.0}}, {2.0}); + }, + "nonzero inactive coordinate at point index 0"); +} + +TEST(QuadratureRuleValidation, AllowsZeroAndNegativeWeights) +{ + const TestQuadratureRule rule( + svmp::CellFamily::Triangle, + 0, + {{1.0 / 3.0, 1.0 / 3.0, 0.0}, + {0.2, 0.2, 0.0}, + {0.1, 0.1, 0.0}}, + {-0.25, 0.0, 0.75}); + EXPECT_LT(rule.weight(0), 0.0); + EXPECT_DOUBLE_EQ(rule.weight(1), 0.0); +} + +TEST(QuadratureRuleValidation, AllowsExteriorActiveCoordinates) +{ + const TestQuadratureRule rule( + svmp::CellFamily::Line, + 0, + {{2.0, 0.0, 0.0}}, + {2.0}); + EXPECT_DOUBLE_EQ(rule.point(0)[0], 2.0); +} + +TEST(QuadratureRuleValidation, LeavesWeightNormalizationToGenerators) +{ + const TestQuadratureRule rule( + svmp::CellFamily::Triangle, + 0, + {{0.25, 0.25, 0.0}}, + {1.0}); + EXPECT_DOUBLE_EQ(rule.weight(0), 1.0); + EXPECT_DOUBLE_EQ(rule.reference_cell_measure(), 0.5); +} + +TEST(QuadratureRuleContract, SupportsValueSemanticsAndReadOnlyQueries) +{ + static_assert(std::is_abstract_v); + static_assert(!std::is_final_v); + static_assert(std::has_virtual_destructor_v); + static_assert(!std::is_abstract_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_copy_assignable_v); + static_assert(std::is_move_assignable_v); + static_assert( + std::is_same().point(0)), + const QuadPoint&>::value, + "A quadrature point must be exposed through a const reference"); + static_assert( + std::is_same().points()), + const std::vector&>::value, + "Quadrature points must be exposed through a read-only view"); + static_assert( + std::is_same().weights()), + const std::vector&>::value, + "Quadrature weights must be exposed through a read-only view"); + + const double a = 1.0 / std::sqrt(3.0); + std::unique_ptr owned_rule = + std::make_unique( + svmp::CellFamily::Line, + 3, + std::vector{{-a, 0.0, 0.0}, {a, 0.0, 0.0}}, + std::vector{1.0, 1.0}); + const QuadratureRule& rule = *owned_rule; + + EXPECT_EQ(rule.cell_family(), svmp::CellFamily::Line); + EXPECT_EQ(rule.dimension(), 1); + EXPECT_EQ(rule.polynomial_exactness(), 3); + EXPECT_DOUBLE_EQ(rule.reference_cell_measure(), 2.0); + ASSERT_EQ(rule.num_points(), 2u); + ASSERT_EQ(rule.points().size(), 2u); + ASSERT_EQ(rule.weights().size(), 2u); + EXPECT_DOUBLE_EQ(rule.point(0)[0], -a); + EXPECT_DOUBLE_EQ(rule.point(1)[0], a); + EXPECT_DOUBLE_EQ(rule.weight(0), 1.0); + EXPECT_DOUBLE_EQ(rule.weight(1), 1.0); +}