diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index db67bbbdb..c1f8a3b5d 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - name: Build doxygen documentation run: | - sudo apt install -y doxygen + sudo apt install -y doxygen graphviz doxygen Documentation/Doxyfile - name: Save documentation uses: actions/upload-artifact@v4 diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index c546c2822..bac65c976 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -23,15 +23,18 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) include_directories(${SV_SOURCE_DIR}/ThirdParty/eigen/include) +include_directories(${SV_SOURCE_DIR}/ThirdParty/eigen/include/eigen3) include_directories(${SV_SOURCE_DIR}/ThirdParty/parmetis_internal/simvascular_parmetis_internal/ParMETISLib) include_directories(${SV_SOURCE_DIR}/ThirdParty/tetgen/simvascular_tetgen) include_directories(${SV_SOURCE_DIR}/ThirdParty/tinyxml/simvascular_tinyxml) include_directories(${MPI_C_INCLUDE_PATH}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/Core) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/FE) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/FE/Common) # Find Trilinos package if requested @@ -86,7 +89,7 @@ endif() # add trilinos flags and defines if(USE_TRILINOS) ADD_DEFINITIONS(-DWITH_TRILINOS) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++${CMAKE_CXX_STANDARD}") endif() # Build with the PETSc linear algebra package. @@ -245,9 +248,21 @@ file(GLOB SOLVER_FE_COMMON_SRCS CONFIGURE_DEPENDS FE/Common/*.h ) +file(GLOB SOLVER_FE_BASIS_SRCS CONFIGURE_DEPENDS + FE/Basis/*.cpp + FE/Basis/*.h +) + +file(GLOB SOLVER_FE_MATH_SRCS CONFIGURE_DEPENDS + FE/Math/*.cpp + FE/Math/*.h +) + list(APPEND CSRCS ${SOLVER_CORE_SRCS} ${SOLVER_FE_COMMON_SRCS} + ${SOLVER_FE_BASIS_SRCS} + ${SOLVER_FE_MATH_SRCS} ) # Set PETSc interace code. @@ -324,16 +339,24 @@ if(ENABLE_UNIT_TEST) # link pthread on ubuntu20 find_package(Threads REQUIRED) - + include(FetchContent) # install Google Test #if(NOT TARGET gtest_main AND NOT TARGET gtest) - include(FetchContent) FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/heads/main.zip - DOWNLOAD_EXTRACT_TIMESTAMP TRUE + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.17.0 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(googletest) + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_STANDARD GREATER_EQUAL 20) + foreach(GTEST_TARGET gtest gtest_main gmock gmock_main) + if(TARGET ${GTEST_TARGET}) + target_compile_options(${GTEST_TARGET} PRIVATE -std=gnu++17) + endif() + endforeach() + endif() #endif() enable_testing() diff --git a/Code/Source/solver/Core/Exception.h b/Code/Source/solver/Core/Exception.h index 80e6968ec..0c197c8bc 100644 --- a/Code/Source/solver/Core/Exception.h +++ b/Code/Source/solver/Core/Exception.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -281,12 +282,23 @@ class ExceptionBase : public std::exception { rebuild_what(); } + /// @brief Record the originating source location and refresh what(). + /// + /// @details Called by raise() after construction, so exception constructors + /// do not need to accept (and forward) the file/line/function themselves. + void set_source_location(const SourceLocation& location) + { + context_.set_source_location(location.file, location.line, + location.function); + rebuild_what(); + } + virtual ~ExceptionBase() noexcept = default; protected: ExceptionBase(std::string message, StatusCode status, - std::string_view subsystem_label, const char* file, - int line, const char* function) + std::string_view subsystem_label, const char* file = "", + int line = 0, const char* function = "") : message_(std::move(message)), subsystem_label_(subsystem_label.empty() ? std::string_view("Exception") : subsystem_label) @@ -323,28 +335,51 @@ class CoreException : public ExceptionBase { } }; -class ParseException : public CoreException { -public: - ParseException(const std::string& message, - const char* file = "", - int line = 0, - const char* function = "") - : CoreException(message, StatusCode::ParseError, file, line, function) - { +/** + * @brief Define a simple, message-only exception type in one line. + * + * @details Expands to a class @p Name deriving from @p Base with a single + * `explicit Name(const std::string& message)` constructor that records @p Status. + * Use it for exceptions that carry only a message; write the class by hand when it + * needs extra structured context (members and accessors). The source location is + * stamped by raise(), so no file/line/function constructor is needed. + * + * @p Base must be an ExceptionBase-derived type whose constructor accepts + * `(const std::string&, StatusCode)` (CoreException, FEException, and the + * subsystem bases do). + * + * @code + * SVMP_DEFINE_EXCEPTION(ParseException, CoreException, StatusCode::ParseError); + * @endcode + */ +#define SVMP_DEFINE_EXCEPTION(Name, Base, Status) \ + class Name : public Base { \ + public: \ + explicit Name(const std::string& message) \ + : Base(message, (Status)) \ + { \ + } \ } -}; -class DependencyException : public CoreException { -public: - DependencyException(const std::string& message, - const char* file = "", - int line = 0, - const char* function = "") - : CoreException(message, StatusCode::DependencyError, file, line, - function) - { - } -}; +/// @brief A parsing or input-format error. +SVMP_DEFINE_EXCEPTION(ParseException, CoreException, StatusCode::ParseError); + +/// @brief A required dependency is missing or failed to load. +SVMP_DEFINE_EXCEPTION(DependencyException, CoreException, + StatusCode::DependencyError); + +/// @brief A requested operation or feature is not implemented. +/// +/// @details The default exception raised by not_implemented(). +SVMP_DEFINE_EXCEPTION(NotImplementedException, CoreException, + StatusCode::NotImplemented); + +/// @brief An index is outside its valid range. +/// +/// @details The default exception raised by check_index(); the status code is +/// InvalidArgument because an out-of-range index is a caller error. +SVMP_DEFINE_EXCEPTION(IndexOutOfRangeException, CoreException, + StatusCode::InvalidArgument); inline void ExceptionRuntime::install_terminate_handler() { @@ -366,37 +401,168 @@ inline void ExceptionRuntime::install_terminate_handler() }); } +/** + * @brief A diagnostic message bundled with the source location where it was written. + * + * @details The core helpers (raise(), check(), throw_if(), check_not_null()) take + * a Diagnostic in place of an explicit source location. Its file/line/function + * arguments default to the compiler builtins __builtin_FILE()/__builtin_LINE()/ + * __builtin_FUNCTION(), which capture the caller's location, so a string literal + * or std::string passed at the call site is implicitly wrapped into a Diagnostic + * that records exactly where the call appears -- callers do not pass SVMP_HERE: + * @code + * svmp::check(ptr != nullptr, "pointer must not be null"); + * @endcode + */ +class Diagnostic { +public: + /** + * @brief Wrap a message, capturing the caller's source location by default. + * @param message The diagnostic message. + * @param file Source file; defaults to the caller's via __builtin_FILE(). + * @param line Source line; defaults to the caller's via __builtin_LINE(). + * @param function Function; defaults to the caller's via __builtin_FUNCTION(). + */ + Diagnostic(const char* message, + const char* file = __builtin_FILE(), + int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) + : message_(message), location_{file, line, function} + { + } + /** + * @brief Wrap a message, capturing the caller's source location by default. + * @param message The diagnostic message. + * @param file Source file; defaults to the caller's via __builtin_FILE(). + * @param line Source line; defaults to the caller's via __builtin_LINE(). + * @param function Function; defaults to the caller's via __builtin_FUNCTION(). + */ + Diagnostic(std::string message, + const char* file = __builtin_FILE(), + int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) + : message_(std::move(message)), location_{file, line, function} + { + } + + /** + * @brief The diagnostic message. + * @return The stored message. + */ + const std::string& message() const noexcept { return message_; } + /** + * @brief The source location captured when the Diagnostic was constructed. + * @return The stored source location. + */ + const SourceLocation& location() const noexcept { return location_; } + +private: + std::string message_; + SourceLocation location_; +}; + +/** + * @brief Construct @p ExceptionT from the diagnostic message and @p args, stamp + * the source location, and throw it. + * + * @details @p diagnostic carries the message and the source location captured at + * the call site; @p args are forwarded to the exception constructor after the + * message. The location is recorded via ExceptionBase::set_source_location(), so + * exception types never need a file/line/function constructor -- a `(message)` + * (plus any structured-context) constructor is enough. + */ template -[[noreturn]] void raise(SourceLocation location, Args&&... args) +[[noreturn]] void raise(Diagnostic diagnostic, Args&&... args) { - throw ExceptionT(std::forward(args)..., location.file, location.line, - location.function); + static_assert(std::is_base_of_v, + "raise<>() requires an svmp::ExceptionBase-derived exception type"); + ExceptionT exception(diagnostic.message(), std::forward(args)...); + exception.set_source_location(diagnostic.location()); + throw exception; } +/** + * @brief Raise @p ExceptionT when @p condition is false (a required condition). + * + * @details The general success-condition check, used for argument, state, and + * invariant validation: `check(ptr != nullptr, "...")`. throw_if() is the + * logical inverse -- it raises when its condition is true. + */ template -void check(bool condition, SourceLocation location, Args&&... args) +void check(bool condition, Diagnostic diagnostic, Args&&... args) { if (!condition) { - raise(location, std::forward(args)...); + raise(std::move(diagnostic), std::forward(args)...); } } -template -void check_arg(bool condition, SourceLocation location, Args&&... args) +/** + * @brief Raise @p ExceptionT when @p ptr is null. + */ +template +void check_not_null(PointerT ptr, Diagnostic diagnostic, Args&&... args) { - if (!condition) { - raise(location, std::forward(args)...); + if (ptr == nullptr) { + raise(std::move(diagnostic), std::forward(args)...); } } -template -void check_not_null(PointerT ptr, SourceLocation location, Args&&... args) +/** + * @brief Raise @p ExceptionT when @p condition is true. + * + * @details The logical inverse of check(): check() raises when its condition is + * false (a required condition); throw_if() raises when its condition is true (a + * failure condition). The two are not interchangeable. + */ +template +void throw_if(bool condition, Diagnostic diagnostic, Args&&... args) { - if (ptr == nullptr) { - raise(location, std::forward(args)...); + if (condition) { + raise(std::move(diagnostic), std::forward(args)...); } } +/** + * @brief Raise an exception when @p index is outside [0, @p size). + * + * @details @p ExceptionT defaults to IndexOutOfRangeException; supply a different + * type only when a subsystem needs its own exception. The bounds message and the + * source location are generated automatically. + */ +template +void check_index(IndexT index, SizeT size, + const char* file = __builtin_FILE(), + int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) +{ + const long long index_value = static_cast(index); + const long long size_value = static_cast(size); + check( + index_value >= 0 && index_value < size_value, + Diagnostic("Index " + std::to_string(index_value) + " out of bounds [0, " + + std::to_string(size_value) + ")", + file, line, function)); +} + +/** + * @brief Raise an exception reporting an unimplemented feature, with a message. + * + * @details @p ExceptionT defaults to NotImplementedException, so most call sites + * pass only a message: + * @code + * svmp::not_implemented("GPU assembly is not supported"); + * @endcode + * Pass a different exception type explicitly only when a subsystem needs one. + */ +template +[[noreturn]] void not_implemented(std::string message, + const char* file = __builtin_FILE(), + int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) +{ + raise(Diagnostic(std::move(message), file, line, function)); +} + } // namespace svmp #define SVMP_HERE ::svmp::SourceLocation{__FILE__, __LINE__, __func__} @@ -405,7 +571,7 @@ void check_not_null(PointerT ptr, SourceLocation location, Args&&... args) #define SVMP_DEBUG_CHECK(ExceptionT, condition, ...) \ do { \ if (!(condition)) { \ - ::svmp::raise(SVMP_HERE, __VA_ARGS__); \ + ::svmp::raise(__VA_ARGS__); \ } \ } while (false) #else diff --git a/Code/Source/solver/FE/Basis/BasisExceptions.h b/Code/Source/solver/FE/Basis/BasisExceptions.h new file mode 100644 index 000000000..cc8680c7a --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisExceptions.h @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_BASISEXCEPTIONS_H +#define SVMP_FE_BASIS_BASISEXCEPTIONS_H + +#include "FEException.h" + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @defgroup FE_BasisExceptions Exceptions + * @ingroup FE_Basis + * @brief Basis-module exception hierarchy. + * + * @details Every Basis exception derives from BasisException (and thus FEException), + * so a caller can catch a specific basis failure or the FE base type. See + * BasisException for why the module raises these basis-specific types rather than + * the generic FE exceptions. + * @{ + */ + +/** + * @brief Base exception type for errors originating in the Basis module + * + * @details The Basis module raises these basis-specific types -- rather than the + * generic FE exceptions in FEException.h -- so a caller can catch a basis failure + * precisely: an unsupported element/order pairing, a non-unisolvent node set, an + * out-of-range reference-node index. They all derive from FEException, so code + * that only wants "some FE error" can still catch the base type, and they carry + * the same StatusCode and source location as the rest of the hierarchy. + */ +class BasisException : public FEException { +public: + explicit BasisException(const std::string& message, + StatusCode status = StatusCode::Unknown) + : FEException(message, status) {} +}; + +/** + * @brief Invalid Basis request or configuration + * + * @details Raised when a request is malformed before any geometry is built: a + * missing or negative polynomial order, a named element layout paired with an + * explicit order that does not match its fixed order, or a field type or + * continuity the scalar Lagrange/Serendipity factory does not support. Example: + * constructing a LagrangeBasis for Tetra10 at order 1, when that layout is fixed + * at order 2. + */ +SVMP_DEFINE_EXCEPTION(BasisConfigurationException, BasisException, + StatusCode::InvalidArgument); + +/** + * @brief Requested element topology is incompatible with the basis family + * + * @details Raised when the family cannot represent the requested topology or + * named layout. Example: requesting wedge serendipity through the arbitrary-order + * topology path (only the named Wedge15 layout is supported), or requesting a + * basis on ElementType::Unknown. + */ +SVMP_DEFINE_EXCEPTION(BasisElementCompatibilityException, BasisException, + StatusCode::InvalidArgument); + +/** + * @brief Basis evaluation request cannot be satisfied + * + * @details Raised at evaluation time rather than construction time. Example: an + * output span smaller than size(), or requesting analytical gradients or Hessians + * from a basis that does not provide them. + */ +SVMP_DEFINE_EXCEPTION(BasisEvaluationException, BasisException, + StatusCode::InvalidArgument); + +/** + * @brief Public-to-canonical node ordering or coordinate lookup failure + * + * @details Raised when a node index or coordinate lookup falls outside the + * reference layout. Example: requesting a tensor-axis node index outside + * [0, order] from line_coord_pm_one. + */ +SVMP_DEFINE_EXCEPTION(BasisNodeOrderingException, BasisException, + StatusCode::InvalidArgument); + +/** + * @brief Internal basis construction or transform setup failure + * + * @details Signals a violated internal invariant during setup (StatusCode:: + * InternalError) rather than bad user input. Example: a generated Lagrange node + * lattice whose index components fall outside [0, order] in get_lagrange_lattice. + */ +SVMP_DEFINE_EXCEPTION(BasisConstructionException, BasisException, + StatusCode::InternalError); + +/** @} */ + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_BASISEXCEPTIONS_H diff --git a/Code/Source/solver/FE/Basis/BasisFactory.cpp b/Code/Source/solver/FE/Basis/BasisFactory.cpp new file mode 100644 index 000000000..11581b454 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFactory.cpp @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "BasisFactory.h" + +#include "BasisTraits.h" +#include "LagrangeBasis.h" +#include "SerendipityBasis.h" + +namespace svmp { +namespace FE { +namespace basis { + +namespace { + +enum class RequestTarget { + NamedElement, + Topology, +}; + +int require_basis_order(const BasisRequest& req, + const char* missing_message, + const char* negative_message) { + svmp::throw_if(!req.order.has_value(), missing_message); + svmp::throw_if(*req.order < 0, negative_message); + return *req.order; +} + +RequestTarget require_single_request_target(const BasisRequest& req) { + const bool has_named_element = req.element_type != ElementType::Unknown; + const bool has_topology = req.topology != BasisTopology::Unknown; + svmp::throw_if( + !has_named_element && !has_topology, "BasisFactory: request must specify either a named element_type or a reference topology"); + svmp::throw_if( + has_named_element && has_topology, "BasisFactory: request must specify element_type or topology, not both"); + return has_topology ? RequestTarget::Topology : RequestTarget::NamedElement; +} + +void require_scalar_c0_request(const BasisRequest& req) { + svmp::throw_if( + req.field_type != FieldType::Scalar, "BasisFactory: Lagrange/Serendipity bases support scalar fields only"); + svmp::throw_if( + req.continuity != Continuity::C0, "BasisFactory: Lagrange/Serendipity bases support C0 continuity only"); +} + +std::unique_ptr create_lagrange(const BasisRequest& req) { + require_scalar_c0_request(req); + const int order = require_basis_order( + req, + "BasisFactory: Lagrange creation requires an explicit order", + "BasisFactory: Lagrange requires non-negative order"); + if (require_single_request_target(req) == RequestTarget::Topology) { + return std::make_unique(req.topology, order); + } + return std::make_unique(req.element_type, order); +} + +std::unique_ptr create_serendipity(const BasisRequest& req) { + require_scalar_c0_request(req); + const int order = require_basis_order( + req, + "BasisFactory: Serendipity creation requires an explicit order", + "BasisFactory: Serendipity requires non-negative order"); + if (require_single_request_target(req) == RequestTarget::Topology) { + return std::make_unique(req.topology, order); + } + return std::make_unique(req.element_type, order); +} + +} // namespace + +namespace basis_factory { + +std::unique_ptr create(const BasisRequest& req) { + switch (req.basis_type) { + case BasisType::Lagrange: + return create_lagrange(req); + case BasisType::Serendipity: + return create_serendipity(req); + default: + svmp::raise("BasisFactory: requested basis family is outside the scalar Lagrange/Serendipity scope"); + } +} + +BasisRequest default_basis_request(ElementType element_type) { + switch (element_type) { + // Reduced serendipity node layouts have no complete Lagrange basis at + // their node count; they always use the quadratic serendipity space. + case ElementType::Quad8: + case ElementType::Hex20: + case ElementType::Wedge15: + return BasisRequest{element_type, BasisType::Serendipity, 2}; + case ElementType::Point1: + return BasisRequest{element_type, BasisType::Lagrange, 0}; + default: { + const int order = complete_lagrange_alias_order(element_type); + if (order >= 0) { + return BasisRequest{element_type, BasisType::Lagrange, order}; + } + svmp::raise("BasisFactory: no default basis is defined for the requested element type"); + } + } +} + +std::unique_ptr create_default_for(ElementType element_type) { + return create(default_basis_request(element_type)); +} + +} // namespace basis_factory + +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Basis/BasisFactory.h b/Code/Source/solver/FE/Basis/BasisFactory.h new file mode 100644 index 000000000..33c26be67 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFactory.h @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_BASISFACTORY_H +#define SVMP_FE_BASIS_BASISFACTORY_H + +/** + * @file BasisFactory.h + * @brief Runtime creation of basis families + */ + +#include "BasisFunction.h" +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @brief Runtime description of a basis to construct. + * @ingroup FE_Basis + * + * @details A request identifies exactly one construction target -- a named + * ElementType layout, or a reference BasisTopology with an explicit order -- plus + * the family and field policy; basis_factory::create() validates and builds from + * it. The spline/NURBS fields are reserved for future families and are unused by + * the scalar Lagrange/Serendipity factory. + */ +struct BasisRequest { + ElementType element_type{ElementType::Unknown}; ///< Named element layout, or Unknown to request by topology. + BasisType basis_type{BasisType::Lagrange}; ///< Basis family to construct. + std::optional order{}; ///< Polynomial order; required by the factory. + Continuity continuity{Continuity::C0}; ///< Inter-element continuity (Lagrange/Serendipity are C0). + FieldType field_type{FieldType::Scalar}; ///< Field type (Lagrange/Serendipity support Scalar). + std::vector knot_vector{}; ///< Reserved for spline/NURBS families; unused here. + std::vector weights{}; ///< Reserved for rational (NURBS) families; unused here. + std::vector axis_orders{}; ///< Reserved for per-axis tensor spline orders; unused here. + std::vector> axis_knot_vectors{}; ///< Reserved for per-axis spline knots; unused here. + std::vector> axis_weights{}; ///< Reserved for per-axis rational weights; unused here. + std::vector tensor_extents{}; ///< Reserved for tensor-product extents; unused here. + std::string custom_id{}; ///< Optional identifier for Custom families. + /// Reference topology for arbitrary-order requests, or Unknown to request by + /// element_type. + BasisTopology topology{BasisTopology::Unknown}; + // Implementation note (kept out of the rendered docs): topology is declared + // last so existing aggregate initializers for named elements keep their + // positional meaning. +}; + +namespace basis_factory { + +/** + * @brief Create a basis from a runtime request. + * @ingroup FE_Basis + * + * @details A request must identify exactly one construction target: set + * BasisRequest::element_type for a named mesh-node layout, or set + * BasisRequest::topology for an arbitrary-order reference-topology basis. + * Setting neither target, or setting both, is rejected. Named element requests + * keep the element's fixed polynomial order contract; topology requests are the + * arbitrary-order path. + * + * @param req Basis family, target, and order request. + * @return Unique basis instance. Move it into a std::shared_ptr at the call site + * if shared ownership is needed. + */ +[[nodiscard]] std::unique_ptr create(const BasisRequest& req); + +/** + * @brief Return the default basis request (family and order) for an element type. + * @ingroup FE_Basis + * + * @details This is the single source of truth for which basis family and + * polynomial order a given element type uses by default: serendipity node + * layouts (Quad8, Hex20, Wedge15) select the quadratic serendipity family, + * and every complete Lagrange element selects the Lagrange family at the + * order given by its node layout. Solver-facing adapters should translate + * their element names to ElementType and delegate the basis choice here + * rather than tabulating family/order themselves. + * + * @param element_type Element type to select a default basis for. + * @return Basis request suitable for create(). + * @throws BasisElementCompatibilityException If no default basis is defined + * for the element type. + */ +[[nodiscard]] BasisRequest default_basis_request(ElementType element_type); + +/** + * @brief Create the default basis for an element type. + * @ingroup FE_Basis + * + * @details Equivalent to create(default_basis_request(element_type)). + * + * @param element_type Element type to create a default basis for. + * @return Unique basis instance. Move it into a std::shared_ptr at the call site + * if shared ownership is needed. + */ +[[nodiscard]] std::unique_ptr create_default_for(ElementType element_type); + +} // namespace basis_factory + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_BASISFACTORY_H diff --git a/Code/Source/solver/FE/Basis/BasisFunction.cpp b/Code/Source/solver/FE/Basis/BasisFunction.cpp new file mode 100644 index 000000000..982b4c4c1 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFunction.cpp @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "BasisFunction.h" + +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +void require_span_size(std::size_t actual, + std::size_t expected, + const char* label) { + svmp::throw_if(actual < expected, std::string(label) + ": output span is smaller than basis size"); +} + +const std::vector>& BasisFunction::nodes() const noexcept { + // Default for bases that do not expose interpolation nodes; nodal families + // (LagrangeBasis, SerendipityBasis) override this to return their layout. + static const std::vector> kNoNodes; + return kNoNodes; +} + +// Vector-output overloads: size the container and forward to the matching span +// primitive. Defined once here so concrete families implement only the span +// primitives below. +void BasisFunction::evaluate_values(const math::Vector& xi, + std::vector& values) const { + values.resize(size()); + evaluate_values_to(xi, std::span(values.data(), values.size())); +} + +void BasisFunction::evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const { + gradients.resize(size()); + evaluate_gradients_to(xi, std::span(gradients.data(), gradients.size())); +} + +void BasisFunction::evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const { + hessians.resize(size()); + evaluate_hessians_to(xi, std::span(hessians.data(), hessians.size())); +} + +void BasisFunction::evaluate_all(const math::Vector& xi, + std::vector& values, + std::vector& gradients, + std::vector& hessians) const { + values.resize(size()); + gradients.resize(size()); + hessians.resize(size()); + evaluate_all_to(xi, + std::span(values.data(), values.size()), + std::span(gradients.data(), gradients.size()), + std::span(hessians.data(), hessians.size())); +} + +// The gradient/Hessian span primitives default to reporting "not implemented"; a +// family supplies analytical derivatives by overriding them. evaluate_values_to +// has no base definition: every basis must provide values. +void BasisFunction::evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const { + (void)xi; + (void)gradients_out; + svmp::raise("Analytic gradient evaluation is not implemented for this basis"); +} + +void BasisFunction::evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const { + (void)xi; + (void)hessians_out; + svmp::raise("Analytic Hessian evaluation is not implemented for this basis"); +} + +// Combined evaluator default: forward each requested (non-empty) quantity to its +// single-quantity span primitive. Families override this to share per-point setup +// across the requested quantities. +void BasisFunction::evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + if (!values_out.empty()) { + evaluate_values_to(xi, values_out); + } + if (!gradients_out.empty()) { + evaluate_gradients_to(xi, gradients_out); + } + if (!hessians_out.empty()) { + evaluate_hessians_to(xi, hessians_out); + } +} + +void BasisFunction::numerical_gradient(const math::Vector& xi, + std::vector& gradients, + double eps) const { + std::vector base; + evaluate_values(xi, base); + gradients.assign(base.size(), Gradient::Zero()); + + for (int d = 0; d < dimension(); ++d) { + math::Vector forward = xi; + math::Vector backward = xi; + const auto idx = static_cast(d); + forward[idx] += eps; + backward[idx] -= eps; + + std::vector fwd; + std::vector bwd; + evaluate_values(forward, fwd); + evaluate_values(backward, bwd); + + for (std::size_t i = 0; i < base.size(); ++i) { + gradients[i][idx] = (fwd[i] - bwd[i]) / (double(2) * eps); + } + } +} + +void BasisFunction::numerical_hessian(const math::Vector& xi, + std::vector& hessians, + double eps) const { + std::vector base_grad; + evaluate_gradients(xi, base_grad); + hessians.assign(base_grad.size(), Hessian::Zero()); + + for (int d = 0; d < dimension(); ++d) { + math::Vector forward = xi; + math::Vector backward = xi; + const auto col = static_cast(d); + forward[col] += eps; + backward[col] -= eps; + + std::vector g_forward; + std::vector g_backward; + evaluate_gradients(forward, g_forward); + evaluate_gradients(backward, g_backward); + + for (std::size_t i = 0; i < base_grad.size(); ++i) { + for (int k = 0; k < dimension(); ++k) { + const auto row = static_cast(k); + hessians[i](row, col) = + (g_forward[i][row] - g_backward[i][row]) / (double(2) * eps); + } + } + } +} + +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Basis/BasisFunction.h b/Code/Source/solver/FE/Basis/BasisFunction.h new file mode 100644 index 000000000..ba9e256cb --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFunction.h @@ -0,0 +1,388 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_BASISFUNCTION_H +#define SVMP_FE_BASIS_BASISFUNCTION_H + +#include "BasisExceptions.h" +#include "BasisTraits.h" +#include "Math/Matrix.h" +#include "Math/Vector.h" +#include "Types.h" + +#include +#include +#include + +/** + * @defgroup FE_Basis Basis + * @ingroup FE + * @brief Basis-function interfaces, concrete basis families, and reference-node conventions. + * + * @details + * ## Scope + * + * The Basis module owns reference-element shape functions. It provides the + * number of basis functions and the values and derivatives, + * @f$N_i@f$, @f$\partial N_i / \partial \xi_j@f$, and + * @f$\partial^2 N_i / \partial \xi_j \partial \xi_k@f$ at reference + * points. It does not own mesh storage, quadrature selection, field + * formulation policy, or transformation of derivatives to physical + * coordinates. Those decisions stay with the solver layer that has the mesh, + * material model, and equation context. + * + * The main pieces are: + * - @ref svmp::FE::basis::BasisFunction "BasisFunction" (BasisFunction.h): the + * abstract query and evaluation contract for code that does not need to know + * the concrete family. + * - @ref FE_LagrangeBasis "LagrangeBasis" and + * @ref FE_SerendipityBasis "SerendipityBasis": the implemented nodal + * families, including analytical first and second derivatives in reference + * coordinates. + * - basis_factory (BasisFactory.h): runtime construction from a + * @ref svmp::FE::basis::BasisRequest "BasisRequest". + * basis_factory::default_basis_request() centralizes the family/order that + * matches each supported element's public node layout. + * - @ref svmp::FE::basis::ReferenceNodeLayout "ReferenceNodeLayout" + * (NodeOrderingConventions.h): canonical reference-node coordinates and the + * output ordering used by every basis evaluator. + * - @ref svmp::FE::basis::BasisTopology "BasisTopology" (BasisTraits.h) and the + * @ref FE_BasisExceptions "basis exceptions" (BasisExceptions.h): topology + * classification, compile-time helpers, and module-specific exception types. + * + * ## Object and evaluation contract + * + * A basis object is immutable after construction. It represents one reference + * topology (e.g. tetrahedron, hexahedron), basis family (Lagrange or + * serendipity), and effective polynomial order, and can be shared + * safely across evaluations. Construction may build node lattices or invert + * interpolation matrices, so callers should construct through basis_factory + * and cache one instance for each distinct basis request instead of rebuilding + * inside element loops. + * + * Every evaluator takes a three-component reference coordinate. For + * lower-dimensional elements, only the first dimension() components are + * active. Returned gradients always have three components and Hessians are + * always 3-by-3 matrices; inactive reference directions are expected to be + * zero for conforming lower-dimensional bases. The *_to overloads write to + * caller-owned spans and are the override points a concrete family implements: + * the nodal families (LagrangeBasis, SerendipityBasis) compute directly into the + * span, so this is the allocation-free path for assembly. The std::vector + * overloads are convenient for setup, tests, and adapter code; they are defined + * once on the base class, which sizes the output and forwards to the matching + * span overload. + * + * Outputs are in ReferenceNodeLayout basis order, not necessarily the mesh or + * solver's native node order. A caller that stores elements in another local + * ordering must apply the appropriate permutation at the boundary between the + * basis module and that storage format. + * + * ## Inputs and ownership + * + * Constructing and evaluating a basis combines several independent choices: + * + * - **Element topology comes from the mesh.** The mesh cell type is translated + * to ElementType, which defines the reference topology and public node + * layout. This is structural information, not a complete discretization + * policy. + * - **Geometry interpolation follows the mesh nodes.** The basis used for the + * reference-to-physical map must be compatible with the element's node + * count and ordering. For that case, callers normally use + * basis_factory::create_default_for(element_type), which selects the + * Lagrange or serendipity space associated with that element layout. A + * Tetra10 mesh therefore implies a quadratic geometry map; a Hex20 mesh + * implies the supported Hex20 serendipity geometry basis. + * - **Field approximation is chosen by the formulation.** Field bases do not + * have to match the geometry map. Mixed formulations, stabilized methods, + * enrichment, and convergence studies may use different families or orders + * for different fields on the same mesh topology. Those bases should be + * requested explicitly with basis_factory::create() and a BasisRequest + * naming the desired family, topology, and order. + * - **Evaluation points come from the caller.** Quadrature rules, probe + * points, interpolation targets, and error-sampling locations are outside + * this module. The basis only evaluates at the reference coordinates it is + * given. + * + * @dot "Basis inputs and responsibilities" + * digraph fe_basis_information_flow { + * rankdir=LR; + * node [shape=box, fontname=Helvetica, fontsize=10]; + * mesh [label="Mesh element type"]; + * request [label="BasisRequest\nfamily + order"]; + * topology [label="Reference topology\nand node layout"]; + * basis [label="Basis object", style=filled, fillcolor=lightgray]; + * points [label="Reference points"]; + * outputs [label="Reference values\nand derivatives"]; + * mesh -> topology; + * request -> basis; + * topology -> basis; + * basis -> outputs; + * points -> outputs; + * } + * @enddot + * + * ## Reference scope and the solver adapter + * + * The solver-facing adapter in nn.cpp is the boundary between this reference + * basis contract and legacy solver storage. It translates solver element + * enums to ElementType, obtains cached default bases for mesh/face shape + * tables, permutes from ReferenceNodeLayout order into solver node order, and + * stores N, Nx, and, where needed, packed Nxx at Gauss points. At that stage + * Nx and Nxx are still derivatives with respect to reference coordinates. + * Physical-coordinate derivatives are formed later, for a particular + * configuration and element geometry, by composing the cached reference data + * with the mapping Jacobian (nn::gnn for first derivatives and nn::gn_nxx for + * second derivatives). + */ + +namespace svmp { +namespace FE { +namespace basis { + +/** @brief Gradient vector type used by basis evaluators. */ +using Gradient = math::Vector; + +/** @brief Hessian matrix type used by basis evaluators. */ +using Hessian = math::Matrix; + +/** + * @brief Throw BasisEvaluationException when an output span is smaller than the + * basis size. \p label is the full "Class::method" context used in the message, + * so each basis family passes its own qualified name. + */ +void require_span_size(std::size_t actual, std::size_t expected, const char* label); + +/** + * @brief Abstract interface for finite-element basis-function families. + * @ingroup FE_Basis + * + * BasisFunction defines the common query and evaluation API used by solver + * code that does not need to know the concrete basis implementation. Concrete + * families implement the span output primitives -- shape function values at + * minimum, and optionally analytical gradients and Hessians; the vector + * overloads and the combined evaluator are provided once by the base class. The + * interface is deliberately limited to reference-space quantities; callers own + * node ordering translation, physical mapping, and any field-level discretization + * policy. + */ +class BasisFunction { +public: + /** @brief Destroy a basis function through the abstract interface. */ + virtual ~BasisFunction() = default; + + /** + * @brief Return the concrete basis family. + * @return Basis family identifier. + */ + virtual BasisType basis_type() const noexcept = 0; + + /** + * @brief Return the reference topology of this basis. + * + * @details Together with order() and basis_type(), this is the authoritative + * identity of a basis: a topology, a polynomial order, and a basis family, + * with no node-count assumption. The family is part of the identity because + * the same topology and order can denote different bases -- a hexahedron at + * order 2 is the Hex20 serendipity space or the Hex27 Lagrange space + * depending on basis_type(). Arbitrary-order bases are constructed from a + * BasisTopology and an order; named ElementType layouts (Hex8, Hex27, ...) + * are a fixed-order shorthand that maps to the same (topology, order, family) + * triple. + * + * @return Reference topology. + */ + virtual BasisTopology topology() const noexcept = 0; + + /** + * @brief Return the reference-space dimension of the basis. + * @return Reference dimension, from zero for points through three for volume elements. + */ + virtual int dimension() const noexcept = 0; + + /** + * @brief Return the polynomial order represented by this basis. + * @return Polynomial order of the basis. A named element layout reports the + * order implied by that layout (Quad8 and Hex20 report 2, Hex8 + * reports 1), not its node count. + */ + virtual int order() const noexcept = 0; + + /** + * @brief Return the number of basis functions and reference nodes. + * @return Basis function count. + */ + virtual std::size_t size() const noexcept = 0; + + /** + * @brief Return the reference interpolation nodes in basis ordering. + * + * @details Nodal families return one reference-element coordinate per basis + * function, in the same order as the evaluator outputs. Bases that do not + * define interpolation nodes (non-nodal families, or abstract base usage) + * return an empty vector. The returned reference is valid for the lifetime + * of the basis object. + * + * @return Reference node coordinates: size() entries for nodal families, + * empty otherwise. + */ + virtual const std::vector>& nodes() const noexcept; + + /** + * @brief Evaluate basis function values at a reference coordinate. + * + * @details Convenience overload: it sizes \p values to size() and forwards to + * evaluate_values_to(). It is implemented once on the base class, so concrete + * families override the span primitive rather than this overload. The result + * is delivered through the output argument rather than by return value so a + * caller can reuse one container across repeated evaluations (for example, + * across quadrature points) instead of allocating on every call. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values Receives one value per basis function. + */ + void evaluate_values(const math::Vector& xi, + std::vector& values) const; + + /** + * @brief Evaluate basis gradients at a reference coordinate. + * + * @details Convenience overload over evaluate_gradients_to(); see + * evaluate_values() for the sizing and forwarding contract. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param gradients Receives one three-component gradient per basis function. + * @throws BasisEvaluationException If gradients are not available for the basis. + */ + void evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const; + + /** + * @brief Evaluate basis Hessians at a reference coordinate. + * + * @details Convenience overload over evaluate_hessians_to(); see + * evaluate_values() for the sizing and forwarding contract. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param hessians Receives one 3-by-3 Hessian per basis function. + * @throws BasisEvaluationException If Hessians are not available for the basis. + */ + void evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const; + + /** + * @brief Evaluate values, gradients, and Hessians together. + * + * @details Convenience overload over evaluate_all_to(): it sizes all three + * containers to size() and forwards them in a single pass. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values Receives one value per basis function. + * @param gradients Receives one three-component gradient per basis function. + * @param hessians Receives one 3-by-3 Hessian per basis function. + */ + void evaluate_all(const math::Vector& xi, + std::vector& values, + std::vector& gradients, + std::vector& hessians) const; + + /** + * @brief Evaluate basis values into caller-provided storage. + * + * @details This span primitive is the single required override for a concrete + * basis: the vector overloads above and the combined evaluate_all_to() are all + * defined in terms of it, so a minimal basis implements only this method. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values_out Output span with at least size() entries. + */ + virtual void evaluate_values_to(const math::Vector& xi, + std::span values_out) const = 0; + + /** + * @brief Evaluate basis gradients into caller-provided storage. + * + * @details Override to supply analytical gradients. The base implementation + * throws, so a family that provides no gradients reports it uniformly through + * every gradient entry point. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param gradients_out Output span with at least size() entries. + * @throws BasisEvaluationException If gradients are not available for the basis. + */ + virtual void evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const; + + /** + * @brief Evaluate basis Hessians into caller-provided storage. + * + * @details Override to supply analytical Hessians. The base implementation + * throws, so a family that provides no Hessians reports it uniformly through + * every Hessian entry point. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param hessians_out Output span with at least size() entries. + * @throws BasisEvaluationException If Hessians are not available for the basis. + */ + virtual void evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const; + +protected: + /** + * @brief Evaluate any non-empty subset of values, gradients, and Hessians + * into caller-provided storage in a single pass. + * + * @details An empty span selects "skip that quantity". The base + * implementation forwards each requested quantity to its single-quantity span + * primitive; families that can share per-point setup override this to compute + * the requested quantities together. It backs the public evaluate_all() + * overload. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values_out Values output span, or empty to skip. + * @param gradients_out Gradients output span, or empty to skip. + * @param hessians_out Hessians output span, or empty to skip. + */ + virtual void evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const; + + /** + * @brief Approximate gradients by centered finite differences of values. + * + * @details This helper is primarily a verification utility for tests: it + * provides a basis-independent reference that checks a concrete basis's + * analytical evaluate_gradients() against centered finite differences of + * evaluate_values(). It lives on the base class so any BasisFunction can be + * checked uniformly, and having no production caller is by design — every + * shipped basis supplies analytical gradients. Centered differences add + * truncation/roundoff sensitivity and require multiple value evaluations + * per reference coordinate, so analytical gradients are always preferred + * outside this testing context. + */ + void numerical_gradient(const math::Vector& xi, + std::vector& gradients, + double eps = double(1e-6)) const; + + /** + * @brief Approximate Hessians by centered finite differences of gradients. + * + * @details Companion verification utility to numerical_gradient: it checks + * a basis's analytical evaluate_hessians() against centered finite + * differences of evaluate_gradients(). Because it differentiates gradients, + * it is only meaningful for bases that already provide them. Like + * numerical_gradient it is test-support rather than a production fallback — + * finite-difference Hessians amplify numerical error and require repeated + * gradient evaluations, so analytical Hessians are used everywhere outside + * tests. + */ + void numerical_hessian(const math::Vector& xi, + std::vector& hessians, + double eps = double(1e-5)) const; +}; + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_BASISFUNCTION_H diff --git a/Code/Source/solver/FE/Basis/BasisTraits.h b/Code/Source/solver/FE/Basis/BasisTraits.h new file mode 100644 index 000000000..c0ab1e5c5 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisTraits.h @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_BASISTRAITS_H +#define SVMP_FE_BASIS_BASISTRAITS_H + +/** + * @file BasisTraits.h + * @brief Reference-topology vocabulary (BasisTopology) and the internal + * ElementType/topology/order maps. + */ + +#include "Types.h" + +#include + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @brief Reference-cell topology of a basis (the shape, independent of order). + * @ingroup FE_Basis + * + * @details Together with a polynomial order this is the order-agnostic identity a + * basis is built from: the arbitrary-order constructors take a BasisTopology and an + * order, and BasisRequest::topology selects that path. A named ElementType maps to + * one of these through topology(). + */ +enum class BasisTopology { + Unknown, ///< Unrecognized or uninitialized topology. + Point, ///< 0D point. + Line, ///< 1D line segment. + Triangle, ///< 2D triangle (simplex). + Quadrilateral, ///< 2D quadrilateral (tensor product). + Tetrahedron, ///< 3D tetrahedron (simplex). + Hexahedron, ///< 3D hexahedron (tensor product). + Wedge, ///< 3D triangular prism. +}; + +// The maps below are internal to the Basis module (used to build the basis classes +// and the factory); they are excluded from the public Doxygen output. +/** @cond INTERNAL */ + +// --------------------------------------------------------------------------- +// ElementType / BasisTopology / order mapping helpers. +// +// A basis identity is expressed three ways -- a named ElementType, a +// (BasisTopology, order) pair, and a reference dimension -- and the constexpr +// maps below convert between them. They are grouped here so the relationships +// stay in one place: +// +// ElementType -> BasisTopology topology() +// ElementType -> ElementType canonical_lagrange_type() (alias -> linear representative) +// ElementType -> order complete_lagrange_alias_order(), named_lagrange_order() +// BasisTopology -> int (dimension) topology_dimension() +// BasisTopology -> ElementType lagrange_topology_representative() (lowest-order representative) +// (BasisTopology, order, family) -> ElementType named_element_for() (inverse of topology() + order()) +// +// The two ElementType -> order maps differ only at Point1: +// complete_lagrange_alias_order() returns -1 (a point is not a complete-Lagrange +// alias) while named_lagrange_order() returns 0 (the point layout's order). +// named_lagrange_order() is defined in terms of complete_lagrange_alias_order(), +// so the order-1 / order-2 alias values have a single source of truth. +// --------------------------------------------------------------------------- + +// Reference-cell topology is derived from the single mesh cell-family +// classification (to_mesh_family) so the basis layer never maintains a parallel +// ElementType->shape switch; adding an ElementType updates only to_mesh_family. +// ElementType::Unknown must stay Unknown here: CellFamily has no "unknown" +// member, so to_mesh_family() falls back to Point for unrecognized types. +[[nodiscard]] constexpr BasisTopology topology(ElementType type) noexcept { + if (type == ElementType::Unknown) { + return BasisTopology::Unknown; + } + switch (to_mesh_family(type)) { + case CellFamily::Point: return BasisTopology::Point; + case CellFamily::Line: return BasisTopology::Line; + case CellFamily::Triangle: return BasisTopology::Triangle; + case CellFamily::Quad: return BasisTopology::Quadrilateral; + case CellFamily::Tetra: return BasisTopology::Tetrahedron; + case CellFamily::Hex: return BasisTopology::Hexahedron; + case CellFamily::Wedge: return BasisTopology::Wedge; + // Pyramid/Polygon/Polyhedron are outside the current basis scope. + // BasisTopology::Unknown is a sentinel the basis constructors validate + // and convert into a BasisElementCompatibilityException at the call site, + // not an error raised from this constexpr noexcept classifier. + default: return BasisTopology::Unknown; + } +} + +[[nodiscard]] constexpr ElementType canonical_lagrange_type(ElementType type) noexcept { + switch (type) { + case ElementType::Line2: + case ElementType::Line3: + return ElementType::Line2; + case ElementType::Triangle3: + case ElementType::Triangle6: + return ElementType::Triangle3; + case ElementType::Quad4: + case ElementType::Quad9: + return ElementType::Quad4; + case ElementType::Tetra4: + case ElementType::Tetra10: + return ElementType::Tetra4; + case ElementType::Hex8: + case ElementType::Hex27: + return ElementType::Hex8; + case ElementType::Wedge6: + case ElementType::Wedge18: + return ElementType::Wedge6; + default: + return type; + } +} + +[[nodiscard]] constexpr int complete_lagrange_alias_order(ElementType type) noexcept { + switch (type) { + case ElementType::Line2: + case ElementType::Triangle3: + case ElementType::Quad4: + case ElementType::Tetra4: + case ElementType::Hex8: + case ElementType::Wedge6: + return 1; + case ElementType::Line3: + case ElementType::Triangle6: + case ElementType::Quad9: + case ElementType::Tetra10: + case ElementType::Hex27: + case ElementType::Wedge18: + return 2; + default: + // -1 is a sentinel for "not a complete-Lagrange alias" (serendipity + // layouts, pyramids, Unknown), not an error: the LagrangeBasis + // (ElementType, order) constructor compares the requested order + // against named_lagrange_order() and raises BasisConfigurationException + // on mismatch. These classifiers are constexpr noexcept and so cannot + // throw themselves. + return -1; + } +} + +// Reference-space dimension of a basis topology: 0 for points up to 3 for +// volume topologies; -1 for Unknown. +[[nodiscard]] constexpr int topology_dimension(BasisTopology top) noexcept { + switch (top) { + case BasisTopology::Point: return 0; + case BasisTopology::Line: return 1; + case BasisTopology::Triangle: + case BasisTopology::Quadrilateral: return 2; + case BasisTopology::Tetrahedron: + case BasisTopology::Hexahedron: + case BasisTopology::Wedge: return 3; + default: return -1; + } +} + +// Lowest-order named element that represents a topology. Used internally to +// drive the reference-node generators, which key on a canonical ElementType +// (and re-canonicalize it). This is the inverse of topology() for the linear +// elements and is purely an implementation detail: the node-count name never +// leaks into the public basis identity. +[[nodiscard]] constexpr ElementType lagrange_topology_representative(BasisTopology top) noexcept { + switch (top) { + case BasisTopology::Point: return ElementType::Point1; + case BasisTopology::Line: return ElementType::Line2; + case BasisTopology::Triangle: return ElementType::Triangle3; + case BasisTopology::Quadrilateral: return ElementType::Quad4; + case BasisTopology::Tetrahedron: return ElementType::Tetra4; + case BasisTopology::Hexahedron: return ElementType::Hex8; + case BasisTopology::Wedge: return ElementType::Wedge6; + default: return ElementType::Unknown; + } +} + +// Polynomial order baked into a named Lagrange element layout: 0 for the point, +// 1 for the linear elements, 2 for the complete-quadratic aliases; -1 for types +// with no complete-Lagrange order (serendipity, pyramid, Unknown). Unlike +// complete_lagrange_alias_order this also maps Point1 -> 0, so it is the single +// source of truth the (ElementType, order) constructor validates against. +[[nodiscard]] constexpr int named_lagrange_order(ElementType type) noexcept { + if (type == ElementType::Point1) { + return 0; + } + return complete_lagrange_alias_order(type); +} + +/** @endcond */ + +/** + * @brief Named ElementType denoted by a (topology, order, family) triple. + * @ingroup FE_Basis + * + * @details Inverse of topology() + order() for the named layouts: returns the + * ElementType a basis identity denotes, or ElementType::Unknown when no named + * layout exists (order 0 on a non-point topology, any order >= 3, or a reduced + * family at an unsupported order). topology() + order() remain the authoritative + * identity; callers that want a named ElementType for a basis pass its topology(), + * order(), and basis_type() here. + * + * @param top Reference topology. + * @param order Polynomial order. + * @param family Basis family; only Serendipity is distinguished from nodal/Lagrange naming. + * @return Named ElementType, or ElementType::Unknown when none applies. + */ +[[nodiscard]] constexpr ElementType named_element_for(BasisTopology top, int order, + BasisType family) noexcept { + if (family == BasisType::Serendipity) { + switch (top) { + case BasisTopology::Quadrilateral: + return order == 2 ? ElementType::Quad8 : ElementType::Unknown; + case BasisTopology::Hexahedron: + if (order == 1) { return ElementType::Hex8; } + if (order == 2) { return ElementType::Hex20; } + return ElementType::Unknown; + case BasisTopology::Wedge: + return order == 2 ? ElementType::Wedge15 : ElementType::Unknown; + default: + return ElementType::Unknown; + } + } + + // Lagrange (and any nodal family built on the complete layouts). + if (top == BasisTopology::Point) { + return order == 0 ? ElementType::Point1 : ElementType::Unknown; + } + switch (order) { + case 1: + switch (top) { + case BasisTopology::Line: return ElementType::Line2; + case BasisTopology::Triangle: return ElementType::Triangle3; + case BasisTopology::Quadrilateral: return ElementType::Quad4; + case BasisTopology::Tetrahedron: return ElementType::Tetra4; + case BasisTopology::Hexahedron: return ElementType::Hex8; + case BasisTopology::Wedge: return ElementType::Wedge6; + default: return ElementType::Unknown; + } + case 2: + switch (top) { + case BasisTopology::Line: return ElementType::Line3; + case BasisTopology::Triangle: return ElementType::Triangle6; + case BasisTopology::Quadrilateral: return ElementType::Quad9; + case BasisTopology::Tetrahedron: return ElementType::Tetra10; + case BasisTopology::Hexahedron: return ElementType::Hex27; + case BasisTopology::Wedge: return ElementType::Wedge18; + default: return ElementType::Unknown; + } + default: + return ElementType::Unknown; + } +} + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_BASISTRAITS_H diff --git a/Code/Source/solver/FE/Basis/LagrangeBasis.cpp b/Code/Source/solver/FE/Basis/LagrangeBasis.cpp new file mode 100644 index 000000000..58925171a --- /dev/null +++ b/Code/Source/solver/FE/Basis/LagrangeBasis.cpp @@ -0,0 +1,600 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "LagrangeBasis.h" +#include "NodeOrderingConventions.h" + +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +namespace { + +using Vec3 = math::Vector; + +struct AxisEval { + std::vector value; + std::vector first; + std::vector second; +}; + +struct SimplexEval { + std::vector value; + std::vector gradient; + std::vector hessian; +}; + +// Validate a named-element Lagrange request and return its reference topology. +// +// Serendipity layouts and pyramids are rejected. The requested order must equal +// the order baked into the named layout (0 for Point1, 1 for the linear +// elements, 2 for the complete-quadratic aliases Line3/Triangle6/Quad9/Tetra10/ +// Hex27/Wedge18). A named element therefore cannot carry a conflicting order; +// arbitrary orders are requested through the BasisTopology constructor, so a +// reader never has to read a polynomial order out of a node-count name such as +// Hex8 or Tetra10. +BasisTopology validated_lagrange_topology(ElementType element_type, int order) { + switch (element_type) { + case ElementType::Quad8: + svmp::raise("LagrangeBasis: Quad8 is serendipity; use SerendipityBasis"); + case ElementType::Hex20: + svmp::raise("LagrangeBasis: Hex20 is serendipity; use SerendipityBasis"); + case ElementType::Wedge15: + svmp::raise("LagrangeBasis: Wedge15 is serendipity; use SerendipityBasis"); + case ElementType::Pyramid5: + case ElementType::Pyramid13: + case ElementType::Pyramid14: + svmp::raise("LagrangeBasis: pyramid support is not within the current solver basis scope"); + default: + break; + } + + const BasisTopology top = topology(element_type); + svmp::throw_if(top == BasisTopology::Unknown, "LagrangeBasis: unsupported element type"); + + const int baked_order = named_lagrange_order(element_type); + svmp::throw_if(order != baked_order, "LagrangeBasis: a named element layout has a fixed polynomial order; request the matching " + "BasisTopology with an explicit order to choose a different order"); + return top; +} + +// Convert an integer lattice index (i, j[, k]) into the barycentric exponent +// tuple (order - i - j - k, i, j, k). The lattice already carries the exact +// coordinate indices, the accessor's structural invariants guarantee +// i + j + k <= order, hence e[0] >= 0. +LagrangeBasis::SimplexExponent simplex_exponent_from_lattice(const std::array& idx, + BasisTopology top, + int order) { + LagrangeBasis::SimplexExponent e{0, 0, 0, 0}; + e[1] = idx[0]; + e[2] = idx[1]; + if (top == BasisTopology::Tetrahedron) { + e[3] = idx[2]; + e[0] = order - idx[0] - idx[1] - idx[2]; + } else { + e[0] = order - idx[0] - idx[1]; + } + return e; +} + +// Evaluate 1D Lagrange polynomials and their derivatives at a point in the +// barycentric form l_i(x) = w_i * prod_{j!=i}(x - x_j), where the weights +// w_i = 1 / prod_{j!=i}(x_i - x_j) depend only on the fixed node set and are +// precomputed once by the caller. For each i the numerator and its first and +// second derivatives are built by a single product-rule accumulation over the +// remaining nodes. +void evaluate_1d_lagrange(double x, + const std::vector& nodes, + const std::vector& weights, + AxisEval& out, + int level) { + const std::size_t n = nodes.size(); + out.value.assign(n, double(0)); + out.first.assign(n, double(0)); + out.second.assign(n, double(0)); + + for (std::size_t i = 0; i < n; ++i) { + double value = double(1); + double first = double(0); + double second = double(0); + for (std::size_t j = 0; j < n; ++j) { + if (j == i) { + continue; + } + const double f = x - nodes[j]; + if (level >= 2) { + second = second * f + double(2) * first; + } + if (level >= 1) { + first = first * f + value; + } + value = value * f; + } + + const double w = weights[i]; + out.value[i] = value * w; + if (level >= 1) { + out.first[i] = first * w; + } + if (level >= 2) { + out.second[i] = second * w; + } + } +} + +// Evaluate one barycentric polynomial factor and its derivatives. `level` +// selects how far the recurrence runs: 0 for the value only, 1 to also produce +// the first derivative, and 2 to also produce the second. +std::array simplex_factor(int alpha, double lambda, int order, int level) { + double value = double(1); + double first = double(0); + double second = double(0); + + for (int m = 0; m < alpha; ++m) { + const double factor = double(order) * lambda - double(m); + const double inv = double(1) / double(m + 1); + const double old_value = value; + const double old_first = first; + const double old_second = second; + value = old_value * factor * inv; + if (level >= 1) { + first = (old_first * factor + old_value * double(order)) * inv; + } + if (level >= 2) { + second = (old_second * factor + double(2) * old_first * double(order)) * inv; + } + } + + return {value, first, second}; +} + +// Evaluate simplex Lagrange basis functions and the requested derivatives. +// Gradients and Hessians are assembled only when asked for; `out.gradient` and +// `out.hessian` are left empty otherwise so a values-only request neither +// allocates those buffers nor runs the derivative loops. +void evaluate_simplex(const Vec3& xi, + BasisTopology top, + int order, + const std::vector& exponents, + SimplexEval& out, + bool want_gradient, + bool want_hessian) { + const std::size_t n = exponents.size(); + out.value.assign(n, double(0)); + out.gradient.assign(want_gradient ? n : std::size_t{0}, Gradient::Zero()); + out.hessian.assign(want_hessian ? n : std::size_t{0}, Hessian::Zero()); + + if (n == 1u && order == 0) { + out.value[0] = double(1); + return; + } + + // A Hessian factor also needs the first-derivative recurrence, so the + // per-factor work runs to the highest requested order. + const int factor_level = want_hessian ? 2 : (want_gradient ? 1 : 0); + + const std::size_t bary_count = top == BasisTopology::Triangle ? 3u : 4u; + std::array lambda{double(0), double(0), double(0), double(0)}; + std::array lambda_grad; + lambda_grad.fill(Gradient::Zero()); + + lambda[1] = xi[0]; + lambda[2] = xi[1]; + lambda_grad[1][0] = double(1); + lambda_grad[2][1] = double(1); + if (top == BasisTopology::Triangle) { + lambda[0] = double(1) - xi[0] - xi[1]; + lambda_grad[0][0] = double(-1); + lambda_grad[0][1] = double(-1); + } else { + lambda[3] = xi[2]; + lambda[0] = double(1) - xi[0] - xi[1] - xi[2]; + lambda_grad[0][0] = double(-1); + lambda_grad[0][1] = double(-1); + lambda_grad[0][2] = double(-1); + lambda_grad[3][2] = double(1); + } + + for (std::size_t i = 0; i < n; ++i) { + std::array, 4> f{}; + for (std::size_t a = 0; a < bary_count; ++a) { + f[a] = simplex_factor(exponents[i][a], lambda[a], order, factor_level); + } + + double value = double(1); + for (std::size_t a = 0; a < bary_count; ++a) { + value *= f[a][0]; + } + out.value[i] = value; + + if (want_gradient) { + for (std::size_t a = 0; a < bary_count; ++a) { + double product = f[a][1]; + for (std::size_t b = 0; b < bary_count; ++b) { + if (b != a) { + product *= f[b][0]; + } + } + for (std::size_t c = 0; c < 3u; ++c) { + out.gradient[i][c] += product * lambda_grad[a][c]; + } + } + } + + if (want_hessian) { + for (std::size_t a = 0; a < bary_count; ++a) { + for (std::size_t b = 0; b < bary_count; ++b) { + double product = (a == b) ? f[a][2] : f[a][1] * f[b][1]; + for (std::size_t k = 0; k < bary_count; ++k) { + if (k != a && k != b) { + product *= f[k][0]; + } + } + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + out.hessian[i](r, c) += + product * lambda_grad[a][r] * lambda_grad[b][c]; + } + } + } + } + } + } +} + +} // namespace + +LagrangeBasis::LagrangeBasis(BasisTopology topology, int order) + : topology_(topology), order_(order) { + svmp::throw_if(topology_ == BasisTopology::Unknown, "LagrangeBasis: unknown reference topology"); + svmp::throw_if(order_ < 0, "LagrangeBasis requires non-negative polynomial order"); + svmp::throw_if( + topology_ == BasisTopology::Point && order_ != 0, "LagrangeBasis: Point topology supports order 0 only"); + dimension_ = topology_dimension(topology_); + init_nodes(); +} + +LagrangeBasis::LagrangeBasis(ElementType type, int order) + : LagrangeBasis(validated_lagrange_topology(type, order), order) {} + +LagrangeBasis::LagrangeBasis(ElementType type) + : LagrangeBasis(type, named_lagrange_order(type)) {} + +// Initialize the 1D tensor-axis interpolation nodes (Gauss-Lobatto-Legendre, via +// line_coord_pm_one) and their barycentric weights for tensor-product axes. +void LagrangeBasis::init_tensor_axis_nodes() { + const std::size_t n = static_cast(order_ + 1); + nodes_1d_.resize(n); + for (int i = 0; i <= order_; ++i) { + nodes_1d_[static_cast(i)] = + line_coord_pm_one(i, order_); + } + + // Barycentric weights w_i = 1 / prod_{j!=i}(x_i - x_j); the nodes are + // distinct so every denominator is nonzero. + nodes_1d_weights_.assign(n, double(1)); + for (std::size_t i = 0; i < n; ++i) { + double denom = double(1); + for (std::size_t j = 0; j < n; ++j) { + if (j != i) { + denom *= nodes_1d_[i] - nodes_1d_[j]; + } + } + nodes_1d_weights_[i] = double(1) / denom; + } +} + +// Initialize reference nodes and topology-specific lookup data. +void LagrangeBasis::init_nodes() { + nodes_.clear(); + nodes_1d_.clear(); + nodes_1d_weights_.clear(); + tensor_indices_.clear(); + simplex_exponents_.clear(); + wedge_indices_.clear(); + + switch (topology_) { + case BasisTopology::Point: + build_point_nodes(); + return; + case BasisTopology::Line: + case BasisTopology::Quadrilateral: + case BasisTopology::Hexahedron: + build_tensor_product_nodes(); + return; + case BasisTopology::Triangle: + case BasisTopology::Tetrahedron: + build_simplex_nodes(); + return; + case BasisTopology::Wedge: + build_wedge_nodes(); + return; + default: + break; + } + + svmp::raise("Unsupported element type in LagrangeBasis::init_nodes"); +} + +// Build the single reference node for a point basis. +void LagrangeBasis::build_point_nodes() { + nodes_.push_back(Vec3{double(0), double(0), double(0)}); +} + +// Build nodes and axis indices for tensor-product elements. +void LagrangeBasis::build_tensor_product_nodes() { + init_tensor_axis_nodes(); + const auto layout = + ReferenceNodeLayout::get_lagrange_lattice(lagrange_topology_representative(topology_), order_); + nodes_ = layout.coords; + tensor_indices_.reserve(layout.lattice.size()); + for (const auto& idx : layout.lattice) { + // The lattice already holds the per-axis node index 0..order along each + // axis (unused axes are zero; the coordinate itself is the GLL node for + // that index), so no coordinate-to-index inversion is needed. + tensor_indices_.push_back(TensorNodeIndex{ + static_cast(idx[0]), + static_cast(idx[1]), + static_cast(idx[2])}); + } +} + +// Build nodes and barycentric exponents for simplex elements. +void LagrangeBasis::build_simplex_nodes() { + const auto layout = + ReferenceNodeLayout::get_lagrange_lattice(lagrange_topology_representative(topology_), order_); + nodes_ = layout.coords; + simplex_exponents_.reserve(layout.lattice.size()); + for (const auto& idx : layout.lattice) { + simplex_exponents_.push_back(simplex_exponent_from_lattice(idx, topology_, order_)); + } +} + +// Build nodes and mixed triangle-axis lookup data for wedge elements. +void LagrangeBasis::build_wedge_nodes() { + init_tensor_axis_nodes(); + const auto layout = + ReferenceNodeLayout::get_lagrange_lattice(lagrange_topology_representative(topology_), order_); + nodes_ = layout.coords; + + const auto tri_layout = + ReferenceNodeLayout::get_lagrange_lattice(ElementType::Triangle3, order_); + simplex_exponents_.reserve(tri_layout.lattice.size()); + for (const auto& idx : tri_layout.lattice) { + simplex_exponents_.push_back( + simplex_exponent_from_lattice(idx, BasisTopology::Triangle, order_)); + } + + // Map a triangle cross-section lattice (i, j) to its triangle-node ordinal + // through the flat key i * (order + 1) + j, so each wedge node's triangle + // index is an exact integer lookup. + const int stride = order_ + 1; + std::vector tri_ordinal_for_key(static_cast(stride * stride), -1); + for (std::size_t t = 0; t < tri_layout.lattice.size(); ++t) { + const auto& idx = tri_layout.lattice[t]; + tri_ordinal_for_key[static_cast(idx[0] * stride + idx[1])] = + static_cast(t); + } + + wedge_indices_.reserve(layout.lattice.size()); + for (const auto& idx : layout.lattice) { + const int tri_ordinal = + tri_ordinal_for_key[static_cast(idx[0] * stride + idx[1])]; + svmp::throw_if(tri_ordinal < 0, "LagrangeBasis: wedge node triangle index lookup failed"); + wedge_indices_.push_back({static_cast(tri_ordinal), + static_cast(idx[2])}); + } +} + +// Evaluate the constant point basis. +void LagrangeBasis::evaluate_point_to(std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + if (!values_out.empty()) { + values_out[0] = double(1); + } + if (!gradients_out.empty()) { + gradients_out[0] = Gradient::Zero(); + } + if (!hessians_out.empty()) { + hessians_out[0] = Hessian::Zero(); + } +} + +// Evaluate line, quadrilateral, and hexahedron bases as axis-polynomial products. +void LagrangeBasis::evaluate_tensor_product_to(const Vec3& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + const int level = !hessians_out.empty() ? 2 : (!gradients_out.empty() ? 1 : 0); + + AxisEval ax; + AxisEval ay; + AxisEval az; + evaluate_1d_lagrange(xi[0], nodes_1d_, nodes_1d_weights_, ax, level); + if (dimension_ >= 2) { + evaluate_1d_lagrange(xi[1], nodes_1d_, nodes_1d_weights_, ay, level); + } + if (dimension_ >= 3) { + evaluate_1d_lagrange(xi[2], nodes_1d_, nodes_1d_weights_, az, level); + } + + for (std::size_t node = 0; node < tensor_indices_.size(); ++node) { + const auto& idx = tensor_indices_[node]; + const double vx = ax.value[idx[0]]; + const double dx = ax.first[idx[0]]; + const double d2x = ax.second[idx[0]]; + const double vy = dimension_ >= 2 ? ay.value[idx[1]] : double(1); + const double dy = dimension_ >= 2 ? ay.first[idx[1]] : double(0); + const double d2y = dimension_ >= 2 ? ay.second[idx[1]] : double(0); + const double vz = dimension_ >= 3 ? az.value[idx[2]] : double(1); + const double dz = dimension_ >= 3 ? az.first[idx[2]] : double(0); + const double d2z = dimension_ >= 3 ? az.second[idx[2]] : double(0); + + if (!values_out.empty()) { + values_out[node] = vx * vy * vz; + } + if (!gradients_out.empty()) { + Gradient& g = gradients_out[node]; + g[0] = dx * vy * vz; + g[1] = vx * dy * vz; + g[2] = vx * vy * dz; + } + if (!hessians_out.empty()) { + Hessian& h = hessians_out[node]; + h(0, 0) = d2x * vy * vz; + h(0, 1) = dx * dy * vz; + h(0, 2) = dx * vy * dz; + h(1, 0) = h(0, 1); + h(1, 1) = vx * d2y * vz; + h(1, 2) = vx * dy * dz; + h(2, 0) = h(0, 2); + h(2, 1) = h(1, 2); + h(2, 2) = vx * vy * d2z; + } + } +} + +// Evaluate triangle and tetrahedron bases from barycentric factors. +void LagrangeBasis::evaluate_simplex_to(const Vec3& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + const bool want_values = !values_out.empty(); + const bool want_gradients = !gradients_out.empty(); + const bool want_hessians = !hessians_out.empty(); + + SimplexEval simplex; + evaluate_simplex(xi, topology_, order_, simplex_exponents_, simplex, + want_gradients, want_hessians); + for (std::size_t i = 0; i < simplex.value.size(); ++i) { + if (want_values) { + values_out[i] = simplex.value[i]; + } + if (want_gradients) { + gradients_out[i] = simplex.gradient[i]; + } + if (want_hessians) { + hessians_out[i] = simplex.hessian[i]; + } + } +} + +// Evaluate wedge bases as triangle/through-axis products. +void LagrangeBasis::evaluate_wedge_to(const Vec3& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + const bool want_values = !values_out.empty(); + const bool want_gradients = !gradients_out.empty(); + const bool want_hessians = !hessians_out.empty(); + + // The wedge gradient pairs the triangle gradient with the through-axis value, + // and the wedge Hessian reuses the triangle gradient for its mixed terms, so + // the triangle factor must supply gradients whenever the wedge needs either + // gradients or Hessians. + const bool want_tri_gradient = want_gradients || want_hessians; + const int z_level = want_hessians ? 2 : (want_gradients ? 1 : 0); + + SimplexEval tri; + AxisEval z_axis; + evaluate_simplex(xi, BasisTopology::Triangle, order_, simplex_exponents_, tri, + want_tri_gradient, want_hessians); + evaluate_1d_lagrange(xi[2], nodes_1d_, nodes_1d_weights_, z_axis, z_level); + + for (std::size_t node = 0; node < wedge_indices_.size(); ++node) { + const auto [tri_idx, z_idx] = wedge_indices_[node]; + const double tv = tri.value[tri_idx]; + const double zv = z_axis.value[z_idx]; + + if (want_values) { + values_out[node] = tv * zv; + } + if (want_gradients) { + const double dz = z_axis.first[z_idx]; + Gradient& g = gradients_out[node]; + g[0] = tri.gradient[tri_idx][0] * zv; + g[1] = tri.gradient[tri_idx][1] * zv; + g[2] = tv * dz; + } + if (want_hessians) { + const double dz = z_axis.first[z_idx]; + const double d2z = z_axis.second[z_idx]; + Hessian& h = hessians_out[node]; + const Hessian& th = tri.hessian[tri_idx]; + const Gradient& tg = tri.gradient[tri_idx]; + h(0, 0) = th(0, 0) * zv; + h(0, 1) = th(0, 1) * zv; + h(0, 2) = tg[0] * dz; + h(1, 0) = h(0, 1); + h(1, 1) = th(1, 1) * zv; + h(1, 2) = tg[1] * dz; + h(2, 0) = h(0, 2); + h(2, 1) = h(1, 2); + h(2, 2) = tv * d2z; + } + } +} + +// Evaluate requested basis quantities into caller-provided spans. +void LagrangeBasis::evaluate_all_to(const Vec3& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + // Private sink: callers guarantee valid output spans -- the public *_to methods + // validate their one output with require_span_size, and the vector evaluators + // resize to size(). An empty span here means "skip that quantity". + + if (values_out.empty() && gradients_out.empty() && hessians_out.empty()) { + return; + } + + switch (topology_) { + case BasisTopology::Point: + evaluate_point_to(values_out, gradients_out, hessians_out); + return; + case BasisTopology::Line: + case BasisTopology::Quadrilateral: + case BasisTopology::Hexahedron: + evaluate_tensor_product_to(xi, values_out, gradients_out, hessians_out); + return; + case BasisTopology::Triangle: + case BasisTopology::Tetrahedron: + evaluate_simplex_to(xi, values_out, gradients_out, hessians_out); + return; + case BasisTopology::Wedge: + evaluate_wedge_to(xi, values_out, gradients_out, hessians_out); + return; + default: + break; + } + + svmp::raise("Unsupported element in LagrangeBasis evaluation"); +} + +void LagrangeBasis::evaluate_values_to(const Vec3& xi, + std::span values_out) const { + require_span_size(values_out.size(), size(), "LagrangeBasis::evaluate_values_to"); + evaluate_all_to(xi, values_out, std::span{}, std::span{}); +} + +void LagrangeBasis::evaluate_gradients_to(const Vec3& xi, + std::span gradients_out) const { + require_span_size(gradients_out.size(), size(), "LagrangeBasis::evaluate_gradients_to"); + evaluate_all_to(xi, std::span{}, gradients_out, std::span{}); +} + +void LagrangeBasis::evaluate_hessians_to(const Vec3& xi, + std::span hessians_out) const { + require_span_size(hessians_out.size(), size(), "LagrangeBasis::evaluate_hessians_to"); + evaluate_all_to(xi, std::span{}, std::span{}, hessians_out); +} + +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Basis/LagrangeBasis.h b/Code/Source/solver/FE/Basis/LagrangeBasis.h new file mode 100644 index 000000000..001960675 --- /dev/null +++ b/Code/Source/solver/FE/Basis/LagrangeBasis.h @@ -0,0 +1,292 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_LAGRANGEBASIS_H +#define SVMP_FE_BASIS_LAGRANGEBASIS_H + +#include "BasisFunction.h" +#include "BasisTraits.h" + +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @defgroup FE_LagrangeBasis LagrangeBasis + * @ingroup FE_Basis + * @brief Construction and evaluation API for nodal Lagrange finite-element bases. + * + * @details This group documents the complete nodal Lagrange basis evaluator + * used by the FE library. The implementation covers tensor-product, + * simplex, and wedge reference topologies with exact analytical first and + * second derivatives in reference coordinates. + * @{ + */ + +/** + * @brief Nodal Lagrange basis on supported reference finite elements. + * + * @details LagrangeBasis represents the complete (full-degree) nodal + * interpolation basis on a reference topology. It supports point, line, + * quadrilateral, hexahedron, triangle, tetrahedron, and wedge reference + * topologies. The primary constructor takes a BasisTopology and an explicit + * polynomial order, so an arbitrary order carries no node-count assumption + * (an order-2 hexahedron is BasisTopology::Hexahedron with order 2). A named + * ElementType such as Line3, Quad9, Tetra10, or Hex27 is a fixed-order + * shorthand: it maps to the same (topology, order) pair and the requested order + * must equal the order baked into that layout (1 for the linear elements, 2 for + * the complete-quadratic aliases, 0 for Point1). + * + * ## Reference-node distribution + * + * The interpolation nodes are not a single distribution across topologies; each + * family uses the node set its evaluator is built for: + * - **Tensor-product (line, quadrilateral, hexahedron):** the shared + * Gauss-Lobatto-Legendre (GLL) tensor-axis nodes -- see line_coord_pm_one for + * the distribution and its conditioning -- not an equispaced layout. + * - **Simplex (triangle, tetrahedron):** the equispaced barycentric lattice + * (each barycentric coordinate at @f$i/p@f$). The closed-form evaluator below + * is specific to this equispaced lattice. + * - **Wedge:** the tensor product of an equispaced triangle cross-section with a + * GLL through-axis. + * + * Because GLL coincides with the equispaced layout at orders 1 and 2 + * (line_coord_pm_one), the linear and quadratic tensor elements -- Line2/Line3, + * Quad4/Quad9, Hex8/Hex27, and the wedge through-axis -- are built on equispaced + * nodes, and the GLL/equispaced distinction appears only for order >= 3. + * + * ## Evaluation + * + * Tensor-product elements use the one-dimensional nodal polynomials + * @f[ + * l_i(x) = \prod_{j \ne i} \frac{x - x_j}{x_i - x_j} + * @f] + * on the per-axis GLL coordinates @f$x_j \in [-1, 1]@f$ (the barycentric-weight + * form, valid for any distinct node set). Multi-dimensional basis functions are + * products of the active axis polynomials, for example + * @f$N_{ijk}(r,s,t) = l_i(r)l_j(s)l_k(t)@f$ on a hexahedron. + * + * Simplex elements use barycentric coordinates and integer lattice + * exponents on the equispaced lattice. For a node with exponent tuple + * @f$\alpha@f$, where + * @f$\sum_a \alpha_a = p@f$, the basis is assembled from scaled + * falling-factorial factors, + * @f[ + * N_\alpha(\lambda) = + * \prod_a \prod_{m=0}^{\alpha_a-1} + * \frac{p\lambda_a - m}{m + 1}. + * @f] + * Gradients and Hessians are evaluated analytically by differentiating these + * factors and applying the barycentric-coordinate chain rule. + * + * Wedge elements are treated as a tensor product between a triangle simplex + * basis and a one-dimensional through-axis basis: + * @f$N_{a k}(r,s,t) = T_a(r,s)l_k(t)@f$. + * + * ## Conditioning and the supported order range + * + * Interpolation conditioning is governed by the node distribution and so differs + * by topology: + * - **Tensor-product topologies stay well-conditioned at high order.** GLL nodes + * have a logarithmic Lebesgue constant, so line/quadrilateral/hexahedron bases + * remain trustworthy well beyond the production orders. + * - **Simplex topologies degrade at high order.** The equispaced barycentric + * lattice has a Lebesgue constant that grows roughly exponentially with order + * (the Runge phenomenon), so triangle and tetrahedron bases are reliable + * through low orders but become increasingly ill-conditioned beyond them. The + * wedge inherits this through its equispaced triangle cross-section. + * + * The vector-returning evaluators are convenient API wrappers. The `*_to` + * methods write to caller-provided spans and are intended for assembly paths + * that avoid temporary allocations. + */ +class LagrangeBasis final : public BasisFunction { +public: + /** @brief Axis-index tuple for tensor-product reference nodes. */ + using TensorNodeIndex = std::array; + + /** @brief Barycentric exponent tuple for simplex reference nodes. */ + using SimplexExponent = std::array; + + /** @brief Triangle-node and axis-node tuple for wedge reference nodes. */ + using WedgeNodeIndex = std::array; + + /** + * @brief Construct a Lagrange basis on a reference topology at a polynomial order. + * + * @details This is the primary, arbitrary-order entry point: a BasisTopology + * carries no node-count assumption, so any supported order is requested + * explicitly (e.g. an order-5 hexahedron is BasisTopology::Hexahedron with + * order 5). The constructor builds the reference node coordinates and the + * topology-specific lookup data used by evaluation. Tensor-product bases + * store per-axis node indices, simplex bases store barycentric exponent + * tuples, and wedge bases store the triangle-node/axis-node decomposition. + * + * Reference nodes follow the per-topology distribution described in the class + * documentation (Reference-node distribution). Unlike SerendipityBasis, this + * constructor does not reject ill-conditioned high-order simplex/wedge requests + * (where the equispaced barycentric lattice degrades); that choice is the + * caller's. + * + * @param topology Reference topology; Point through the volume topologies. + * @param order Polynomial order; must be non-negative. Point is order 0. + * @throws BasisConfigurationException If the order is negative, or if Point + * is requested with a nonzero order. + * @throws BasisElementCompatibilityException If the topology is Unknown. + */ + LagrangeBasis(BasisTopology topology, int order); + + /** + * @brief Construct a Lagrange basis from a named element layout. + * + * @details Convenience overload for a named mesh element. The order is baked + * into the layout (0 for Point1, 1 for the linear elements, 2 for the + * complete-quadratic aliases such as Hex27/Tetra10) and the requested + * @p order must match it; arbitrary orders must be requested through the + * BasisTopology overload. Serendipity and pyramid layouts are rejected. + * + * @param type Named element type used to determine topology and baked-in order. + * @param order Requested order; must equal the element's baked-in order. + * @throws BasisConfigurationException If @p order does not match the element's baked-in order. + * @throws BasisElementCompatibilityException If the element type is unsupported. + */ + LagrangeBasis(ElementType type, int order); + + /** + * @brief Construct a Lagrange basis from a named element layout at its baked-in order. + * + * @details Single-argument convenience overload: the polynomial order is the + * one baked into the layout (0 for Point1, 1 for the linear elements, 2 for + * the complete-quadratic aliases such as Hex27/Tetra10), so the caller does + * not repeat it. Equivalent to LagrangeBasis(type, ). + * Serendipity and pyramid layouts are rejected, as for the two-argument + * overload. + * + * @param type Named element type; determines both topology and order. + * @throws BasisElementCompatibilityException If the element type is unsupported. + */ + explicit LagrangeBasis(ElementType type); + + /** @copydoc BasisFunction::basis_type() */ + BasisType basis_type() const noexcept final { return BasisType::Lagrange; } + + /** @copydoc BasisFunction::topology() */ + BasisTopology topology() const noexcept final { return topology_; } + + /** @copydoc BasisFunction::dimension() */ + int dimension() const noexcept final { return dimension_; } + + /** @copydoc BasisFunction::order() */ + int order() const noexcept final { return order_; } + + /** @copydoc BasisFunction::size() */ + std::size_t size() const noexcept final { return nodes_.size(); } + + /** + * @brief Return the reference interpolation nodes in basis ordering. + * + * @details The returned node order matches the basis-function order used by + * all evaluators; the coordinates follow the per-topology distribution + * described in the class documentation (Reference-node distribution). + * + * @return Reference node coordinates, one per basis function. + */ + const std::vector>& nodes() const noexcept final { return nodes_; } + + /** + * @brief Evaluate Lagrange basis values into caller-provided storage. + * + * @details This is the low-allocation API intended for element assembly + * loops. The span is filled in basis-node order and no vector resizing is + * performed. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values_out Output span with at least size() entries. + */ + void evaluate_values_to(const math::Vector& xi, + std::span values_out) const final; + + /** + * @brief Evaluate Lagrange basis gradients into caller-provided storage. + * + * @details Gradients are written in basis-node order with one + * three-component gradient per node. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param gradients_out Output span with at least size() entries. + */ + void evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const final; + + /** + * @brief Evaluate Lagrange basis Hessians into caller-provided storage. + * + * @details Hessians are written in basis-node order with one 3-by-3 + * Hessian per node. + * + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param hessians_out Output span with at least size() entries. + */ + void evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const final; + +private: + BasisTopology topology_{BasisTopology::Unknown}; + int dimension_{0}; + int order_{0}; + + // Topology-specific construction data. nodes_ (the reference nodes in basis + // order) is populated for every topology and backs size(); each remaining + // vector is filled only for the topologies that use it and stays empty + // otherwise: + // line/quad/hex : nodes_1d_, nodes_1d_weights_, tensor_indices_ + // triangle/tetra: simplex_exponents_ + // wedge : nodes_1d_, nodes_1d_weights_, wedge_indices_, and + // simplex_exponents_ (the triangle cross-section exponents) + // point : nodes_ only + std::vector nodes_1d_; + std::vector nodes_1d_weights_; + std::vector> nodes_; + std::vector tensor_indices_; + std::vector simplex_exponents_; + std::vector wedge_indices_; + + void init_nodes(); + void build_point_nodes(); + void build_tensor_product_nodes(); + void build_simplex_nodes(); + void build_wedge_nodes(); + void init_tensor_axis_nodes(); + + void evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const override; + void evaluate_point_to(std::span values_out, + std::span gradients_out, + std::span hessians_out) const; + void evaluate_tensor_product_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const; + void evaluate_simplex_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const; + void evaluate_wedge_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const; +}; + +/** @} */ + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_LAGRANGEBASIS_H diff --git a/Code/Source/solver/FE/Basis/NodeOrderingConventions.cpp b/Code/Source/solver/FE/Basis/NodeOrderingConventions.cpp new file mode 100644 index 000000000..856406de3 --- /dev/null +++ b/Code/Source/solver/FE/Basis/NodeOrderingConventions.cpp @@ -0,0 +1,885 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "NodeOrderingConventions.h" +#include "BasisExceptions.h" +#include "BasisTraits.h" + +#include +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +// Internal to the Basis module; excluded from the public Doxygen output (the +// matching conditional region is in NodeOrderingConventions.h). +/** @cond INTERNAL */ + +namespace { + +using Point = math::Vector; +using Lattice = std::array; + +// Gauss-Lobatto-Legendre nodes on [-1, 1] for a degree-`order` distribution +// (order + 1 nodes). The endpoints are -1 and +1; the interior nodes are the +// roots of P'_order, found by Newton iteration on f(x) = x P_order(x) - +// P_{order-1}(x) -- whose roots are exactly the GLL nodes -- from the +// Chebyshev-Gauss-Lobatto seed. +const std::vector& gll_points(int order) { + thread_local std::map> cache; + const auto found = cache.find(order); + if (found != cache.end()) { + return found->second; + } + + // Newton converges quadratically from the Chebyshev-Gauss-Lobatto seed, so a + // few iterations suffice for any practical order; the cap is only a safety + // bound. Reaching it without meeting the tolerance signals a real failure + constexpr int kMaxNewtonIterations = 100; + constexpr double kNewtonTolerance = double(1e-15); + + std::vector pts(static_cast(order + 1), double(0)); + if (order >= 1) { + pts.front() = double(-1); + pts.back() = double(1); + } + const double pi = std::acos(double(-1)); + const int half = order / 2; + for (int j = 1; j <= half; ++j) { + if (2 * j == order) { + pts[static_cast(j)] = double(0); // exact center, even order + continue; + } + double x = -std::cos(pi * static_cast(j) / static_cast(order)); + bool converged = false; + for (int iter = 0; iter < kMaxNewtonIterations; ++iter) { + // Legendre P_k and P'_k up to k = order at x, by the three-term + // recurrences (regular at x = +/-1). + double p_km1 = double(1); // P_0 + double p_k = x; // P_1 + double d_km1 = double(0); // P'_0 + double d_k = double(1); // P'_1 + for (int k = 1; k < order; ++k) { + const double kk = static_cast(k); + const double inv = double(1) / (kk + double(1)); + const double p_kp1 = + ((double(2) * kk + double(1)) * x * p_k - kk * p_km1) * inv; + const double d_kp1 = + ((double(2) * kk + double(1)) * (p_k + x * d_k) - kk * d_km1) * inv; + p_km1 = p_k; + p_k = p_kp1; + d_km1 = d_k; + d_k = d_kp1; + } + // p_k = P_order, p_km1 = P_{order-1}, d_k = P'_order, d_km1 = P'_{order-1}. + const double f = x * p_k - p_km1; + const double f_prime = p_k + x * d_k - d_km1; + const double dx = f / f_prime; + x -= dx; + if (std::abs(dx) <= kNewtonTolerance) { + converged = true; + break; + } + } + svmp::throw_if( + !converged, "ReferenceNodeLayout: Gauss-Lobatto-Legendre Newton iteration did not converge " + "(order outside the trustworthy range)"); + pts[static_cast(j)] = x; + } + for (int j = half + 1; j < order; ++j) { + pts[static_cast(j)] = -pts[static_cast(order - j)]; + } + + auto inserted = cache.emplace(order, std::move(pts)); + return inserted.first->second; +} + +double line_coord_zero_one(int i, int order) { + if (order <= 0) { + return double(0); + } + return static_cast(i) / static_cast(order); +} + +// Interpolate an integer lattice index along an edge between two corner +// vertices: index = (LA * (order - m) + LB * m) / order. The division is exact +// because edge endpoints are element corners (each component is 0 or order), so +// the result is the integer lattice point at parameter m / order. +Lattice lerp_lattice(const Lattice& a, const Lattice& b, int m, int order) { + Lattice result{0, 0, 0}; + for (std::size_t d = 0; d < 3u; ++d) { + const int numerator = a[d] * (order - m) + b[d] * m; + svmp::throw_if( + numerator % order != 0, "ReferenceNodeLayout: non-integral edge lattice index"); + result[d] = numerator / order; + } + return result; +} + +// Barycentric combination of three corner lattice indices for a triangular +// face-interior node: index = (a * L0 + b * L1 + c * L2) / order, with +// a + b + c == order. Exact for corner inputs (components 0 or order). +Lattice combine_lattice(const Lattice& l0, const Lattice& l1, const Lattice& l2, + int a, int b, int c, int order) { + Lattice result{0, 0, 0}; + for (std::size_t d = 0; d < 3u; ++d) { + const int numerator = a * l0[d] + b * l1[d] + c * l2[d]; + svmp::throw_if( + numerator % order != 0, "ReferenceNodeLayout: non-integral face-interior lattice index"); + result[d] = numerator / order; + } + return result; +} + +// Append the interior nodes of a triangular face spanned by v0, v1, v2 (with +// matching corner lattice indices l0, l1, l2), emitting both the coordinate and +// its integer lattice index. Shared by triangle interiors, tetra faces, and the +// two wedge caps. +void append_triangle_face_interior(LagrangeNodeLayout& out, + const Point& v0, + const Point& v1, + const Point& v2, + const Lattice& l0, + const Lattice& l1, + const Lattice& l2, + int order) { + for (int c = 1; c <= order - 2; ++c) { + for (int b = 1; b <= order - c - 1; ++b) { + const int a = order - b - c; + const double inv = double(1) / double(order); + out.coords.push_back(v0 * (double(a) * inv) + + v1 * (double(b) * inv) + + v2 * (double(c) * inv)); + out.lattice.push_back(combine_lattice(l0, l1, l2, a, b, c, order)); + } + } +} + +// One-node layout for the order-0 (constant) basis: the element centroid carried +// with the origin lattice index. Shared by every generator's order-0 branch. +LagrangeNodeLayout single_node_layout(const Point& centroid) { + LagrangeNodeLayout out; + out.coords.push_back(centroid); + out.lattice.push_back(Lattice{0, 0, 0}); + return out; +} + +// Append the element corner vertices (coordinate paired with lattice index) in +// the given order. Shared by the volume generators, which all open with the same +// corner loop. +void append_vertices(LagrangeNodeLayout& out, + std::span verts, + std::span vert_lattice) { + for (std::size_t v = 0; v < verts.size(); ++v) { + out.coords.push_back(verts[v]); + out.lattice.push_back(vert_lattice[v]); + } +} + +LagrangeNodeLayout generate_line_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(0), double(0), double(0)}); + } + + out.coords.reserve(static_cast(order + 1)); + out.lattice.reserve(static_cast(order + 1)); + out.coords.push_back(Point{double(-1), double(0), double(0)}); + out.lattice.push_back(Lattice{0, 0, 0}); + out.coords.push_back(Point{double(1), double(0), double(0)}); + out.lattice.push_back(Lattice{order, 0, 0}); + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), double(0), double(0)}); + out.lattice.push_back(Lattice{i, 0, 0}); + } + return out; +} + +LagrangeNodeLayout generate_triangle_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(1) / double(3), double(1) / double(3), double(0)}); + } + + out.coords.reserve(static_cast((order + 1) * (order + 2) / 2)); + out.lattice.reserve(static_cast((order + 1) * (order + 2) / 2)); + out.coords.push_back(Point{double(0), double(0), double(0)}); + out.lattice.push_back(Lattice{0, 0, 0}); + out.coords.push_back(Point{double(1), double(0), double(0)}); + out.lattice.push_back(Lattice{order, 0, 0}); + out.coords.push_back(Point{double(0), double(1), double(0)}); + out.lattice.push_back(Lattice{0, order, 0}); + + for (int m = 1; m < order; ++m) { + out.coords.push_back(Point{line_coord_zero_one(m, order), double(0), double(0)}); + out.lattice.push_back(Lattice{m, 0, 0}); + } + for (int m = 1; m < order; ++m) { + out.coords.push_back(Point{line_coord_zero_one(order - m, order), + line_coord_zero_one(m, order), double(0)}); + out.lattice.push_back(Lattice{order - m, m, 0}); + } + for (int m = 1; m < order; ++m) { + out.coords.push_back(Point{double(0), line_coord_zero_one(order - m, order), double(0)}); + out.lattice.push_back(Lattice{0, order - m, 0}); + } + + append_triangle_face_interior(out, + Point{double(0), double(0), double(0)}, + Point{double(1), double(0), double(0)}, + Point{double(0), double(1), double(0)}, + Lattice{0, 0, 0}, + Lattice{order, 0, 0}, + Lattice{0, order, 0}, + order); + return out; +} + +LagrangeNodeLayout generate_quad_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(0), double(0), double(0)}); + } + + out.coords.reserve(static_cast((order + 1) * (order + 1))); + out.lattice.reserve(static_cast((order + 1) * (order + 1))); + out.coords.push_back(Point{double(-1), double(-1), double(0)}); + out.lattice.push_back(Lattice{0, 0, 0}); + out.coords.push_back(Point{double(1), double(-1), double(0)}); + out.lattice.push_back(Lattice{order, 0, 0}); + out.coords.push_back(Point{double(1), double(1), double(0)}); + out.lattice.push_back(Lattice{order, order, 0}); + out.coords.push_back(Point{double(-1), double(1), double(0)}); + out.lattice.push_back(Lattice{0, order, 0}); + + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), double(-1), double(0)}); + out.lattice.push_back(Lattice{i, 0, 0}); + } + for (int j = 1; j < order; ++j) { + out.coords.push_back(Point{double(1), line_coord_pm_one(j, order), double(0)}); + out.lattice.push_back(Lattice{order, j, 0}); + } + for (int i = order - 1; i >= 1; --i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), double(1), double(0)}); + out.lattice.push_back(Lattice{i, order, 0}); + } + for (int j = order - 1; j >= 1; --j) { + out.coords.push_back(Point{double(-1), line_coord_pm_one(j, order), double(0)}); + out.lattice.push_back(Lattice{0, j, 0}); + } + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), + line_coord_pm_one(j, order), double(0)}); + out.lattice.push_back(Lattice{i, j, 0}); + } + } + return out; +} + +LagrangeNodeLayout generate_tetra_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(0.25), double(0.25), double(0.25)}); + } + + const Point verts[] = { + Point{double(0), double(0), double(0)}, + Point{double(1), double(0), double(0)}, + Point{double(0), double(1), double(0)}, + Point{double(0), double(0), double(1)}, + }; + const Lattice vert_lattice[] = { + Lattice{0, 0, 0}, + Lattice{order, 0, 0}, + Lattice{0, order, 0}, + Lattice{0, 0, order}, + }; + + out.coords.reserve(static_cast((order + 1) * (order + 2) * (order + 3) / 6)); + out.lattice.reserve(static_cast((order + 1) * (order + 2) * (order + 3) / 6)); + append_vertices(out, verts, vert_lattice); + + // Edge vertex pairs in VTK quadratic-tetra edge order. + const int edges[6][2] = {{0, 1}, {1, 2}, {2, 0}, {0, 3}, {1, 3}, {2, 3}}; + for (const auto& edge : edges) { + for (int m = 1; m < order; ++m) { + const double t = static_cast(m) / static_cast(order); + out.coords.push_back(verts[edge[0]] * (double(1) - t) + verts[edge[1]] * t); + out.lattice.push_back(lerp_lattice(vert_lattice[edge[0]], vert_lattice[edge[1]], m, order)); + } + } + + // Triangular faces in VTK tetra face order (vertex triples). + const int faces[4][3] = {{0, 1, 2}, {0, 1, 3}, {1, 2, 3}, {0, 2, 3}}; + for (const auto& face : faces) { + append_triangle_face_interior(out, + verts[face[0]], verts[face[1]], verts[face[2]], + vert_lattice[face[0]], vert_lattice[face[1]], vert_lattice[face[2]], + order); + } + + for (int l = 1; l <= order - 3; ++l) { + for (int k = 1; k <= order - l - 2; ++k) { + for (int j = 1; j <= order - l - k - 1; ++j) { + out.coords.push_back(Point{double(j) / double(order), + double(k) / double(order), + double(l) / double(order)}); + out.lattice.push_back(Lattice{j, k, l}); + } + } + } + return out; +} + +LagrangeNodeLayout generate_hex_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(0), double(0), double(0)}); + } + + const Point verts[] = { + Point{double(-1), double(-1), double(-1)}, + Point{double(1), double(-1), double(-1)}, + Point{double(1), double(1), double(-1)}, + Point{double(-1), double(1), double(-1)}, + Point{double(-1), double(-1), double(1)}, + Point{double(1), double(-1), double(1)}, + Point{double(1), double(1), double(1)}, + Point{double(-1), double(1), double(1)}, + }; + const Lattice vert_lattice[] = { + Lattice{0, 0, 0}, + Lattice{order, 0, 0}, + Lattice{order, order, 0}, + Lattice{0, order, 0}, + Lattice{0, 0, order}, + Lattice{order, 0, order}, + Lattice{order, order, order}, + Lattice{0, order, order}, + }; + + out.coords.reserve(static_cast((order + 1) * (order + 1) * (order + 1))); + out.lattice.reserve(static_cast((order + 1) * (order + 1) * (order + 1))); + append_vertices(out, verts, vert_lattice); + + // Edge vertex pairs in VTK quadratic-hex edge order (bottom ring, top ring, + // then the four vertical edges). + const int edges[12][2] = { + {0, 1}, {1, 2}, {2, 3}, {3, 0}, + {4, 5}, {5, 6}, {6, 7}, {7, 4}, + {0, 4}, {1, 5}, {2, 6}, {3, 7}, + }; + // Edge-interior nodes at the Gauss-Lobatto-Legendre position of their lattice + // index on each axis (line_coord_pm_one), consistent with the corner, face, and + // interior strata and with the 1D tensor-axis nodes the evaluator uses. (A plain + // equispaced interpolation along the edge would disagree with the GLL faces and + // interior at order >= 3.) + for (const auto& edge : edges) { + for (int m = 1; m < order; ++m) { + const Lattice idx = + lerp_lattice(vert_lattice[edge[0]], vert_lattice[edge[1]], m, order); + out.coords.push_back(Point{line_coord_pm_one(idx[0], order), + line_coord_pm_one(idx[1], order), + line_coord_pm_one(idx[2], order)}); + out.lattice.push_back(idx); + } + } + + // Face-interior nodes, emitted in VTK face order so the layout matches the + // VTK cell node numbering the solver ingests from .vtu meshes: + // -X, +X, -Y, +Y, -Z, +Z (e.g. Hex27 face centers become nodes 20..25). + // For order >= 3 the within-face node sequence follows the loops below; only + // the face order is normalized to VTK, which is all the supported Hex8/20/27 + // elements require. + // -X face (x = -1) + for (int k = 1; k < order; ++k) { + for (int j = order - 1; j >= 1; --j) { + out.coords.push_back(Point{double(-1), line_coord_pm_one(j, order), line_coord_pm_one(k, order)}); + out.lattice.push_back(Lattice{0, j, k}); + } + } + // +X face (x = +1) + for (int k = 1; k < order; ++k) { + for (int j = 1; j < order; ++j) { + out.coords.push_back(Point{double(1), line_coord_pm_one(j, order), line_coord_pm_one(k, order)}); + out.lattice.push_back(Lattice{order, j, k}); + } + } + // -Y face (y = -1) + for (int k = 1; k < order; ++k) { + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), double(-1), line_coord_pm_one(k, order)}); + out.lattice.push_back(Lattice{i, 0, k}); + } + } + // +Y face (y = +1) + for (int k = 1; k < order; ++k) { + for (int i = order - 1; i >= 1; --i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), double(1), line_coord_pm_one(k, order)}); + out.lattice.push_back(Lattice{i, order, k}); + } + } + // -Z face (z = -1) + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), line_coord_pm_one(j, order), double(-1)}); + out.lattice.push_back(Lattice{i, j, 0}); + } + } + // +Z face (z = +1) + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), line_coord_pm_one(j, order), double(1)}); + out.lattice.push_back(Lattice{i, j, order}); + } + } + for (int k = 1; k < order; ++k) { + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + out.coords.push_back(Point{line_coord_pm_one(i, order), + line_coord_pm_one(j, order), + line_coord_pm_one(k, order)}); + out.lattice.push_back(Lattice{i, j, k}); + } + } + } + return out; +} + +LagrangeNodeLayout generate_wedge_nodes(int order) { + LagrangeNodeLayout out; + if (order == 0) { + return single_node_layout(Point{double(1) / double(3), double(1) / double(3), double(0)}); + } + + const Point verts[] = { + Point{double(0), double(0), double(-1)}, + Point{double(1), double(0), double(-1)}, + Point{double(0), double(1), double(-1)}, + Point{double(0), double(0), double(1)}, + Point{double(1), double(0), double(1)}, + Point{double(0), double(1), double(1)}, + }; + const Lattice vert_lattice[] = { + Lattice{0, 0, 0}, + Lattice{order, 0, 0}, + Lattice{0, order, 0}, + Lattice{0, 0, order}, + Lattice{order, 0, order}, + Lattice{0, order, order}, + }; + + out.coords.reserve(static_cast((order + 1) * (order + 1) * (order + 2) / 2)); + out.lattice.reserve(static_cast((order + 1) * (order + 1) * (order + 2) / 2)); + append_vertices(out, verts, vert_lattice); + + // Edge vertex pairs in VTK quadratic-wedge edge order (bottom triangle, top + // triangle, then the three vertical edges). + const int edges[9][2] = { + {0, 1}, {1, 2}, {2, 0}, + {3, 4}, {4, 5}, {5, 3}, + {0, 3}, {1, 4}, {2, 5}, + }; + // The triangle cross-section (x, y) keeps its equispaced simplex placement; the + // through-axis (z) uses the Gauss-Lobatto-Legendre node of the lattice index, so + // the prism's tensor axis matches the 1D nodes the evaluator uses. (Triangle + // edges have z lattice 0 or `order`, for which line_coord_pm_one is -1 / +1, so + // their z is unchanged.) + for (const auto& edge : edges) { + for (int m = 1; m < order; ++m) { + const double t = static_cast(m) / static_cast(order); + const Lattice idx = + lerp_lattice(vert_lattice[edge[0]], vert_lattice[edge[1]], m, order); + Point coord = verts[edge[0]] * (double(1) - t) + verts[edge[1]] * t; + coord[2] = line_coord_pm_one(idx[2], order); + out.coords.push_back(coord); + out.lattice.push_back(idx); + } + } + + append_triangle_face_interior(out, verts[0], verts[1], verts[2], + vert_lattice[0], vert_lattice[1], vert_lattice[2], order); + append_triangle_face_interior(out, verts[3], verts[4], verts[5], + vert_lattice[3], vert_lattice[4], vert_lattice[5], order); + + for (int r = 1; r < order; ++r) { + const double z = line_coord_pm_one(r, order); + for (int m = 1; m < order; ++m) { + const double t = static_cast(m) / static_cast(order); + out.coords.push_back(Point{t, double(0), z}); + out.lattice.push_back(Lattice{m, 0, r}); + } + for (int m = 1; m < order; ++m) { + const double t = static_cast(m) / static_cast(order); + out.coords.push_back(Point{double(1) - t, t, z}); + out.lattice.push_back(Lattice{order - m, m, r}); + } + for (int m = 1; m < order; ++m) { + const double t = static_cast(m) / static_cast(order); + out.coords.push_back(Point{double(0), double(1) - t, z}); + out.lattice.push_back(Lattice{0, order - m, r}); + } + } + + for (int r = 1; r < order; ++r) { + const double z = line_coord_pm_one(r, order); + for (int c = 1; c <= order - 2; ++c) { + for (int b = 1; b <= order - c - 1; ++b) { + out.coords.push_back(Point{double(b) / double(order), + double(c) / double(order), + z}); + out.lattice.push_back(Lattice{b, c, r}); + } + } + } + return out; +} + +LagrangeNodeLayout complete_lagrange_nodes(ElementType canonical_type, int order) { + svmp::throw_if(order < 0, "ReferenceNodeLayout requires non-negative Lagrange order"); + const ElementType type = canonical_lagrange_type(canonical_type); + switch (type) { + case ElementType::Point1: { + LagrangeNodeLayout out; + out.coords.push_back(Point{double(0), double(0), double(0)}); + out.lattice.push_back(Lattice{0, 0, 0}); + return out; + } + case ElementType::Line2: + return generate_line_nodes(order); + case ElementType::Triangle3: + return generate_triangle_nodes(order); + case ElementType::Quad4: + return generate_quad_nodes(order); + case ElementType::Tetra4: + return generate_tetra_nodes(order); + case ElementType::Hex8: + return generate_hex_nodes(order); + case ElementType::Wedge6: + return generate_wedge_nodes(order); + case ElementType::Pyramid5: + svmp::raise("ReferenceNodeLayout: pyramid node ordering is disabled"); + default: + svmp::raise("ReferenceNodeLayout: unsupported Lagrange topology"); + } +} + +// Topological interior dimension of a wedge-prism lattice node: the number of +// independent directions in which the point sits in the relative interior of the +// reference cell. A vertex gives 0, an edge-interior node 1, a face-interior node +// 2, and a volume-interior node 3. Only the wedge needs this classification -- it +// is the one serendipity layout still built by truncating a complete layout +// (serendipity_subset_nodes). Quadrilateral and hexahedral serendipity geometries +// are generated directly by quad_/hex_serendipity_nodes and never go through here. +int wedge_interior_dim(const Lattice& idx, int order) { + const auto tensor_interior = [order](int v) { return (v > 0 && v < order) ? 1 : 0; }; + // (idx[0], idx[1]) is the triangle cross-section with implied third + // barycentric index k; idx[2] is the tensor through-axis. A triangle vertex + // contributes 0, a triangle edge 1, and the triangle interior 2. + const int i = idx[0]; + const int j = idx[1]; + const int k = order - i - j; + const bool tri_vertex = (i == order) || (j == order) || (i + j == 0); + const bool tri_interior = (i > 0) && (j > 0) && (k > 0); + const int tri_dim = tri_vertex ? 0 : (tri_interior ? 2 : 1); + return tri_dim + tensor_interior(idx[2]); +} + +// Build the Wedge15 serendipity reference layout from the complete quadratic wedge +// layout. Serendipity layouts keep only the element's vertices and edge midpoints +// and drop the face- and volume-interior nodes; the complete-quadratic generators +// emit the vertex/edge nodes first, so the serendipity set is exactly the leading +// keep_count nodes. (Quadrilateral and hexahedral serendipity geometries are +// generated directly by quad_/hex_serendipity_nodes, not by truncation here.) +std::vector serendipity_subset_nodes(LagrangeNodeLayout complete, + std::size_t keep_count, + std::size_t complete_count) { + constexpr int kQuadraticOrder = 2; + svmp::throw_if( + complete.coords.size() != complete_count || + complete.lattice.size() != complete_count, + "ReferenceNodeLayout: unexpected complete-quadratic node count for serendipity layout"); + svmp::throw_if( + keep_count >= complete_count, "ReferenceNodeLayout: serendipity node count must be smaller than the complete layout"); + + for (std::size_t n = 0; n < complete.lattice.size(); ++n) { + const bool on_skeleton = + wedge_interior_dim(complete.lattice[n], kQuadraticOrder) <= 1; + const bool kept = n < keep_count; + svmp::throw_if( + kept != on_skeleton, "ReferenceNodeLayout: serendipity truncation does not separate skeleton nodes from interior nodes"); + } + + std::vector nodes = std::move(complete.coords); + nodes.resize(keep_count); + return nodes; +} + +// --------------------------------------------------------------------------- +// Arbitrary-order serendipity node geometry (quadrilateral and hexahedral). +// +// The corner+edge skeleton is the leading prefix of the complete Lagrange layout +// of the same order (the complete generators above); only the reduced face/volume +// interior below is serendipity-specific. These back +// ReferenceNodeLayout::serendipity_node_coords and the named Quad8/Hex20 layouts, +// so serendipity node geometry has a single owner. (Wedge15 is a fixed named +// layout, handled by serendipity_subset_nodes above.) +// --------------------------------------------------------------------------- + +std::size_t quad_serendipity_interior_count(int order) { + if (order < 4) { + return 0u; + } + const auto m = static_cast(order - 4); + return (m + 1u) * (m + 2u) / 2u; +} + +// Interior nodes are a triangular row set for P_m, m = order - 4: a serendipity +// polynomial vanishing at the p + 1 boundary nodes on every edge factors as +// (1 - x^2)(1 - y^2) q with q in P_m, and the staircase below is unisolvent for q +// by induction over rows. It sits on Gauss-Lobatto-Legendre interior nodes (the +// same 1D distribution as the boundary) so the reduced space stays well-conditioned +// at high order; GLL only moves where the distinct points sit, not the staircase +// structure. +void append_quad_serendipity_interior_nodes(std::vector& nodes, int order) { + if (order < 4) { + return; + } + const int m = order - 4; + for (int row = 0; row <= m; ++row) { + const int row_count = m + 1 - row; + const double y = line_coord_pm_one(row + 1, m + 2); + for (int col = 0; col < row_count; ++col) { + const double x = line_coord_pm_one(col + 1, row_count + 1); + nodes.push_back(Point{x, y, double(0)}); + } + } +} + +// Quadrilateral serendipity reference nodes at the given order: the 4 corners and +// 4(order-1) edge nodes (the leading prefix of the complete Quad layout, in VTK +// boundary order) followed by the reduced triangular interior. +std::vector quad_serendipity_nodes(int order) { + std::vector nodes = generate_quad_nodes(order).coords; + const std::size_t boundary_count = static_cast(4 * order); + svmp::throw_if( + boundary_count > nodes.size(), "ReferenceNodeLayout: quadrilateral serendipity skeleton exceeds the complete Lagrange layout"); + nodes.resize(boundary_count); + append_quad_serendipity_interior_nodes(nodes, order); + return nodes; +} + +// Volume-interior node count for hexahedral serendipity. Once the boundary trace +// is fixed, an interior serendipity function factors as the cube bubble +// (1 - r^2)(1 - s^2)(1 - t^2) times a quotient of total degree at most order - 6, +// so the interior space is P_{order-6} in three variables: empty until order 6, +// then dim P_{order-6} = (m+1)(m+2)(m+3)/6 with m = order - 6. +std::size_t hex_serendipity_volume_interior_count(int order) { + if (order < 6) { + return 0u; + } + const auto m = static_cast(order - 6); + return (m + 1u) * (m + 2u) * (m + 3u) / 6u; +} + +// Append the face-interior nodes. The restriction of the order-`order` cube +// serendipity space to a face is the order-`order` quadrilateral serendipity +// space, so every face carries the same 2D quad-serendipity interior set, +// embedded into the face plane. Faces are visited in VTK face order +// (-X, +X, -Y, +Y, -Z, +Z); the in-plane (u, v) point maps to the two free axes +// of each face. Empty until order 4 (when the quad interior first appears). +void append_hex_serendipity_face_interior_nodes(std::vector& nodes, int order) { + std::vector face_interior; // (u, v, 0) interior points of one quad face + append_quad_serendipity_interior_nodes(face_interior, order); + if (face_interior.empty()) { + return; + } + + // Each face: the fixed axis (0 = r, 1 = s, 2 = t), its +/-1 value, and the two + // in-plane axes that carry the 2D interior point (u, v). + struct Face { + int fixed_axis; + double fixed_value; + int u_axis; + int v_axis; + }; + static constexpr Face faces[6] = { + {0, double(-1), 1, 2}, // -X: (s, t) in plane + {0, double(1), 1, 2}, // +X + {1, double(-1), 0, 2}, // -Y: (r, t) in plane + {1, double(1), 0, 2}, // +Y + {2, double(-1), 0, 1}, // -Z: (r, s) in plane + {2, double(1), 0, 1}, // +Z + }; + + for (const auto& face : faces) { + for (const auto& p : face_interior) { + Point node = Point::Zero(); + node[static_cast(face.fixed_axis)] = face.fixed_value; + node[static_cast(face.u_axis)] = p[0]; + node[static_cast(face.v_axis)] = p[1]; + nodes.push_back(node); + } + } +} + +// Append the volume-interior nodes: a tetrahedral staircase unisolvent for the +// interior residual P_{order-6}, on Gauss-Lobatto-Legendre interior nodes. Each +// t-layer is a triangular staircase whose total degree decreases by one per layer, +// so the layers consume P_{order-6} by induction in t exactly as the quad interior +// consumes P_{order-4} by induction in s. Empty until order 6. +void append_hex_serendipity_volume_interior_nodes(std::vector& nodes, int order) { + if (order < 6) { + return; + } + const int m = order - 6; + for (int layer = 0; layer <= m; ++layer) { + const int tri_order = m - layer; + const double t = line_coord_pm_one(layer + 1, m + 2); + for (int row = 0; row <= tri_order; ++row) { + const int row_count = tri_order + 1 - row; + const double s = line_coord_pm_one(row + 1, tri_order + 2); + for (int col = 0; col < row_count; ++col) { + const double r = line_coord_pm_one(col + 1, row_count + 1); + nodes.push_back(Point{r, s, t}); + } + } + } +} + +// Hexahedral serendipity reference nodes in VTK-consistent stratified order: 8 +// corners, 12(order-1) edge nodes (the leading prefix of the complete Hex layout), +// then the 6 face interiors in VTK face order, then the volume interior. At order 1 +// (corners) and order 2 (corners + edge midpoints) this is exactly the public +// Hex8 / Hex20 ordering; higher-order face/volume sets are this module's own +// convention. +std::vector hex_serendipity_nodes(int order) { + std::vector nodes = generate_hex_nodes(order).coords; + const std::size_t skeleton_count = + 8u + 12u * static_cast(order - 1); + svmp::throw_if( + skeleton_count > nodes.size(), "ReferenceNodeLayout: hexahedral serendipity skeleton exceeds the complete Lagrange layout"); + nodes.resize(skeleton_count); + + const std::size_t skeleton = nodes.size(); + append_hex_serendipity_face_interior_nodes(nodes, order); + svmp::throw_if( + nodes.size() - skeleton != 6u * quad_serendipity_interior_count(order), "ReferenceNodeLayout: hexahedral serendipity face-interior node count mismatch"); + + const std::size_t before_volume = nodes.size(); + append_hex_serendipity_volume_interior_nodes(nodes, order); + svmp::throw_if( + nodes.size() - before_volume != hex_serendipity_volume_interior_count(order), "ReferenceNodeLayout: hexahedral serendipity volume-interior node count mismatch"); + return nodes; +} + +std::vector element_nodes(ElementType elem_type) { + const int order = complete_lagrange_alias_order(elem_type); + if (order >= 0) { + return complete_lagrange_nodes(elem_type, order).coords; + } + + switch (elem_type) { + case ElementType::Quad8: + return quad_serendipity_nodes(2); + case ElementType::Hex20: + return hex_serendipity_nodes(2); + case ElementType::Wedge15: + return serendipity_subset_nodes(generate_wedge_nodes(2), 15u, 18u); + case ElementType::Pyramid13: + svmp::raise("ReferenceNodeLayout: pyramid node ordering is disabled"); + default: + svmp::raise("ReferenceNodeLayout: unknown element type"); + } +} + +// Structural invariants the lattice must satisfy, checked before the accessor +// hands it out. These replace the floating-point round-trip's near-equality +// guards with exact integer checks. +void validate_lattice(const LagrangeNodeLayout& layout, ElementType type, int order) { + svmp::throw_if( + layout.coords.size() != layout.lattice.size(), "ReferenceNodeLayout: lattice/coordinate count mismatch"); + + const BasisTopology top = topology(type); + for (const auto& idx : layout.lattice) { + for (std::size_t d = 0; d < 3u; ++d) { + svmp::throw_if( + idx[d] < 0 || idx[d] > order, "ReferenceNodeLayout: lattice index outside [0, order]"); + } + if (top == BasisTopology::Triangle || top == BasisTopology::Tetrahedron) { + svmp::throw_if( + idx[0] + idx[1] + idx[2] > order, "ReferenceNodeLayout: simplex lattice index sum exceeds order"); + } else if (top == BasisTopology::Wedge) { + svmp::throw_if( + idx[0] + idx[1] > order, "ReferenceNodeLayout: wedge triangle lattice index sum exceeds order"); + } + } +} + +} // namespace + +double line_coord_pm_one(int i, int order) { + if (order <= 0) { + svmp::throw_if( + i != 0, "ReferenceNodeLayout::line_coord_pm_one: node index out of range"); + return double(0); + } + svmp::throw_if( + i < 0 || i > order, "ReferenceNodeLayout::line_coord_pm_one: node index out of range"); + return gll_points(order)[static_cast(i)]; +} + +math::Vector ReferenceNodeLayout::node_coord_at(ElementType elem_type, + std::size_t local_node) { + const auto nodes = element_nodes(elem_type); + svmp::throw_if(local_node >= nodes.size(), "ReferenceNodeLayout::node_coord_at: node index out of range"); + return nodes[local_node]; +} + +std::size_t ReferenceNodeLayout::num_nodes(ElementType elem_type) { + return element_nodes(elem_type).size(); +} + +std::vector> +ReferenceNodeLayout::node_coords(ElementType elem_type) { + return element_nodes(elem_type); +} + +std::vector> +ReferenceNodeLayout::get_lagrange_node_coords(ElementType canonical_type, int order) { + return complete_lagrange_nodes(canonical_type, order).coords; +} + +LagrangeNodeLayout +ReferenceNodeLayout::get_lagrange_lattice(ElementType canonical_type, int order) { + LagrangeNodeLayout layout = complete_lagrange_nodes(canonical_type, order); + validate_lattice(layout, canonical_type, order); + return layout; +} + +std::vector> +ReferenceNodeLayout::serendipity_node_coords(BasisTopology topology, int order) { + svmp::throw_if( + order < 1, "ReferenceNodeLayout::serendipity_node_coords requires a polynomial order >= 1"); + switch (topology) { + case BasisTopology::Quadrilateral: + return quad_serendipity_nodes(order); + case BasisTopology::Hexahedron: + return hex_serendipity_nodes(order); + default: + svmp::raise("ReferenceNodeLayout::serendipity_node_coords: generated serendipity layouts " + "exist only for Quadrilateral and Hexahedron (Wedge15 is the fixed named layout)"); + } +} + +/** @endcond */ + +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Basis/NodeOrderingConventions.h b/Code/Source/solver/FE/Basis/NodeOrderingConventions.h new file mode 100644 index 000000000..1c01fab22 --- /dev/null +++ b/Code/Source/solver/FE/Basis/NodeOrderingConventions.h @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_NODEORDERINGCONVENTIONS_H +#define SVMP_FE_BASIS_NODEORDERINGCONVENTIONS_H + +#include "BasisTraits.h" +#include "Math/Vector.h" +#include "Types.h" + +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @defgroup FE_BasisNodeOrdering Reference-node generation (internal) + * @ingroup FE_Basis + * @brief Reference-node generators that the basis families build on. + * + * @warning Internal implementation detail. Do not use these directly: obtain a + * basis through basis_factory and read its nodes via BasisFunction::nodes(). + * These declarations are part of the internal node-ordering machinery and their + * interface may change without notice. + * + * @details This is the reference-node generator the basis families build on, not + * a consumer entry point. It is documented for FE core developers; model-level + * code never calls it directly. + * @{ + */ + +/** + * @brief The i-th 1D tensor-axis reference node on [-1, 1] at the given order. + * + * @details Returns the Gauss-Lobatto-Legendre (GLL) node of index @p i for a + * degree-@p order distribution: the endpoints are -1 and +1 and the interior + * nodes are the roots of @f$P'_{order}@f$, so high-order tensor interpolation + * stays well-conditioned (a logarithmic Lebesgue constant instead of the + * exponential growth of equispaced nodes). At order 1 the nodes are + * @f$\{-1, +1\}@f$ and at order 2 @f$\{-1, 0, +1\}@f$, so they coincide with the + * equispaced layout for the production orders and differ only for order >= 3. + * Returns 0 for order <= 0 when @p i is 0. Invalid indices throw. + * + * This is the single definition of the tensor-axis node distribution: the + * reference-node layout generators, the Lagrange tensor-axis initialization, and + * the serendipity edge/face/interior strata all source their 1D nodes here. The + * LagrangeBasis and SerendipityBasis docs point back to this description of the + * GLL distribution and its conditioning rather than restating it. + * + * @param i Node index in [0, order] for positive orders, or 0 for order <= 0. + * @param order Polynomial order of the 1D distribution. + * @return GLL node coordinate on [-1, 1]. + * @throws BasisNodeOrderingException If @p i is outside the valid range. + */ +[[nodiscard]] double line_coord_pm_one(int i, int order); + +/** + * @brief Reference Lagrange node coordinates paired with their integer lattice + * index. + * + * @details `lattice[n]` is the exact integer index of `coords[n]` in the + * element's natural index space, with every component in `[0, order]`: + * - tensor topologies (line/quad/hex): axis indices `(i, j, k)`, unused axes `0`; + * - simplex topologies (triangle/tetra): off-origin barycentric indices + * `(i, j, k)` (with `k = 0` for triangles) satisfying `i + j + k <= order`; + * - wedge: triangle lattice `(i, j)` in the first two components and the + * through-axis index `r` in the third. + * + * Emitting the lattice alongside the coordinate lets callers consume the integer + * index directly instead of reconstructing it from the floating-point coordinate. + */ +struct LagrangeNodeLayout { + std::vector> coords; ///< Reference node coordinates, one per node. + std::vector> lattice; ///< Integer lattice index of each node (see details above). +}; + +/** + * @brief Reference-node coordinate and count lookups for an element type. + */ +class ReferenceNodeLayout { +public: + /** + * @brief One reference node coordinate by local index. Regenerates the full + * layout per call; prefer node_coords() when more than one node is needed. + * + * @param elem_type Element type to look up. + * @param local_node Local node index in [0, num_nodes(elem_type)). + * @return Reference coordinate of the requested node. + */ + static math::Vector node_coord_at(ElementType elem_type, + std::size_t local_node); + + /** + * @brief Number of reference nodes in an element type's public layout. + * @param elem_type Element type to look up. + * @return Node count. + */ + static std::size_t num_nodes(ElementType elem_type); + + /** + * @brief All reference node coordinates for an element type, in public layout order. + * + * @details Returns the complete public reference layout for @p elem_type + * (the same coordinates node_coord_at() returns one at a time), including + * the serendipity layouts. Prefer this single call when the whole layout is + * needed: node_coord_at() regenerates the full list on every call. + * + * @param elem_type Element type to look up. + * @return Reference node coordinates, one per node. + */ + static std::vector> node_coords(ElementType elem_type); + + /** + * @brief Reference Lagrange node coordinates for a canonical type and order. + * @param canonical_type Canonical Lagrange element type (or Point1). + * @param order Polynomial order. + * @return Reference node coordinates, one per node, in basis order. + */ + static std::vector> + get_lagrange_node_coords(ElementType canonical_type, int order); + + /** + * @brief Reference Lagrange nodes with their integer lattice indices. + * + * @details Returns the same coordinates as get_lagrange_node_coords(), paired + * with the integer lattice index of each node (see LagrangeNodeLayout). The + * structural invariants in the contract (size match, components in + * `[0, order]`, simplex/wedge sum bounds) are validated before returning. + * + * @param canonical_type Canonical Lagrange element type (or Point1). + * @param order Polynomial order. + * @return Coordinates and matching lattice indices, one entry per node. + * @throws BasisConstructionException If a structural invariant is violated. + */ + static LagrangeNodeLayout + get_lagrange_lattice(ElementType canonical_type, int order); + + /** + * @brief Reference nodes for an arbitrary-order serendipity layout. + * + * @details Generates the stratified serendipity node set for the + * quadrilateral or hexahedral family at the requested order: the + * corner+edge skeleton (the leading prefix of the complete Lagrange layout + * of the same order, in VTK boundary order) followed by the reduced face + * and volume interior. This is the single source of serendipity node + * geometry -- SerendipityBasis builds its mode space and coefficient table + * on top of these coordinates for both the arbitrary-order path and the + * named Quad8/Hex20 layouts (the order-2 instances). Wedge serendipity + * (Wedge15) is a fixed named layout and is not generated here. + * + * @param topology BasisTopology::Quadrilateral or BasisTopology::Hexahedron. + * @param order Polynomial order; must be >= 1. + * @return Reference node coordinates in stratified (skeleton-then-interior) order. + * @throws BasisConstructionException If @p order is below 1. + * @throws BasisElementCompatibilityException If @p topology is not Quadrilateral or Hexahedron. + */ + static std::vector> + serendipity_node_coords(BasisTopology topology, int order); +}; + +/** @} */ + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_NODEORDERINGCONVENTIONS_H diff --git a/Code/Source/solver/FE/Basis/SerendipityBasis.cpp b/Code/Source/solver/FE/Basis/SerendipityBasis.cpp new file mode 100644 index 000000000..c0a638d6f --- /dev/null +++ b/Code/Source/solver/FE/Basis/SerendipityBasis.cpp @@ -0,0 +1,543 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "SerendipityBasis.h" +#include "NodeOrderingConventions.h" +#include "Math/DenseLinearAlgebra.h" + +#include +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +namespace { +using Vec3 = math::Vector; + +// Superlinear contribution of one axis exponent: degrees 0 and 1 are free (they +// do not raise the superlinear degree), every higher degree contributes its full +// value. Summed over the active axes this gives the serendipity superlinear +// degree that bounds the mode set. +int superlinear_term(int a) { + return a > 1 ? a : 0; +} + +inline double integer_power(double base, int exponent) { + double result = double(1); + for (int k = 0; k < exponent; ++k) { + result *= base; + } + return result; +} + +// Which 1D polynomial family the tensor modes are written in: the quadrilateral +// and hexahedral families use Legendre, the fixed Wedge15 layout (order 2) keeps +// the monomial form. Both span the same serendipity polynomial space; see the +// SerendipityBasis class documentation (Modal basis) for why Legendre is used to +// keep the Vandermonde well-conditioned. +enum class ModalAxisKind { Monomial, Legendre }; + +// Value and first/second derivative of every 1D mode phi_0..phi_{max_degree} at a +// fixed coordinate, indexed by per-axis degree. +struct AxisTable { + std::vector value; + std::vector first; + std::vector second; +}; + +// phi_k(x) = x^k and its derivatives. Matches the previous per-mode evaluation +// exactly, so the Wedge15 monomial path is unchanged. +void fill_monomial_table(double x, int max_degree, AxisTable& out) { + const std::size_t n = static_cast(max_degree) + 1u; + out.value.assign(n, double(0)); + out.first.assign(n, double(0)); + out.second.assign(n, double(0)); + for (int k = 0; k <= max_degree; ++k) { + const std::size_t kp = static_cast(k); + out.value[kp] = integer_power(x, k); + out.first[kp] = (k > 0) ? double(k) * integer_power(x, k - 1) : double(0); + out.second[kp] = + (k > 1) ? double(k * (k - 1)) * integer_power(x, k - 2) : double(0); + } +} + +// phi_k(x) = P_k(x), the degree-k Legendre polynomial on [-1, 1], with first and +// second derivatives. Built from the three-term recurrences +// (k+1) P_{k+1} = (2k+1) x P_k - k P_{k-1} +// (k+1) P'_{k+1} = (2k+1)(P_k + x P'_k) - k P'_{k-1} +// (k+1) P''_{k+1} = (2k+1)(2 P'_k + x P''_k) - k P''_{k-1} +// all regular at x = +/-1 (no division by 1 - x^2). +void fill_legendre_table(double x, int max_degree, AxisTable& out) { + const std::size_t n = static_cast(max_degree) + 1u; + out.value.assign(n, double(0)); + out.first.assign(n, double(0)); + out.second.assign(n, double(0)); + out.value[0] = double(1); + if (max_degree >= 1) { + out.value[1] = x; + out.first[1] = double(1); + } + for (int k = 1; k < max_degree; ++k) { + const std::size_t kp = static_cast(k); + const double kk = static_cast(k); + const double two_k_plus_one = double(2) * kk + double(1); + const double inv = double(1) / (kk + double(1)); + out.value[kp + 1] = + (two_k_plus_one * x * out.value[kp] - kk * out.value[kp - 1]) * inv; + out.first[kp + 1] = + (two_k_plus_one * (out.value[kp] + x * out.first[kp]) - + kk * out.first[kp - 1]) * inv; + out.second[kp + 1] = + (two_k_plus_one * (double(2) * out.first[kp] + x * out.second[kp]) - + kk * out.second[kp - 1]) * inv; + } +} + +void fill_axis_table(ModalAxisKind kind, double x, int max_degree, AxisTable& out) { + if (kind == ModalAxisKind::Legendre) { + fill_legendre_table(x, max_degree, out); + } else { + fill_monomial_table(x, max_degree, out); + } +} + +// Maximum tolerated infinity-norm condition number of a serendipity interpolation +// (Vandermonde) matrix. Above this the inverse loses more than about half of +// double precision (~1/sqrt(epsilon)), so construction throws rather than return +// silently-degraded functions. With the Legendre modal basis and +// Gauss-Lobatto-Legendre nodes the condition number stays far below this across +// the recommended range (~1.7e4 at quadrilateral order 10, ~1.3e4 at hexahedral +// order 8); the bound is the numerical-soundness backstop for orders pushed well +// past it. The shape-function quality limit (Lebesgue constant) is the tighter, +// inherent constraint and is documented/tested separately. +constexpr double kSerendipityVandermondeMaxCond = double(1e8); + +// Infinity norm (maximum absolute row sum) of a row-major n-by-n matrix. +double matrix_norm_inf(const std::vector& matrix, std::size_t n) { + double max_row = double(0); + for (std::size_t row = 0; row < n; ++row) { + double sum = double(0); + for (std::size_t col = 0; col < n; ++col) { + sum += std::abs(matrix[row * n + col]); + } + max_row = std::max(max_row, sum); + } + return max_row; +} + +// Per-axis degree triples (ax, ay, az) of the serendipity mode space: every +// combination whose superlinear degree (the sum of superlinear_term over the axes; +// see the SerendipityBasis class documentation for the rule) is at most `order`. +// `active_axes` is 2 for the quadrilateral (az pinned to 0) and 3 for the +// hexahedron, so the quad space is exactly the hex space restricted to az = 0. The +// set is downward-closed (so it spans both the monomial and the tensor Legendre +// basis; see ModalAxisKind), and the resulting nodal basis is independent of how +// the set is ordered. +std::vector> serendipity_exponents(int order, int active_axes) { + const int max_y = active_axes >= 2 ? order : 0; + const int max_z = active_axes >= 3 ? order : 0; + std::vector> exponents; + for (int az = 0; az <= max_z; ++az) { + for (int ay = 0; ay <= max_y; ++ay) { + for (int ax = 0; ax <= order; ++ax) { + if (superlinear_term(ax) + superlinear_term(ay) + superlinear_term(az) <= order) { + exponents.push_back({ax, ay, az}); + } + } + } + } + return exponents; +} + +// Build the nodal coefficient table for a serendipity family: assemble the +// generalized Vandermonde V[node][mode] = phi_a(r) phi_b(s) phi_c(t) at the +// public-order reference nodes -- with phi the monomial or Legendre 1D modes per +// `kind` -- and invert it. Because the nodes are in public order, the inverse is +// already in public basis order and needs no output permutation. The same routine +// serves the quadrilateral, hexahedral, and Wedge15 spaces. `max_degree` bounds +// the per-axis mode degree (the family's order). Construction throws if the matrix +// is too ill-conditioned to trust (see kSerendipityVandermondeMaxCond). +std::vector build_inverse_vandermonde( + std::span nodes, + std::span> exponents, + const std::string& label, + ModalAxisKind kind, + int max_degree) { + const std::size_t n = nodes.size(); + svmp::throw_if( + n == 0 || exponents.size() != n, "SerendipityBasis: invalid serendipity interpolation setup"); + + std::vector vandermonde(n * n, double(0)); + AxisTable tx; + AxisTable ty; + AxisTable tz; + for (std::size_t row = 0; row < n; ++row) { + const Vec3& p = nodes[row]; + fill_axis_table(kind, p[0], max_degree, tx); + fill_axis_table(kind, p[1], max_degree, ty); + fill_axis_table(kind, p[2], max_degree, tz); + for (std::size_t col = 0; col < n; ++col) { + const auto& e = exponents[col]; + vandermonde[row * n + col] = + tx.value[static_cast(e[0])] * + ty.value[static_cast(e[1])] * + tz.value[static_cast(e[2])]; + } + } + + // Condition-number backstop: the inverse is explicitly formed just above, so + // this is the true infinity-norm condition number + // cond_inf = ||V||_inf * ||V^{-1}||_inf, not an estimate. Reject orders where + // the inverse can no longer be trusted rather than returning silently-degraded + // shape functions; the comparison is negated so a non-finite value is rejected + // too. + const double norm_v = matrix_norm_inf(vandermonde, n); + + // invert_dense_matrix raises a generic FEException if the Vandermonde is + // exactly singular (a rank-deficient pivot). For a serendipity family that + // means the node set is not unisolvent at this order -- a construction failure + // in basis terms -- so translate it to BasisConstructionException, presenting + // the singular and the ill-conditioned cases (below) as one catchable type in + // one vocabulary. The matrix was just built n-by-n from n modes, so a + // size-mismatch FEException cannot originate here; rank deficiency is the only + // FEException this call can raise. (Defensive: the supported node sets are + // provably unisolvent, so this branch is not reachable for the shipped orders.) + std::vector inverse; + try { + inverse = math::invert_dense_matrix( + std::move(vandermonde), n, + "SerendipityBasis interpolation matrix for " + label); + } catch (const FEException&) { + svmp::raise("SerendipityBasis: " + label + + " interpolation matrix is singular; the serendipity node set is not " + "unisolvent at the requested order"); + } + const double condition_number = norm_v * matrix_norm_inf(inverse, n); + svmp::throw_if( + !(condition_number <= kSerendipityVandermondeMaxCond), "SerendipityBasis: " + label + + " interpolation matrix is too ill-conditioned (condition number ~ " + + std::to_string(condition_number) + + "); the requested order exceeds the well-conditioned range"); + return inverse; +} + +// Wedge15 serendipity monomial space, as (x, y, z) exponent triples. The prism is +// the triangle cross-section (x, y) crossed with the through-axis (z): the 6 +// triangle monomials x^a y^b with a + b <= 2 times the 3 line monomials z^c with +// c <= 2 form the complete 18-mode Wedge18 space. Wedge15 serendipity drops the 3 +// superlinear modes -- a quadratic triangle monomial (a + b == 2) times z^2, +// i.e. (2,0,2), (1,1,2), (0,2,2) -- leaving 6*3 - 3 = 15. The set (not its order) +// fixes the space; the nodal basis is the inverse Vandermonde at the Wedge15 nodes. +constexpr std::array, 15> kWedge15MonomialExponents = {{ + {{0, 0, 0}}, + {{0, 0, 1}}, + {{0, 0, 2}}, + {{0, 1, 0}}, + {{0, 1, 1}}, + {{0, 1, 2}}, + {{0, 2, 0}}, + {{0, 2, 1}}, + {{1, 0, 0}}, + {{1, 0, 1}}, + {{1, 0, 2}}, + {{1, 1, 0}}, + {{1, 1, 1}}, + {{2, 0, 0}}, + {{2, 0, 1}} +}}; + +struct NormalizedSerendipityRequest { + BasisTopology topology; + int dimension; + int order; +}; + +int serendipity_named_order(ElementType type) { + switch (type) { + case ElementType::Hex8: + return 1; + case ElementType::Quad8: + case ElementType::Hex20: + case ElementType::Wedge15: + return 2; + default: + return -1; + } +} + +// Validate a named serendipity element/order pairing and return its topology, +// reference dimension, and order. The named serendipity layouts (Quad8, Hex8, +// Hex20, Wedge15) are each pinned to a single polynomial order by their node +// count, so a mismatched explicit order is rejected. Arbitrary-order +// quadrilateral serendipity is not a named element: it is requested through the +// BasisTopology::Quadrilateral constructor. +NormalizedSerendipityRequest normalize_serendipity_request(ElementType type, int order) { + // The request must supply the layout's fixed order (serendipity_named_order): + // it is never floored or otherwise adjusted to fit, so order 0 and negative + // orders are rejected rather than promoted to a valid layout. + const int expected_order = serendipity_named_order(type); + switch (type) { + case ElementType::Quad8: + svmp::throw_if(order != expected_order, "SerendipityBasis: Quad8 is the quadratic 8-node serendipity layout (order 2 only); " + "use BasisTopology::Quadrilateral for higher-order quadrilateral serendipity"); + return {BasisTopology::Quadrilateral, 2, expected_order}; + case ElementType::Hex8: + svmp::throw_if(order != expected_order, "SerendipityBasis: Hex8 is the trilinear 8-node basis (order 1 only); use Hex20 for quadratic serendipity"); + return {BasisTopology::Hexahedron, 3, expected_order}; + case ElementType::Hex20: + svmp::throw_if(order != expected_order, "SerendipityBasis: Hex20 is the 20-node quadratic serendipity layout (order 2 only)"); + return {BasisTopology::Hexahedron, 3, expected_order}; + case ElementType::Wedge15: + svmp::throw_if(order != expected_order, "SerendipityBasis: Wedge15 is the 15-node quadratic serendipity layout (order 2 only)"); + return {BasisTopology::Wedge, 3, expected_order}; + default: + svmp::raise("SerendipityBasis named elements are Quad8, Hex8, Hex20, and Wedge15; " + "use BasisTopology::Quadrilateral for arbitrary-order quadrilateral serendipity"); + } +} + +} // namespace + +SerendipityBasis::SerendipityBasis(BasisTopology topology, int order) + : topology_(topology) { + const bool supported_topology = topology_ == BasisTopology::Quadrilateral || + topology_ == BasisTopology::Hexahedron; + svmp::throw_if( + !supported_topology, "SerendipityBasis: arbitrary-order topology construction is supported for " + "Quadrilateral and Hexahedron; use the named ElementType (Wedge15) for wedge serendipity"); + svmp::throw_if( + order < 1, "SerendipityBasis: serendipity requires a polynomial order >= 1"); + dimension_ = topology_ == BasisTopology::Hexahedron ? 3 : 2; + order_ = order; + if (topology_ == BasisTopology::Hexahedron) { + init_hexahedron(order_); + } else { + init_quadrilateral(order_); + } +} + +SerendipityBasis::SerendipityBasis(ElementType type, int order) { + const NormalizedSerendipityRequest normalized = normalize_serendipity_request(type, order); + topology_ = normalized.topology; + dimension_ = normalized.dimension; + order_ = normalized.order; + + switch (type) { + case ElementType::Quad8: + // Quad8 is the order-2 instance of the quadrilateral serendipity + // space; the named overload only pins the order. + init_quadrilateral(order_); + return; + case ElementType::Hex8: + // Hex8 is the order-1 instance of the hexahedral serendipity space. + init_hexahedron(1); + return; + case ElementType::Hex20: + // Hex20 is the order-2 instance of the hexahedral serendipity space. + init_hexahedron(2); + return; + case ElementType::Wedge15: + init_fixed_named(type); + return; + default: + // normalize_serendipity_request already rejected anything else. + svmp::raise("SerendipityBasis: unsupported named serendipity element"); + } +} + +SerendipityBasis::SerendipityBasis(ElementType type) + : SerendipityBasis(type, serendipity_named_order(type)) {} + +// Build the quadrilateral serendipity mode set, reference nodes, and nodal +// coefficient table for the given order. The coefficient table is the inverse +// Vandermonde of tensor Legendre modes spanning the same polynomial space as the +// monomial degree triples; because the nodes are in public order, evaluation +// needs no output permutation. Reference nodes come from the single +// ReferenceNodeLayout serendipity generator for both the named Quad8 layout and +// the arbitrary-order path. +void SerendipityBasis::init_quadrilateral(int order) { + mode_exponents_ = serendipity_exponents(order, /*active_axes=*/2); + size_ = mode_exponents_.size(); + nodes_ = ReferenceNodeLayout::serendipity_node_coords(BasisTopology::Quadrilateral, order); + svmp::throw_if( + nodes_.size() != size_, "SerendipityBasis: quadrilateral serendipity setup produced inconsistent sizes"); + uses_legendre_modes_ = true; + inv_vandermonde_ = build_inverse_vandermonde( + nodes_, mode_exponents_, "Quad order " + std::to_string(order), + ModalAxisKind::Legendre, order); +} + +// Build the hexahedral serendipity mode set, reference nodes, and nodal +// coefficient table for the given order, mirroring init_quadrilateral. Reference +// nodes come from the single ReferenceNodeLayout serendipity generator; Hex8 +// (order 1) and Hex20 (order 2) are its order-1/order-2 instances and match the +// public Hex8/Hex20 ordering exactly. +void SerendipityBasis::init_hexahedron(int order) { + mode_exponents_ = serendipity_exponents(order, /*active_axes=*/3); + size_ = mode_exponents_.size(); + nodes_ = ReferenceNodeLayout::serendipity_node_coords(BasisTopology::Hexahedron, order); + svmp::throw_if( + nodes_.size() != size_, "SerendipityBasis: hexahedral serendipity setup produced inconsistent sizes"); + uses_legendre_modes_ = true; + inv_vandermonde_ = build_inverse_vandermonde( + nodes_, mode_exponents_, "Hex order " + std::to_string(order), + ModalAxisKind::Legendre, order); +} + +// Build the Wedge15 serendipity layout from its tabulated monomial space and +// public-order ReferenceNodeLayout nodes. Hexahedral serendipity (Hex8 and Hex20 +// included) is generated by init_hexahedron, so the prism is the only named +// layout that still carries a fixed monomial table. +void SerendipityBasis::init_fixed_named(ElementType type) { + svmp::throw_if( + type != ElementType::Wedge15, "SerendipityBasis: init_fixed_named builds only the Wedge15 layout"); + size_ = 15u; + const std::span> family_exponents( + kWedge15MonomialExponents.data(), kWedge15MonomialExponents.size()); + nodes_ = ReferenceNodeLayout::node_coords(type); + svmp::throw_if( + nodes_.size() != size_, "SerendipityBasis: Wedge15 layout node count does not match basis size"); + svmp::throw_if( + family_exponents.size() != size_, "SerendipityBasis: Wedge15 monomial count does not match basis size"); + mode_exponents_.assign(family_exponents.begin(), family_exponents.end()); + // Wedge15 is the fixed order-2 layout; its 15x15 system is trivially + // well-conditioned, so it keeps the monomial modal basis. + uses_legendre_modes_ = false; + inv_vandermonde_ = build_inverse_vandermonde( + nodes_, mode_exponents_, "Wedge15", ModalAxisKind::Monomial, order_); +} + +void SerendipityBasis::evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + // Private sink: callers guarantee valid output spans -- the public *_to methods + // validate their one output with require_span_size, and the vector evaluators + // resize to size_. An empty span here means "skip that quantity". + + if (values_out.empty() && gradients_out.empty() && hessians_out.empty()) { + return; + } + + if (!values_out.empty()) { + std::fill(values_out.begin(), values_out.end(), double(0)); + } + if (!gradients_out.empty()) { + std::fill(gradients_out.begin(), gradients_out.end(), Gradient::Zero()); + } + if (!hessians_out.empty()) { + std::fill(hessians_out.begin(), hessians_out.end(), Hessian::Zero()); + } + + const double x = xi[0]; + const double y = xi[1]; + const double z = xi[2]; + + // Every serendipity family evaluates through its generated coefficient table, + // which is already in public basis order. + svmp::throw_if( + mode_exponents_.size() != size_ || + inv_vandermonde_.size() != size_ * size_, + "SerendipityBasis: interpolation tables are not initialized for evaluation"); + + // Build the per-axis modal tables once, then accumulate over the modes. The + // mode family must match the one the coefficient table was built with. + const ModalAxisKind kind = + uses_legendre_modes_ ? ModalAxisKind::Legendre : ModalAxisKind::Monomial; + AxisTable tx; + AxisTable ty; + AxisTable tz; + fill_axis_table(kind, x, order_, tx); + fill_axis_table(kind, y, order_, ty); + fill_axis_table(kind, z, order_, tz); + + // Accumulate the nodal shape functions from the modal tables. For each mode j, + // phi = phi_a(r) phi_b(s) phi_c(t) (and its derivatives) is weighted by the + // inverse-Vandermonde coefficient for each basis slot; the table is already in + // public basis order, so slot i reads column i directly. The spans were zeroed + // above and an empty span is skipped. + const bool want_values = !values_out.empty(); + const bool want_gradients = !gradients_out.empty(); + const bool want_hessians = !hessians_out.empty(); + + for (std::size_t j = 0; j < size_; ++j) { + const std::array& e = mode_exponents_[j]; + const std::size_t ex = static_cast(e[0]); + const std::size_t ey = static_cast(e[1]); + const std::size_t ez = static_cast(e[2]); + + const double vx = tx.value[ex]; + const double vy = ty.value[ey]; + const double vz = tz.value[ez]; + const double phi = vx * vy * vz; + + double d_dr = double(0), d_ds = double(0), d_dt = double(0); + if (want_gradients || want_hessians) { + d_dr = tx.first[ex] * vy * vz; + d_ds = vx * ty.first[ey] * vz; + d_dt = vx * vy * tz.first[ez]; + } + + double d_drr = double(0), d_dss = double(0), d_dtt = double(0); + double d_drs = double(0), d_drt = double(0), d_dst = double(0); + if (want_hessians) { + d_drr = tx.second[ex] * vy * vz; + d_dss = vx * ty.second[ey] * vz; + d_dtt = vx * vy * tz.second[ez]; + d_drs = tx.first[ex] * ty.first[ey] * vz; + d_drt = tx.first[ex] * vy * tz.first[ez]; + d_dst = vx * ty.first[ey] * tz.first[ez]; + } + + for (std::size_t slot = 0; slot < size_; ++slot) { + const double c = inv_vandermonde_[j * size_ + slot]; + if (want_values) { + values_out[slot] += c * phi; + } + if (want_gradients) { + Gradient& g = gradients_out[slot]; + g[0] += c * d_dr; + g[1] += c * d_ds; + g[2] += c * d_dt; + } + if (want_hessians) { + Hessian& h = hessians_out[slot]; + h(0, 0) += c * d_drr; + h(1, 1) += c * d_dss; + h(2, 2) += c * d_dtt; + h(0, 1) += c * d_drs; + h(1, 0) += c * d_drs; + h(0, 2) += c * d_drt; + h(2, 0) += c * d_drt; + h(1, 2) += c * d_dst; + h(2, 1) += c * d_dst; + } + } + } +} + +void SerendipityBasis::evaluate_values_to(const math::Vector& xi, + std::span values_out) const { + require_span_size(values_out.size(), size_, "SerendipityBasis::evaluate_values_to"); + evaluate_all_to(xi, values_out, std::span{}, std::span{}); +} + +void SerendipityBasis::evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const { + require_span_size(gradients_out.size(), size_, "SerendipityBasis::evaluate_gradients_to"); + evaluate_all_to(xi, std::span{}, gradients_out, std::span{}); +} + +void SerendipityBasis::evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const { + require_span_size(hessians_out.size(), size_, "SerendipityBasis::evaluate_hessians_to"); + evaluate_all_to(xi, std::span{}, std::span{}, hessians_out); +} + +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Basis/SerendipityBasis.h b/Code/Source/solver/FE/Basis/SerendipityBasis.h new file mode 100644 index 000000000..edef4cf6c --- /dev/null +++ b/Code/Source/solver/FE/Basis/SerendipityBasis.h @@ -0,0 +1,296 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_BASIS_SERENDIPITYBASIS_H +#define SVMP_FE_BASIS_SERENDIPITYBASIS_H + +/** + * @file SerendipityBasis.h + * @brief Reduced-degree-of-freedom serendipity bases + */ + +#include "BasisFunction.h" + +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +/** + * @defgroup FE_SerendipityBasis SerendipityBasis + * @ingroup FE_Basis + * @brief Construction and evaluation API for reduced serendipity finite-element bases. + * + * @details This group documents reduced degree-of-freedom basis families that + * preserve nodal interpolation on supported element boundaries while omitting + * selected interior tensor-product modes. These bases are used for standard + * serendipity elements and geometry-mode mappings that intentionally use a + * lower-order interpolation space. + * @{ + */ + +/** + * @brief Reduced-degree-of-freedom serendipity basis on supported reference elements. + * + * @details SerendipityBasis implements nodal bases for the quadrilateral and + * hexahedral serendipity families at arbitrary order, plus the Wedge15 prism + * layout. Compared with a complete tensor-product Lagrange basis of the same + * nominal order, a serendipity basis removes selected interior modes while + * retaining nodal interpolation on the supported node layout. The named layouts + * Quad8, Hex8, and Hex20 are the fixed-order instances of these families + * (quadrilateral order 2, hexahedron orders 1 and 2). + * + * The quadrilateral serendipity polynomial space is described by monomials + * @f$x^{a_x}y^{a_y}@f$ whose superlinear degree is at most the requested + * order. The implementation evaluates this space through tensor Legendre + * modes, which span the same polynomial space but give a better-conditioned + * Vandermonde. The superlinear degree is + * @f[ + * sldeg(x^{a_x}y^{a_y}) = + * \begin{cases} a_x, & a_x > 1 \\ 0, & a_x \le 1 \end{cases} + * + + * \begin{cases} a_y, & a_y > 1 \\ 0, & a_y \le 1 \end{cases}. + * @f] + * The nodal basis is recovered by inverting the generalized Vandermonde + * interpolation matrix at the selected reference nodes. Values, gradients, and + * Hessians are then evaluated by differentiating the modal vector and applying + * the inverse Vandermonde coefficients. + * For order @f$p \ge 1@f$, this space has @f$4p@f$ boundary modes for + * @f$p \le 3@f$ and + * @f[ + * 4p + \frac{(p - 3)(p - 2)}{2} + * @f] + * modes for @f$p \ge 4@f$. + * + * The quadrilateral node set is unisolvent by construction. If + * @f$s(x,y)@f$ in this space vanishes at the @f$p + 1@f$ distinct nodes on + * every edge, each edge restriction is a degree-@f$p@f$ one-variable + * polynomial with @f$p + 1@f$ roots, so all edge restrictions vanish. Thus + * @f$s@f$ is divisible by the boundary bubble + * @f$(1 - x^2)(1 - y^2)@f$, and the quotient lies in + * @f$P_{p-4}@f$ (with no quotient for @f$p < 4@f$). For @f$p \ge 4@f$, the + * interior nodes form triangular rows for @f$P_{p-4}@f$: the first row has + * @f$m + 1@f$ distinct @f$x@f$ values, the next row has @f$m@f$, and so on + * for @f$m = p - 4@f$. A total-degree polynomial that vanishes on those rows + * is zero by induction over rows, because each vanished row factors out one + * linear term in @f$y@f$. The interpolation Vandermonde is therefore + * nonsingular for the implemented quadrilateral serendipity space. + * + * Hexahedral serendipity generalizes the same construction to the cube. The + * polynomial space is described by every monomial + * @f$r^{a_r}s^{a_s}t^{a_t}@f$ whose superlinear degree (the three-axis form of + * the rule above) is at most @f$p@f$, and the nodal basis is again the inverse + * Vandermonde at the reference nodes. Those nodes are + * distributed by boundary stratum: 8 corners, @f$12(p-1)@f$ edge nodes, + * @f$6\,q(p)@f$ face-interior nodes -- each face carries the 2D quadrilateral + * serendipity interior, since the trace of the cube space on a face is the + * square space -- and a volume interior that is empty until @f$p \ge 6@f$. + * Unisolvence follows the same factorization: a function vanishing on every + * boundary node vanishes on each face by the quadrilateral result above, hence + * is divisible by the cube bubble @f$(1 - r^2)(1 - s^2)(1 - t^2)@f$ with quotient + * in @f$P_{p-6}@f$; the volume-interior nodes form a tetrahedral staircase that + * is unisolvent for @f$P_{p-6}@f$ by induction over @f$t@f$-layers, so the cube + * Vandermonde is nonsingular. + * + * `SerendipityBasis(BasisTopology::Quadrilateral, p)` and + * `SerendipityBasis(BasisTopology::Hexahedron, p)` are the arbitrary-order entry + * points (@f$p \ge 1@f$; orders below one are rejected). Reference nodes for both + * the arbitrary-order and the named paths come from the single + * ReferenceNodeLayout serendipity generator, in a VTK-consistent stratified + * order; for @f$p \ge 3@f$ the interior ordering is an implementation convention + * rather than a public layout. The named fixed layouts -- `ElementType::Quad8` + * (order 2), `Hex8` (order 1), and `Hex20` (order 2) -- are the same construction + * at those orders; the named overload only pins the order, so the named and + * topology constructions produce identical objects and share the single public + * node ordering the solver permutes against (order 1 and order 2 reuse the VTK + * corner/edge ordering exactly). Wedge serendipity remains a single fixed layout + * (Wedge15), constructed only from its named ElementType. Solver-default basis + * selection is separate: `basis_factory` maps the complete Quad4 layout to the + * default linear Lagrange basis and maps Quad8/Hex20 to serendipity unless a + * caller explicitly requests a different supported basis. + * + * Every supported family -- quadrilateral, hexahedral, and Wedge15 -- is built by + * inverting the generalized Vandermonde of its mode space at the public-order + * reference nodes. Quadrilateral and hexahedral bases use tensor Legendre modes; + * the fixed Wedge15 table uses monomial modes. Values, gradients, and Hessians + * are evaluated by differentiating the matching mode vector and applying the + * inverse-Vandermonde coefficients. Because the tables are generated in public + * node order, evaluation needs no output reordering, and there is no hand-written + * special case -- the Hex8 basis is the order-1 instance of the generated + * hexahedral space, not a separate trilinear evaluator. + * + * ## Conditioning and the well-conditioned order range + * + * High-order nodal interpolation is governed by two conditioning factors, both + * addressed so that arbitrary orders produce trustworthy shape functions: + * - **Node distribution.** The quadrilateral and hexahedral families place their + * nodes on the shared Gauss-Lobatto-Legendre (GLL) distribution -- edges, faces, + * and the interior staircase all use the GLL 1D nodes (line_coord_pm_one), whose + * logarithmic Lebesgue constant keeps high-order interpolation well-conditioned. + * The named production layouts are unaffected, since GLL coincides with the + * equispaced layout at orders 1 and 2 (so Quad8/Hex8/Hex20 keep their exact + * public coordinates); the layout is this module's own convention only for + * order >= 3. + * - **Modal basis.** The quadrilateral and hexahedral Vandermondes are assembled + * in a tensor **Legendre** basis rather than raw monomials. The serendipity + * exponent set is downward-closed, so the Legendre and monomial spans are + * identical (the change of basis is triangular) -- the nodal shape functions are + * unchanged -- but the Legendre Vandermonde is far better conditioned. (The + * fixed Wedge15 layout, order 2, keeps the monomial form; it is trivially + * well-conditioned.) + */ +class SerendipityBasis final : public BasisFunction { +public: + /** + * @brief Construct an arbitrary-order quadrilateral or hexahedral serendipity basis. + * + * @details This is the arbitrary-order entry point for the serendipity + * families with a free order: the quadrilateral and the hexahedron. The + * topology carries no node-count assumption; the serendipity polynomial + * space, reference nodes (generated here in VTK-consistent stratified order), + * and nodal coefficient table are built from the requested order (which must + * be @f$p \ge 1@f$). Wedge serendipity is a single fixed layout and is not + * constructed this way -- use the named ElementType overload (Wedge15). + * + * @param topology Must be BasisTopology::Quadrilateral or BasisTopology::Hexahedron. + * @param order Polynomial order @f$p \ge 1@f$; orders below 1 are rejected. + * @throws BasisConfigurationException If @p order is less than 1. + * @throws BasisElementCompatibilityException If @p topology is not Quadrilateral or Hexahedron. + */ + SerendipityBasis(BasisTopology topology, int order); + + /** + * @brief Construct a serendipity basis from a named element layout. + * + * @details Convenience overload for the named, fixed serendipity layouts. + * Each layout is the fixed-order instance of its family, built through the + * same generated construction as the arbitrary-order path and taking its + * nodes from ReferenceNodeLayout: Quad8 is the quadrilateral at order 2, Hex8 + * and Hex20 are the hexahedron at orders 1 and 2, and Wedge15 is the prism + * layout. Each layout carries an inferred fixed order (Hex8 to 1; Quad8, + * Hex20, and Wedge15 to 2); the requested @p order must equal that inferred + * order and is never adjusted to fit, so a mismatched request (including + * order 0 or negative) is rejected. Arbitrary-order quadrilateral and + * hexahedral serendipity is requested through the BasisTopology overload. + * + * @param type Named serendipity element type (Quad8, Hex8, Hex20, or Wedge15). + * @param order Requested order; must equal the layout's inferred fixed order + * (1 for Hex8; 2 for Quad8, Hex20, and Wedge15). + * @throws BasisConfigurationException If @p order does not match the layout's inferred order. + * @throws BasisElementCompatibilityException If the element type is unsupported. + */ + SerendipityBasis(ElementType type, int order); + + /** + * @brief Construct a serendipity basis from a named layout at its fixed order. + * + * @details Single-argument convenience overload for the named serendipity + * layouts: the order is the one fixed by the layout (1 for Hex8; 2 for Quad8, + * Hex20, and Wedge15), so the caller does not repeat it. Equivalent to + * SerendipityBasis(type, ). + * + * @param type Named serendipity element type (Quad8, Hex8, Hex20, or Wedge15). + * @throws BasisElementCompatibilityException If the element type is unsupported. + */ + explicit SerendipityBasis(ElementType type); + + /** @copydoc BasisFunction::basis_type() */ + BasisType basis_type() const noexcept final { return BasisType::Serendipity; } + + /** @copydoc BasisFunction::topology() */ + BasisTopology topology() const noexcept final { return topology_; } + + /** @copydoc BasisFunction::dimension() */ + int dimension() const noexcept final { return dimension_; } + + /** @copydoc BasisFunction::order() */ + int order() const noexcept final { return order_; } + + /** @copydoc BasisFunction::size() */ + std::size_t size() const noexcept final { return size_; } + + /** + * @brief Return the reference interpolation nodes in basis ordering. + * + * @details Node coordinates are the points at which the serendipity basis + * satisfies the nodal interpolation property. All families take their nodes + * from ReferenceNodeLayout, the public node-ordering source the solver adapter + * permutes against: the fixed Wedge15 layout and the quadrilateral/hexahedral + * families (named or arbitrary-order) alike, in VTK-consistent stratified + * order -- corners and edges first (matching the public Quad8/Hex8/Hex20 + * ordering at the named orders), then the face and volume interior points + * needed to make the reduced polynomial space unisolvent. For @f$p \ge 3@f$ + * that interior ordering is an implementation convention; callers should pair + * it with basis values from the same object rather than assume an external + * mesh ordering contract beyond the supported named production layouts. + * + * @return Reference node coordinates, one per basis function. + */ + const std::vector>& nodes() const noexcept final { return nodes_; } + + /** + * @brief Evaluate serendipity basis values into caller-provided storage. + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param values_out Output span with at least size() entries. + */ + void evaluate_values_to(const math::Vector& xi, + std::span values_out) const final; + + /** + * @brief Evaluate serendipity basis gradients into caller-provided storage. + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param gradients_out Output span with at least size() entries. + */ + void evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const final; + + /** + * @brief Evaluate serendipity basis Hessians into caller-provided storage. + * @param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + * @param hessians_out Output span with at least size() entries. + */ + void evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const final; + +private: + BasisTopology topology_{BasisTopology::Unknown}; + int dimension_{0}; + int order_{0}; + std::size_t size_{0}; + std::vector> nodes_; + // Per-axis degrees (a, b, c) of the tensor modes spanning the family's + // polynomial space. Interpreted as monomial powers r^a s^b t^c or, when + // uses_legendre_modes_ is set, as tensor Legendre degrees P_a(r) P_b(s) P_c(t) + // (the same space; see ModalAxisKind in SerendipityBasis.cpp). + std::vector> mode_exponents_; + // Row-major inverse (generalized) Vandermonde, indexed as [mode, basis]. + std::vector inv_vandermonde_; + // Whether the tensor modes are Legendre polynomials (quadrilateral/hexahedral + // families) or plain monomials (the fixed Wedge15 layout). Evaluation must use + // the same family the coefficient table was built with. + bool uses_legendre_modes_{false}; + + // Build the quadrilateral serendipity mode set, nodes, and Legendre + // coefficient table for the given order. (Details at the definition.) + void init_quadrilateral(int order); + // Build the hexahedral serendipity mode set, nodes, and Legendre coefficient + // table for the given order; Hex8/Hex20 are its order-1/order-2 instances. + void init_hexahedron(int order); + // Build the fixed Wedge15 layout from its tabulated monomial mode space. + void init_fixed_named(ElementType type); + + void evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const override; +}; + +/** @} */ + +} // namespace basis +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_BASIS_SERENDIPITYBASIS_H diff --git a/Code/Source/solver/FE/Common/FEException.h b/Code/Source/solver/FE/Common/FEException.h index 67b7da234..cf5a153cd 100644 --- a/Code/Source/solver/FE/Common/FEException.h +++ b/Code/Source/solver/FE/Common/FEException.h @@ -22,61 +22,94 @@ namespace svmp { namespace FE { +/** + * @defgroup FE_CommonExceptions Exceptions + * @ingroup FE_Common + * @brief FE exception hierarchy. + * + * @details All FE-specific exceptions derive from FEException, which itself + * derives from the shared solver ExceptionBase. Specialized subclasses carry + * structured context (element type, DOF index, backend name and error code, + * iteration counts, Jacobian determinants) so call sites can report + * actionable diagnostics. + * + * Throw FE exceptions through the canonical core helpers in Core/Exception.h: + * + * @code + * svmp::raise(message); + * svmp::throw_if(failure_condition, message); + * svmp::check(valid_condition, message); + * svmp::check_not_null(ptr, message); + * svmp::check_index(index, size); + * svmp::not_implemented(message); + * @endcode + * + * check() raises when its (success) condition is false; throw_if() raises when + * its (failure) condition is true. FE owns exception types; helper spelling is + * owned by the core layer. + * @{ + */ + +/** + * @brief Base exception type for errors originating in the FE library + * + * Carries a status code and source location alongside the message. Derived + * classes select an appropriate StatusCode and may attach additional + * structured context. + */ class FEException : public ExceptionBase { public: - FEException(const std::string& message, - StatusCode status = StatusCode::Unknown, - const char* file = "", - int line = 0, - const char* function = "") - : ExceptionBase(message, - status, - "FE Exception", - file, - line, - function) - { - } - - FEException(const std::string& message, - const char* file, - int line, - const char* function = "") - : FEException(message, StatusCode::Unknown, file, line, function) + /** + * @brief Construct with a message and optional status code. + * @param message Human-readable error description. + * @param status Status code classifying the failure. + * + * @details The source location is stamped by svmp::raise(); construct FE + * exceptions through the core helpers rather than passing file/line/function. + */ + explicit FEException(const std::string& message, + StatusCode status = StatusCode::Unknown) + : ExceptionBase(message, status, "FE Exception") { } + /** + * @brief Status code classifying the failure. + * @return The status code recorded at construction. + */ StatusCode status() const noexcept { return status_code(); } }; -class InvalidArgumentException : public FEException { -public: - InvalidArgumentException(const std::string& message, - const char* file = "", - int line = 0, - const char* function = "") - : FEException(message, StatusCode::InvalidArgument, file, line, - function) - { - } -}; +/** + * @brief An argument failed validation + */ +SVMP_DEFINE_EXCEPTION(InvalidArgumentException, FEException, + StatusCode::InvalidArgument); +/** + * @brief Unsupported or malformed element request + * + * Records the offending element type so error reports can name it. + */ class InvalidElementException : public FEException { public: + /** + * @brief Construct with a message and optional element-type context. + * @param message Human-readable error description. + * @param element_type Name of the offending element type; appended to the message when non-empty. + */ InvalidElementException(const std::string& message, - std::string element_type = "", - const char* file = "", - int line = 0, - const char* function = "") + std::string element_type = "") : FEException(build_message(message, element_type), - StatusCode::InvalidArgument, - file, - line, - function), + StatusCode::InvalidArgument), element_type_(std::move(element_type)) { } + /** + * @brief Name of the offending element type. + * @return Element-type name; empty when not provided. + */ const std::string& element_type() const noexcept { return element_type_; } private: @@ -93,23 +126,35 @@ class InvalidElementException : public FEException { std::string element_type_; }; +/** + * @brief Degree-of-freedom numbering or lookup failure + * + * Records the offending DOF index so error reports can name it. + */ class DofException : public FEException { public: + /** + * @brief Construct with a message and optional DOF-index context. + * @param message Human-readable error description. + * @param dof_index Offending DOF index; appended to the message unless it equals invalid_dof_index(). + */ DofException(const std::string& message, - long long dof_index = invalid_dof_index(), - const char* file = "", - int line = 0, - const char* function = "") + long long dof_index = invalid_dof_index()) : FEException(build_message(message, dof_index), - StatusCode::InvalidArgument, - file, - line, - function), + StatusCode::InvalidArgument), dof_index_(dof_index) { } + /** + * @brief Offending DOF index. + * @return DOF index; invalid_dof_index() when not provided. + */ long long dof_index() const noexcept { return dof_index_; } + /** + * @brief Sentinel meaning "no DOF index attached". + * @return The sentinel value -1. + */ static constexpr long long invalid_dof_index() noexcept { return -1; } private: @@ -126,36 +171,44 @@ class DofException : public FEException { long long dof_index_ = invalid_dof_index(); }; -class AssemblyException : public FEException { -public: - AssemblyException(const std::string& message, - const char* file = "", - int line = 0, - const char* function = "") - : FEException(message, StatusCode::InvalidState, file, line, function) - { - } -}; +/** + * @brief Global assembly failure + */ +SVMP_DEFINE_EXCEPTION(AssemblyException, FEException, StatusCode::InvalidState); +/** + * @brief Failure reported by a linear-algebra or solver backend + * + * Records the backend name and its native error code so error reports can + * identify the failing dependency. + */ class BackendException : public FEException { public: + /** + * @brief Construct with a message and optional backend context. + * @param message Human-readable error description. + * @param backend_name Name of the failing backend; appended to the message when non-empty. + * @param error_code Backend-native error code; appended to the message when nonzero. + */ BackendException(const std::string& message, std::string backend_name = "", - int error_code = 0, - const char* file = "", - int line = 0, - const char* function = "") + int error_code = 0) : FEException(build_message(message, backend_name, error_code), - StatusCode::DependencyError, - file, - line, - function), + StatusCode::DependencyError), backend_name_(std::move(backend_name)), error_code_(error_code) { } + /** + * @brief Name of the failing backend. + * @return Backend name; empty when not provided. + */ const std::string& backend_name() const noexcept { return backend_name_; } + /** + * @brief Backend-native error code. + * @return Error code; zero when not provided. + */ int error_code() const noexcept { return error_code_; } private: @@ -185,55 +238,65 @@ class BackendException : public FEException { int error_code_ = 0; }; -class NotImplementedException : public FEException { -public: - NotImplementedException(const std::string& feature, - const char* file = "", - int line = 0, - const char* function = "") - : FEException("Feature not implemented: " + feature, - StatusCode::NotImplemented, - file, - line, - function) - { - } -}; +/** + * @brief Requested feature is not implemented. + * + * @details Alias for svmp::NotImplementedException (Core/Exception.h), the single + * not-implemented type used across the solver and the default raised by + * svmp::not_implemented(). Kept in the FE namespace for source compatibility; it + * derives from CoreException, not FEException. + */ +using NotImplementedException = svmp::NotImplementedException; +/** + * @brief Required initialization step has not been performed + */ class NotInitializedException : public FEException { public: - NotInitializedException(const std::string &feature, - const char *file, - int line = 0, - const char *function = "") + /** + * @brief Construct from the name of the uninitialized feature. + * @param feature Description of the missing initialization. + */ + explicit NotInitializedException(const std::string& feature) : FEException("Missing initialization: " + feature, - StatusCode::InvalidState, - file, - line, - function) + StatusCode::InvalidState) { } }; +/** + * @brief Iterative process failed to converge + * + * Records the iteration count and final residual so error reports can show + * how far the iteration progressed. + */ class ConvergenceException : public FEException { public: + /** + * @brief Construct with a message and optional iteration context. + * @param message Human-readable error description. + * @param iteration Iteration at which the failure was detected; appended to the message when non-negative. + * @param residual Final residual; appended to the message when positive. + */ ConvergenceException(const std::string& message, int iteration = -1, - double residual = 0.0, - const char* file = "", - int line = 0, - const char* function = "") + double residual = 0.0) : FEException(build_message(message, iteration, residual), - StatusCode::InvalidState, - file, - line, - function), + StatusCode::InvalidState), iteration_(iteration), residual_(residual) { } + /** + * @brief Iteration at which the failure was detected. + * @return Iteration count; -1 when not provided. + */ int iteration() const noexcept { return iteration_; } + /** + * @brief Final residual value. + * @return Residual; 0.0 when not provided. + */ double residual() const noexcept { return residual_; } private: @@ -257,22 +320,31 @@ class ConvergenceException : public FEException { double residual_ = 0.0; }; +/** + * @brief Element geometric mapping is singular or inverted + * + * Records the offending Jacobian determinant so error reports can show the + * degeneracy. + */ class SingularMappingException : public FEException { public: + /** + * @brief Construct with a message and the offending Jacobian determinant. + * @param message Human-readable error description. + * @param jacobian_det Jacobian determinant at the failure point; appended to the message. + */ SingularMappingException(const std::string& message, - double jacobian_det = 0.0, - const char* file = "", - int line = 0, - const char* function = "") + double jacobian_det = 0.0) : FEException(build_message(message, jacobian_det), - StatusCode::InvalidState, - file, - line, - function), + StatusCode::InvalidState), jacobian_det_(jacobian_det) { } + /** + * @brief Jacobian determinant at the failure point. + * @return The determinant recorded at construction. + */ double jacobian_det() const noexcept { return jacobian_det_; } private: @@ -285,55 +357,7 @@ class SingularMappingException : public FEException { double jacobian_det_ = 0.0; }; -template -[[noreturn]] inline void raise(SourceLocation location, Args&&... args) -{ - ::svmp::raise(location, std::forward(args)...); -} - -template -inline void throw_if(bool condition, SourceLocation location, Args&&... args) -{ - if (condition) { - ::svmp::FE::raise(location, std::forward(args)...); - } -} - -template -inline void check_arg(bool condition, SourceLocation location, Args&&... args) -{ - ::svmp::check_arg(condition, location, - std::forward(args)...); -} - -template -inline void check_not_null(PointerT ptr, SourceLocation location, - Args&&... args) -{ - ::svmp::check_not_null(ptr, location, std::forward(args)...); -} - -template -inline void check_index(IndexT index, SizeT size, SourceLocation location) -{ - const long long fe_check_index_value = static_cast(index); - const long long fe_check_size_value = static_cast(size); - - ::svmp::FE::check_arg( - fe_check_index_value >= 0 && - fe_check_index_value < fe_check_size_value, - location, - "Index " + std::to_string(fe_check_index_value) + - " out of bounds [0, " + std::to_string(fe_check_size_value) + ")"); -} - -[[noreturn]] inline void not_implemented(const std::string& feature, - SourceLocation location) -{ - ::svmp::FE::raise(location, feature); -} +/** @} */ } // namespace FE } // namespace svmp diff --git a/Code/Source/solver/FE/Common/Types.h b/Code/Source/solver/FE/Common/Types.h new file mode 100644 index 000000000..458fb5649 --- /dev/null +++ b/Code/Source/solver/FE/Common/Types.h @@ -0,0 +1,569 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_TYPES_H +#define SVMP_FE_TYPES_H + +/** + * @file Types.h + * @brief Fundamental type definitions for the finite element library + * + * This header provides core type aliases, enumerations, and strong type + * definitions used throughout the FE library. It establishes a consistent + * type system that integrates with the Mesh library while maintaining + * independence from backend-specific types. + */ + +// The Mesh library is an optional, external module. When the build enables it +// (SVMP_FE_WITH_MESH), FE imports the Mesh scalar/index types so the two libraries +// share a vocabulary; otherwise FE compiles standalone using the fallback +// definitions below (e.g. svmp::CellFamily and the Mesh* aliases). The Mesh +// headers are not part of this repository. +#if defined(SVMP_FE_WITH_MESH) && SVMP_FE_WITH_MESH +# include "Mesh/Core/MeshTypes.h" +/** Nonzero when FE shares scalar/index types with the Mesh library. */ +# define SVMP_FE_HAS_MESH_TYPES 1 +#else +// Build FE without Mesh types unless explicitly enabled. +/** Nonzero when FE shares scalar/index types with the Mesh library. */ +# define SVMP_FE_HAS_MESH_TYPES 0 +#endif + +#if !SVMP_FE_HAS_MESH_TYPES +namespace svmp { +#ifndef SVMP_CELL_FAMILY_DEFINED +/** Guard marking that svmp::CellFamily has been defined. */ +#define SVMP_CELL_FAMILY_DEFINED 1 +/** + * @brief Minimal fallback for svmp::CellFamily when the Mesh library is unavailable + * @ingroup FE_CommonTypes + * + * Keeps FE compilation self-contained while preserving the same namespace + * and enumerator set as the Mesh library's cell-family classification. + */ +enum class CellFamily { + Point, + Line, + Triangle, + Quad, + Tetra, + Hex, + Wedge, + Pyramid, + Polygon, + Polyhedron +}; +#endif +} // namespace svmp +#endif +#include +#include +#include +#include +#include +#include + +/** + * @defgroup FE_Common Common + * @ingroup FE + * @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. + */ + +namespace svmp { +namespace FE { + +/** + * @defgroup FE_CommonTypes Types + * @ingroup FE_Common + * @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. + * @{ + */ + +// ============================================================================ +// Index Types +// ============================================================================ + +/** + * @brief Local index type for element-level operations + * + * Used for local node numbering within elements, local DOF indices, + * and other element-local indexing. Unsigned for safety. + */ +using LocalIndex = std::uint32_t; + +/** + * @brief Global index type for distributed DOF numbering + * + * Signed 64-bit for compatibility with PETSc and Trilinos. + * Negative values can indicate special conditions or invalid indices. + * + * @note Kept as a plain integer alias rather than a StrongType wrapper: this is + * the raw interop type handed directly to PETSc/Trilinos, where a wrapper would + * force an unwrap at every call. Type safety for DOF indices is provided by + * DofIndex (below), the strong wrapper around a GlobalIndex. + */ +using GlobalIndex = std::int64_t; + +/** + * @brief DOF-specific index type + * + * Strong type alias to prevent mixing DOF indices with other indices. + * Provides type safety at compile time. It is hand-rolled to carry an invalid + * sentinel and is_valid(); QuadraturePointIndex uses the general StrongType + * template for the same strong-typing purpose. + */ +struct DofIndex { + GlobalIndex value; ///< Underlying global DOF index; negative values are invalid. + + /** + * @brief Construct a DOF index, defaulting to the invalid sentinel. + * @param v Global DOF index value. + */ + constexpr explicit DofIndex(GlobalIndex v = -1) noexcept : value(v) {} + /** + * @brief Convert to the underlying global index value. + * @return The stored global index. + */ + constexpr operator GlobalIndex() const noexcept { return value; } + /** + * @brief Check whether this index refers to a valid DOF. + * @return True when the stored value is non-negative. + */ + constexpr bool is_valid() const noexcept { return value >= 0; } +}; + +/** + * @brief Field identifier type + * + * Used to distinguish between different physical fields in multi-field problems. + */ +using FieldId = std::uint16_t; + +/** + * @brief Block identifier for block-structured systems + */ +using BlockId = std::uint16_t; + +// Import mesh library scalar/index types when available (optional dependency). +#if SVMP_FE_HAS_MESH_TYPES +using MeshIndex = svmp::index_t; ///< Local mesh entity index, shared with the Mesh library. +using MeshOffset = svmp::offset_t; ///< Offset type for mesh connectivity arrays. +using MeshGlobalId = svmp::gid_t; ///< Global mesh entity identifier. +#else +using MeshIndex = std::int32_t; ///< Local mesh entity index, shared with the Mesh library. +using MeshOffset = std::int64_t; ///< Offset type for mesh connectivity arrays. +using MeshGlobalId = std::int64_t; ///< Global mesh entity identifier. +#endif + +// ============================================================================ +// Constants +// ============================================================================ + +/** Sentinel for an unset or out-of-range local index. */ +constexpr LocalIndex INVALID_LOCAL_INDEX = std::numeric_limits::max(); +/** Sentinel for an unset or out-of-range global index. */ +constexpr GlobalIndex INVALID_GLOBAL_INDEX = -1; +/** Sentinel FieldId meaning "uninitialized / no field". */ +constexpr FieldId INVALID_FIELD_ID = std::numeric_limits::max(); +/** + * Sentinel FieldId for geometry-only quantities (no DOF dependence). + * Uses first registered field's space for quadrature, but logically decoupled + * from any specific field's DOFs. + */ +constexpr FieldId GEOMETRY_FIELD_ID = std::numeric_limits::max() - 1; +/** Sentinel for an unset or out-of-range block identifier. */ +constexpr BlockId INVALID_BLOCK_ID = std::numeric_limits::max(); + +/** + * @brief Sentinel FieldId representing "the current solution state" in tangent forms. + * + * When differentiating a residual form to obtain the tangent (Jacobian), undifferentiated + * TrialFunction occurrences are rewritten to StateField nodes. Those that represent the + * block's own primary unknown (rather than a named external field) use this sentinel + * FieldId. The assembler maps it to the current solution coefficients at each quadrature + * point, regardless of which physics or field variables are involved. + * + * This is distinct from INVALID_FIELD_ID, which means "uninitialized / no field." + * CURRENT_SOLUTION_FIELD_ID uses the same numeric value for backward compatibility + * with existing KernelIR encodings, but carries explicit semantic intent. + */ +constexpr FieldId CURRENT_SOLUTION_FIELD_ID = std::numeric_limits::max(); + +/** Preferred cache-line/SIMD alignment for performance-critical arrays. */ +inline constexpr std::size_t kFEPreferredAlignmentBytes = 64u; + +/** Alignment for small fixed-size math objects that are commonly passed by value. */ +inline constexpr std::size_t kFEFixedObjectAlignmentBytes = 32u; + +// ============================================================================ +// Field Value Entry (for point evaluation of field-dependent expressions) +// ============================================================================ + +/** Maximum number of components in a FieldValueEntry (3x3 tensor). */ +constexpr int MAX_FIELD_VALUE_COMPONENTS = 9; + +/** + * @brief Field value at an evaluation point — scalar, vector, or tensor. + * + * Used by PointEvaluator and the auxiliary assembly path to supply FE + * field values at entity locations (e.g., nodal DOF values for + * Node-scoped auxiliary models with Lagrange Kronecker delta). + */ +struct FieldValueEntry { + FieldId field{INVALID_FIELD_ID}; ///< Field this value belongs to. + int n_components{0}; ///< Number of valid entries in components. + double components[MAX_FIELD_VALUE_COMPONENTS]{}; ///< Component values, row-major for tensors. +}; + +// ============================================================================ +// Element Type Enumerations +// ============================================================================ + +/** + * @brief Reference element types supported by the FE library + * + * Maps to svmp::CellFamily from the Mesh library but provides + * FE-specific categorization including higher-order variants. + * + * @note The explicit enumerator values are intentional and grouped into bands: + * linear (0-6), quadratic (10-20), and special (Point1 = 30; Unknown = 255, the + * uint8_t sentinel). The enum is consumed via its names, not the numeric values, + * but the banding keeps related types together and leaves room to extend each + * group; keep new entries within their band. + */ +enum class ElementType : std::uint8_t { + // Linear elements + Line2 = 0, ///< 2-node line + Triangle3 = 1, ///< 3-node triangle + Quad4 = 2, ///< 4-node quadrilateral + Tetra4 = 3, ///< 4-node tetrahedron + Hex8 = 4, ///< 8-node hexahedron + Wedge6 = 5, ///< 6-node wedge/prism + Pyramid5 = 6, ///< 5-node pyramid + + // Quadratic elements + Line3 = 10, ///< 3-node line + Triangle6 = 11, ///< 6-node triangle + Quad9 = 12, ///< 9-node quadrilateral (bi-quadratic) + Quad8 = 13, ///< 8-node quadrilateral (serendipity) + Tetra10 = 14, ///< 10-node tetrahedron + Hex27 = 15, ///< 27-node hexahedron (tri-quadratic) + Hex20 = 16, ///< 20-node hexahedron (serendipity) + Wedge15 = 17, ///< 15-node wedge + Wedge18 = 18, ///< 18-node wedge (complete quadratic) + Pyramid13 = 19, ///< 13-node pyramid + Pyramid14 = 20, ///< 14-node pyramid + + // Special elements + Point1 = 30, ///< 1-node point element + + Unknown = 255 ///< 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 + */ +enum class BasisType : std::uint8_t { + Lagrange, ///< Standard nodal Lagrange basis + NURBS, ///< Non-uniform rational B-splines (reserved; not yet implemented) + Serendipity, ///< Serendipity elements + Custom ///< User-defined basis +}; + +/** + * @brief Field types for function spaces + */ +enum class FieldType : std::uint8_t { + Scalar, ///< Scalar field (temperature, pressure) + Vector, ///< Vector field (velocity, displacement) + Tensor, ///< Tensor field (stress, strain) + SymmetricTensor, ///< Symmetric tensor field + Mixed ///< Mixed/composite field +}; + +/** + * @brief Continuity requirements for function spaces + */ +enum class Continuity : std::uint8_t { + C0, ///< Continuous (standard FEM) + C1, ///< C1 continuous (for plates/shells) + L2, ///< L2 (discontinuous) + H_div, ///< H(div) conforming + H_curl, ///< H(curl) conforming + Custom ///< User-defined continuity requirement +}; + +/** + * @brief Assembly strategies + */ +enum class AssemblyStrategy : std::uint8_t { + ElementByElement, ///< Traditional element loop + Vectorized, ///< SIMD vectorized assembly + MatrixFree, ///< Matrix-free operators + Hybrid ///< Mixed strategy +}; + +// ============================================================================ +// Geometric Types +// ============================================================================ + +/** + * @brief Point in reference element coordinates + * @tparam Dim Reference-space dimension + */ +template +using ReferencePoint = std::array(Dim)>; + +/** + * @brief Point in physical coordinates + */ +using PhysicalPoint = std::array; + +/** + * @brief Jacobian matrix type + * @tparam SpatialDim Physical-space dimension (rows) + * @tparam ReferenceDim Reference-space dimension (columns) + */ +template +using Jacobian = std::array(ReferenceDim)>, static_cast(SpatialDim)>; + +// ============================================================================ +// Strong Type Wrappers (C++17 idiom for type safety) +// ============================================================================ + +/** + * @brief Strong type wrapper template for type-safe programming + * + * Prevents accidental mixing of conceptually different types that have + * the same underlying representation. + * + * @tparam T Underlying value type + * @tparam Tag Empty tag type that distinguishes otherwise identical wrappers + */ +template +class StrongType { +public: + /** @brief Underlying value type. */ + using ValueType = T; + + /** @brief Value-initialize the wrapped value. */ + constexpr StrongType() noexcept(std::is_nothrow_default_constructible_v) + : value_{} {} + + /** + * @brief Wrap an explicit value. + * @param value Value to store. + */ + constexpr explicit StrongType(T value) noexcept(std::is_nothrow_move_constructible_v) + : value_(std::move(value)) {} + + /** + * @brief Access the wrapped value. + * @return Reference to the wrapped value. + */ + constexpr T& get() noexcept { return value_; } + /** + * @brief Access the wrapped value. + * @return Reference to the wrapped value. + */ + constexpr const T& get() const noexcept { return value_; } + + /** + * @brief Explicitly convert back to the underlying type. + * @return Copy of the wrapped value. + */ + constexpr explicit operator T() const noexcept { return value_; } + + /** + * @brief Compare wrapped values for equality. + * @param other Wrapper to compare against. + * @return True when the wrapped values are equal. + */ + constexpr bool operator==(const StrongType& other) const noexcept { + return value_ == other.value_; + } + /** + * @brief Compare wrapped values for inequality. + * @param other Wrapper to compare against. + * @return True when the wrapped values differ. + */ + constexpr bool operator!=(const StrongType& other) const noexcept { + return value_ != other.value_; + } + /** + * @brief Order by wrapped value. + * @param other Wrapper to compare against. + * @return True when this wrapped value orders before the other. + */ + constexpr bool operator<(const StrongType& other) const noexcept { + return value_ < other.value_; + } + +private: + T value_; +}; + +// Specific strong types for common use cases +struct QuadraturePointTag {}; ///< Tag type for quadrature-point indices. +struct QuadratureWeightTag {}; ///< Tag type for quadrature weights. +struct BasisValueTag {}; ///< Tag type for basis-function values. +struct BasisGradientTag {}; ///< Tag type for basis-function gradients. + +/** Type-safe index of a quadrature point within a rule. */ +using QuadraturePointIndex = StrongType; +/** Type-safe quadrature weight value. */ +using QuadratureWeight = StrongType; + +// ============================================================================ +// Type Traits +// ============================================================================ + +/** + * @brief Check if a type is a valid index type + */ +template +struct is_index_type : std::false_type {}; + +template<> +struct is_index_type : std::true_type {}; + +template<> +struct is_index_type : std::true_type {}; + +template<> +struct is_index_type : std::true_type {}; + +/** Convenience variable template for is_index_type. */ +template +inline constexpr bool is_index_type_v = is_index_type::value; + +/** + * @brief Check if a type represents a field type + */ +template +struct is_field_type : std::false_type {}; + +template<> +struct is_field_type : std::true_type {}; + +/** Convenience variable template for is_field_type. */ +template +inline constexpr bool is_field_type_v = is_field_type::value; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * @brief Convert FE ElementType to Mesh CellFamily + * @param elem Element type to classify. + * @return Cell family of the element's linear topology; Point for unknown types. + */ +constexpr svmp::CellFamily to_mesh_family(ElementType elem) noexcept { + switch(elem) { + case ElementType::Line2: + case ElementType::Line3: + return svmp::CellFamily::Line; + + case ElementType::Triangle3: + case ElementType::Triangle6: + return svmp::CellFamily::Triangle; + + case ElementType::Quad4: + case ElementType::Quad8: + case ElementType::Quad9: + return svmp::CellFamily::Quad; + + case ElementType::Tetra4: + case ElementType::Tetra10: + return svmp::CellFamily::Tetra; + + case ElementType::Hex8: + case ElementType::Hex20: + case ElementType::Hex27: + return svmp::CellFamily::Hex; + + case ElementType::Wedge6: + case ElementType::Wedge15: + case ElementType::Wedge18: + return svmp::CellFamily::Wedge; + + case ElementType::Pyramid5: + case ElementType::Pyramid13: + case ElementType::Pyramid14: + return svmp::CellFamily::Pyramid; + + case ElementType::Point1: + return svmp::CellFamily::Point; + + default: + return svmp::CellFamily::Point; // Fallback + } +} + +/** + * @brief Get spatial dimension of element type + * @param elem Element type to query. + * @return Reference dimension from 0 (point) to 3 (volume); -1 for unknown types. + */ +constexpr int element_dimension(ElementType elem) noexcept { + switch(elem) { + case ElementType::Point1: + return 0; + case ElementType::Line2: + case ElementType::Line3: + return 1; + case ElementType::Triangle3: + case ElementType::Triangle6: + case ElementType::Quad4: + case ElementType::Quad8: + case ElementType::Quad9: + return 2; + case ElementType::Tetra4: + case ElementType::Tetra10: + case ElementType::Hex8: + case ElementType::Hex20: + case ElementType::Hex27: + case ElementType::Wedge6: + case ElementType::Wedge15: + case ElementType::Wedge18: + case ElementType::Pyramid5: + case ElementType::Pyramid13: + case ElementType::Pyramid14: + return 3; + default: + return -1; + } +} + +/** @} */ + +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_TYPES_H diff --git a/Code/Source/solver/FE/FE.h b/Code/Source/solver/FE/FE.h new file mode 100644 index 000000000..125660942 --- /dev/null +++ b/Code/Source/solver/FE/FE.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_FE_H +#define SVMP_FE_FE_H + +/** + * @file FE.h + * @brief Library-level Doxygen group for the finite-element support code. + * + * 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). + */ + +/** + * @defgroup FE FE Library + * @brief Finite-element interfaces and utilities used by the solver. + * + * The FE library groups basis functions, math utilities, assembly interfaces, + * and related support code that can be built and consumed as a coherent + * finite-element component. + */ + +#endif // SVMP_FE_FE_H diff --git a/Code/Source/solver/FE/Math/DenseLinearAlgebra.cpp b/Code/Source/solver/FE/Math/DenseLinearAlgebra.cpp new file mode 100644 index 000000000..86884740c --- /dev/null +++ b/Code/Source/solver/FE/Math/DenseLinearAlgebra.cpp @@ -0,0 +1,341 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "DenseLinearAlgebra.h" + +#include "FEException.h" + +#include + +#include +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace math { + +namespace { + +using DenseMatrix = Eigen::Matrix; +using RowMajorMatrix = + Eigen::Matrix; +using ConstRowMajorMap = Eigen::Map; + +ConstRowMajorMap map_row_major(std::span matrix, + std::size_t rows, + std::size_t cols) { + return ConstRowMajorMap(matrix.data(), + static_cast(rows), + static_cast(cols)); +} + +void copy_to_row_major(const DenseMatrix& source, std::vector& dest) { + const auto rows = static_cast(source.rows()); + const auto cols = static_cast(source.cols()); + dest.resize(rows * cols); + Eigen::Map(dest.data(), source.rows(), source.cols()) = source; +} + +} // namespace + +struct DenseLUSolver::Impl { + Eigen::PartialPivLU lu; +}; + +DenseLUSolver::DenseLUSolver() = default; +DenseLUSolver::~DenseLUSolver() = default; +DenseLUSolver::DenseLUSolver(DenseLUSolver&&) noexcept = default; +DenseLUSolver& DenseLUSolver::operator=(DenseLUSolver&&) noexcept = default; + +double dense_matrix_max_abs(std::span matrix) noexcept { + double max_abs = double(0); + for (const double value : matrix) { + max_abs = std::max(max_abs, std::abs(value)); + } + return max_abs; +} + +double dense_matrix_pivot_tolerance(std::size_t rows, + std::size_t cols, + double max_abs, + double multiplier) noexcept { + const double size_scale = static_cast(std::max(rows, cols)); + const double value_scale = std::max(double(1), max_abs); + return multiplier * std::numeric_limits::epsilon() * + std::max(double(1), size_scale) * value_scale; +} + +double dense_matrix_singular_value_tolerance(std::size_t rows, + std::size_t cols, + double largest_singular_value, + double multiplier) noexcept { + const double size_scale = static_cast(std::max(rows, cols)); + return multiplier * std::numeric_limits::epsilon() * + std::max(double(1), size_scale) * + std::max(double(1), largest_singular_value); +} + +double dense_matrix_condition_fallback_threshold() noexcept { + return double(1.0e12); +} + +double dense_matrix_condition_error_threshold() noexcept { + return double(1.0e14); +} + +void DenseLUSolver::solve_in_place(std::span rhs) const { + solve_in_place(rhs, 1u); +} + +void DenseLUSolver::solve_in_place(std::span rhs, + std::size_t rhs_count) const { + ::svmp::check( + rhs_count > 0, error_message_label + ": dense solve requires at least one right-hand side"); + ::svmp::check( + rhs.size() == n * rhs_count, error_message_label + ": dense multi-RHS solve size mismatch"); + ::svmp::check( + impl && impl->lu.rows() == static_cast(n), error_message_label + ": dense solver is not factorized"); + if (n == 0) { + return; + } + + Eigen::Map rhs_map(rhs.data(), + static_cast(n), + static_cast(rhs_count)); + // Evaluate into a temporary: lu.solve cannot alias its argument. + const DenseMatrix solution = impl->lu.solve(rhs_map); + rhs_map = solution; +} + +std::vector DenseLUSolver::solve(std::span rhs) const { + std::vector x(rhs.begin(), rhs.end()); + solve_in_place(std::span(x.data(), x.size())); + return x; +} + +DenseMatrixDiagnostics dense_matrix_diagnostics( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view error_message_label) { + ::svmp::check( + matrix.size() == rows * cols, std::string(error_message_label) + ": diagnostic size mismatch"); + ::svmp::check( + rows > 0 && cols > 0, std::string(error_message_label) + ": diagnostics require a nonempty matrix"); + + const DenseMatrix dense = map_row_major(matrix, rows, cols); + Eigen::JacobiSVD svd(dense); + + DenseMatrixDiagnostics diagnostics; + const auto& singular_values = svd.singularValues(); + diagnostics.largest_singular_value = + (singular_values.size() > 0) ? singular_values[0] : double(0); + diagnostics.tolerance = + dense_matrix_singular_value_tolerance(rows, cols, + diagnostics.largest_singular_value); + + for (Eigen::Index i = 0; i < singular_values.size(); ++i) { + const double sigma = singular_values[i]; + if (sigma <= diagnostics.tolerance) { + continue; + } + ++diagnostics.rank; + diagnostics.smallest_retained_singular_value = sigma; + } + + const std::size_t full_rank = std::min(rows, cols); + if (diagnostics.rank == full_rank && + diagnostics.smallest_retained_singular_value > double(0)) { + diagnostics.condition_estimate = + diagnostics.largest_singular_value / + diagnostics.smallest_retained_singular_value; + } + return diagnostics; +} + +DenseLUSolver factor_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view error_message_label) { + ::svmp::check( + matrix.size() == n * n, std::string(error_message_label) + ": dense factorization size mismatch"); + + DenseLUSolver solver; + solver.n = n; + solver.error_message_label = std::string(error_message_label); + const double max_abs = + dense_matrix_max_abs(std::span(matrix.data(), matrix.size())); + solver.pivot_tolerance = dense_matrix_pivot_tolerance(n, n, max_abs); + + solver.impl = std::make_unique(); + solver.impl->lu.compute(map_row_major(matrix, n, n)); + + // Partial pivoting leaves the pivots on the diagonal of the packed LU + // factor; a pivot below the scale-aware tolerance marks rank deficiency. + double max_pivot_abs = double(0); + double min_pivot_abs = std::numeric_limits::infinity(); + const auto diagonal = solver.impl->lu.matrixLU().diagonal(); + for (Eigen::Index col = 0; col < diagonal.size(); ++col) { + const double pivot_magnitude = std::abs(diagonal[col]); + ::svmp::check( + pivot_magnitude > solver.pivot_tolerance, solver.error_message_label + ": rank-deficient matrix (rank " + + std::to_string(col) + " of " + std::to_string(n) + + ", pivot below scale-aware tolerance " + + std::to_string(solver.pivot_tolerance) + ")"); + max_pivot_abs = std::max(max_pivot_abs, pivot_magnitude); + min_pivot_abs = std::min(min_pivot_abs, pivot_magnitude); + } + + // PartialPivLU is not rank-revealing, so expose only what the pivots + // legitimately convey: the factorization passed the pivot-tolerance check + // above (full rank) and the pivot magnitudes. + solver.diagnostics.rank = n; + solver.diagnostics.tolerance = solver.pivot_tolerance; + solver.max_pivot = max_pivot_abs; + solver.min_pivot = std::isfinite(min_pivot_abs) ? min_pivot_abs : double(0); + return solver; +} + +DenseInverseResult invert_dense_matrix_with_diagnostics( + std::vector matrix, + std::size_t n, + std::string_view error_message_label) { + ::svmp::check( + matrix.size() == n * n, std::string(error_message_label) + ": dense inverse size mismatch"); + std::vector matrix_for_lu = matrix; + const DenseLUSolver solver = + factor_dense_matrix(std::move(matrix_for_lu), n, error_message_label); + + DenseInverseResult result; + result.diagnostics = + dense_matrix_diagnostics(std::span(matrix.data(), matrix.size()), + n, n, error_message_label); + + if (std::isfinite(result.diagnostics.condition_estimate) && + result.diagnostics.condition_estimate > dense_matrix_condition_fallback_threshold()) { + const DenseMatrix dense = map_row_major(matrix, n, n); + Eigen::JacobiSVD svd(dense, + Eigen::ComputeFullU | Eigen::ComputeFullV); + DenseMatrix sigma_inverse = DenseMatrix::Zero(static_cast(n), + static_cast(n)); + const auto& singular_values = svd.singularValues(); + for (Eigen::Index i = 0; i < singular_values.size(); ++i) { + // Defensive: this branch runs only when condition_estimate is finite, + // and dense_matrix_diagnostics leaves it infinite whenever it drops a + // singular value (rank < full_rank). A sub-tolerance singular value + // therefore cannot reach here in current code; the guard protects + // against future refactors that derive the fallback condition differently. + ::svmp::check( + singular_values[i] > result.diagnostics.tolerance, std::string(error_message_label) + ": high-condition SVD fallback encountered a dropped singular value"); + sigma_inverse(i, i) = double(1) / singular_values[i]; + } + const DenseMatrix inverse = svd.matrixV() * sigma_inverse * svd.matrixU().transpose(); + copy_to_row_major(inverse, result.inverse); + result.used_svd_fallback = true; + return result; + } + + const DenseMatrix inverse = solver.impl->lu.inverse(); + copy_to_row_major(inverse, result.inverse); + return result; +} + +void validate_dense_inverse_diagnostics( + const DenseInverseResult& result, + std::size_t expected_rank, + std::string_view error_message_label, + double max_condition) { + ::svmp::check( + result.diagnostics.rank == expected_rank, std::string(error_message_label) + ": rank-deficient matrix (rank " + + std::to_string(result.diagnostics.rank) + " of " + + std::to_string(expected_rank) + ")"); + + if (!std::isfinite(result.diagnostics.condition_estimate)) { + return; + } + + ::svmp::check( + result.diagnostics.condition_estimate <= max_condition, std::string(error_message_label) + ": condition estimate " + + std::to_string(result.diagnostics.condition_estimate) + + " exceeds supported threshold " + std::to_string(max_condition)); +} + +std::vector invert_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view error_message_label) { + const DenseLUSolver solver = factor_dense_matrix(std::move(matrix), n, error_message_label); + const DenseMatrix inverse = solver.impl->lu.inverse(); + std::vector result; + copy_to_row_major(inverse, result); + return result; +} + +std::size_t dense_matrix_rank(std::vector matrix, + std::size_t rows, + std::size_t cols) { + ::svmp::check( + matrix.size() == rows * cols, "dense_matrix_rank: size mismatch"); + + const DenseMatrix dense = + map_row_major(std::span(matrix.data(), matrix.size()), rows, cols); + Eigen::JacobiSVD svd(dense); + + const auto& singular_values = svd.singularValues(); + const double largest = + (singular_values.size() > 0) ? singular_values[0] : double(0); + const double tolerance = + dense_matrix_singular_value_tolerance(rows, cols, largest); + + std::size_t rank = 0; + for (Eigen::Index i = 0; i < singular_values.size(); ++i) { + if (singular_values[i] > tolerance) { + ++rank; + } + } + return rank; +} + +DensePseudoInverseResult rank_revealing_pseudo_inverse( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view error_message_label) { + ::svmp::check( + matrix.size() == rows * cols, std::string(error_message_label) + ": pseudo-inverse size mismatch"); + ::svmp::check( + rows > 0 && cols > 0, std::string(error_message_label) + ": pseudo-inverse requires a nonempty matrix"); + + const DenseMatrix dense = map_row_major(matrix, rows, cols); + Eigen::JacobiSVD svd(dense, Eigen::ComputeFullU | Eigen::ComputeFullV); + + DensePseudoInverseResult result; + + const auto& singular_values = svd.singularValues(); + result.largest_singular_value = + (singular_values.size() > 0) ? singular_values[0] : double(0); + result.tolerance = + dense_matrix_singular_value_tolerance(rows, cols, result.largest_singular_value); + + DenseMatrix sigma_inverse = DenseMatrix::Zero(static_cast(cols), + static_cast(rows)); + for (Eigen::Index i = 0; i < singular_values.size(); ++i) { + const double sigma = singular_values[i]; + if (sigma <= result.tolerance) { + continue; + } + sigma_inverse(i, i) = double(1) / sigma; + ++result.rank; + result.smallest_retained_singular_value = sigma; + } + + const DenseMatrix pseudo_inverse = + svd.matrixV() * sigma_inverse * svd.matrixU().transpose(); + copy_to_row_major(pseudo_inverse, result.inverse); + return result; +} + +} // namespace math +} // namespace FE +} // namespace svmp diff --git a/Code/Source/solver/FE/Math/DenseLinearAlgebra.h b/Code/Source/solver/FE/Math/DenseLinearAlgebra.h new file mode 100644 index 000000000..df45224a1 --- /dev/null +++ b/Code/Source/solver/FE/Math/DenseLinearAlgebra.h @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_MATH_DENSELINEARALGEBRA_H +#define SVMP_FE_MATH_DENSELINEARALGEBRA_H + +#include "Types.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace svmp { +namespace FE { +namespace math { + +// Dense solve, inverse, rank, and pseudo-inverse support for FE construction +// utilities. Matrices are row-major: matrix[row * cols + col]. The +// error_message_label argument on the routines below is used only to prefix the +// diagnostic message of any exception they throw. + +/** + * @brief Largest absolute entry of a dense matrix. + * @ingroup FE_Math + * @param matrix Row-major matrix entries. + * @return Maximum of |entry| over all entries, or 0 for an empty matrix. + */ +[[nodiscard]] double dense_matrix_max_abs(std::span matrix) noexcept; + +/** + * @brief Scale-aware pivot tolerance for dense factorization. + * @ingroup FE_Math + * + * @details Proportional to machine epsilon scaled by the matrix size and + * magnitude; pivots below it are treated as rank-deficient. + * + * @param rows Row count. + * @param cols Column count. + * @param max_abs Largest absolute matrix entry (see dense_matrix_max_abs()). + * @param multiplier Safety factor applied to the epsilon-scaled tolerance. + * @return Pivot magnitude threshold. + */ +[[nodiscard]] double dense_matrix_pivot_tolerance(std::size_t rows, + std::size_t cols, + double max_abs, + double multiplier = double(64)) noexcept; + +/** + * @brief Scale-aware singular-value tolerance for rank decisions. + * @ingroup FE_Math + * + * @details Singular values at or below the returned tolerance are treated as + * zero when computing rank or a pseudo-inverse. + * + * @param rows Row count. + * @param cols Column count. + * @param largest_singular_value Largest singular value of the matrix. + * @param multiplier Safety factor applied to the epsilon-scaled tolerance. + * @return Singular-value threshold. + */ +[[nodiscard]] double dense_matrix_singular_value_tolerance(std::size_t rows, + std::size_t cols, + double largest_singular_value, + double multiplier = double(64)) noexcept; + +/** @brief Result of a rank-revealing pseudo-inverse. @ingroup FE_Math */ +struct DensePseudoInverseResult { + std::vector inverse; ///< Row-major pseudo-inverse. + std::size_t rank{0}; ///< Numerical rank at the chosen tolerance. + double tolerance{0}; ///< Singular-value tolerance used. + double largest_singular_value{0}; ///< Largest singular value. + double smallest_retained_singular_value{0}; ///< Smallest singular value kept. +}; + +/** @brief SVD-based conditioning and rank diagnostics for a dense matrix. @ingroup FE_Math */ +struct DenseMatrixDiagnostics { + std::size_t rank{0}; ///< Numerical rank at @ref tolerance. + double tolerance{0}; ///< Singular-value tolerance used. + double largest_singular_value{0}; ///< Largest singular value. + double smallest_retained_singular_value{0}; ///< Smallest singular value kept. + double condition_estimate{std::numeric_limits::infinity()}; ///< Condition estimate; infinite when rank-deficient. +}; + +/** @brief A dense inverse together with its diagnostics. @ingroup FE_Math */ +struct DenseInverseResult { + std::vector inverse; ///< Row-major inverse. + DenseMatrixDiagnostics diagnostics; ///< Conditioning/rank diagnostics of the input. + bool used_svd_fallback{false}; ///< True when an SVD fallback was used for a high-condition matrix. +}; + +/** + * @brief Condition estimate above which the inverse switches to an SVD fallback. + * @ingroup FE_Math + * @return The fallback condition-number threshold. + */ +[[nodiscard]] double dense_matrix_condition_fallback_threshold() noexcept; +/** + * @brief Condition estimate above which validation rejects a dense inverse. + * @ingroup FE_Math + * @return The error condition-number threshold. + */ +[[nodiscard]] double dense_matrix_condition_error_threshold() noexcept; + +/** + * @brief LU factorization of a dense square matrix with a cached pivot summary. + * @ingroup FE_Math + * + * @details Produced by factor_dense_matrix(); move-only because it owns the Eigen + * factorization. @ref error_message_label prefixes the messages of exceptions + * thrown by the solve methods. + */ +struct DenseLUSolver { + struct Impl; + + DenseLUSolver(); + ~DenseLUSolver(); + DenseLUSolver(DenseLUSolver&&) noexcept; + DenseLUSolver& operator=(DenseLUSolver&&) noexcept; + DenseLUSolver(const DenseLUSolver&) = delete; + DenseLUSolver& operator=(const DenseLUSolver&) = delete; + + std::size_t n{0}; ///< Matrix dimension. + DenseMatrixDiagnostics diagnostics; ///< Pivot-derived diagnostics (rank, tolerance). + double pivot_tolerance{0}; ///< Scale-aware pivot tolerance used. + double min_pivot{0}; ///< Smallest pivot magnitude. + double max_pivot{0}; ///< Largest pivot magnitude. + std::string error_message_label; ///< Prefix for solve-time exception messages. + std::unique_ptr impl; ///< Eigen factorization (pimpl). + + /** + * @brief Whether the factorization is empty (n == 0). + * @return True when no matrix has been factored. + */ + [[nodiscard]] bool empty() const noexcept { return n == 0; } + + /** + * @brief Solve A x = rhs in place for a single right-hand side. + * @param rhs On entry the right-hand side; on return the solution (size n). + */ + void solve_in_place(std::span rhs) const; + /** + * @brief Solve A X = RHS in place for several right-hand sides. + * @param rhs Row-major block of size n * rhs_count (solutions on return). + * @param rhs_count Number of right-hand sides. + */ + void solve_in_place(std::span rhs, std::size_t rhs_count) const; + /** + * @brief Solve A x = rhs and return the solution. + * @param rhs Right-hand side of size n. + * @return Solution vector of size n. + */ + [[nodiscard]] std::vector solve(std::span rhs) const; +}; + +// Inverses and pseudo-inverses keep the same row-major convention for their +// returned dimensions. + +/** + * @brief SVD-based rank and conditioning diagnostics for a dense matrix. + * @ingroup FE_Math + * @param matrix Row-major matrix of size rows * cols. + * @param rows Row count. + * @param cols Column count. + * @param error_message_label Prefix for the message of any exception thrown. + * @return Rank, tolerance, singular-value, and condition diagnostics. + * @throws FEException If the matrix size is inconsistent or the matrix is empty. + */ +[[nodiscard]] DenseMatrixDiagnostics dense_matrix_diagnostics( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view error_message_label = "dense matrix"); + +/** + * @brief LU-factor a dense square matrix. + * @ingroup FE_Math + * @param matrix Row-major n * n matrix (consumed). + * @param n Matrix dimension. + * @param error_message_label Prefix for the message of any exception thrown. + * @return The factorization. + * @throws FEException If the size is inconsistent or the matrix is rank-deficient. + */ +[[nodiscard]] DenseLUSolver factor_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view error_message_label = "dense matrix"); + +/** + * @brief Invert a dense square matrix. + * @ingroup FE_Math + * @param matrix Row-major n * n matrix (consumed). + * @param n Matrix dimension. + * @param error_message_label Prefix for the message of any exception thrown. + * @return Row-major inverse of size n * n. + * @throws FEException If the size is inconsistent or the matrix is singular. + */ +[[nodiscard]] std::vector invert_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view error_message_label = "dense matrix"); + +/** + * @brief Invert a dense square matrix with diagnostics, using an SVD fallback for + * high-condition matrices. + * @ingroup FE_Math + * @param matrix Row-major n * n matrix (consumed). + * @param n Matrix dimension. + * @param error_message_label Prefix for the message of any exception thrown. + * @return Inverse plus diagnostics and whether the SVD fallback was used. + * @throws FEException If the size is inconsistent or the matrix is rank-deficient. + */ +[[nodiscard]] DenseInverseResult invert_dense_matrix_with_diagnostics( + std::vector matrix, + std::size_t n, + std::string_view error_message_label = "dense matrix"); + +/** + * @brief Validate that a dense inverse has full rank and acceptable conditioning. + * @ingroup FE_Math + * @param result Result from invert_dense_matrix_with_diagnostics(). + * @param expected_rank Required (full) rank. + * @param error_message_label Prefix for the message of any exception thrown. + * @param max_condition Largest acceptable condition estimate. + * @throws FEException If the rank is below expected_rank or the condition exceeds max_condition. + */ +void validate_dense_inverse_diagnostics( + const DenseInverseResult& result, + std::size_t expected_rank, + std::string_view error_message_label = "dense matrix", + double max_condition = dense_matrix_condition_error_threshold()); + +/** + * @brief Numerical rank of a dense matrix from its singular values. + * @ingroup FE_Math + * @param matrix Row-major matrix of size rows * cols (consumed). + * @param rows Row count. + * @param cols Column count. + * @return Number of singular values above the scale-aware tolerance. + * @throws FEException If the matrix size is inconsistent. + */ +[[nodiscard]] std::size_t dense_matrix_rank(std::vector matrix, + std::size_t rows, + std::size_t cols); + +/** + * @brief Moore-Penrose pseudo-inverse via a rank-revealing SVD. + * @ingroup FE_Math + * @param matrix Row-major matrix of size rows * cols. + * @param rows Row count. + * @param cols Column count. + * @param error_message_label Prefix for the message of any exception thrown. + * @return Row-major pseudo-inverse (cols * rows) plus rank/tolerance diagnostics. + * @throws FEException If the matrix size is inconsistent or the matrix is empty. + */ +[[nodiscard]] DensePseudoInverseResult rank_revealing_pseudo_inverse( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view error_message_label = "dense matrix"); + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_DENSELINEARALGEBRA_H diff --git a/Code/Source/solver/FE/Math/Matrix.h b/Code/Source/solver/FE/Math/Matrix.h new file mode 100644 index 000000000..e9aa6510d --- /dev/null +++ b/Code/Source/solver/FE/Math/Matrix.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_MATH_MATRIX_H +#define SVMP_FE_MATH_MATRIX_H + +/** + * @file Matrix.h + * @brief Fixed-size matrix types for FE computations, backed by Eigen. + * + * The FE library standardizes on Eigen for linear algebra. These aliases give + * element-level code a stable vocabulary type without re-exporting all of + * Eigen. Storage is Eigen's default (column-major); element access through + * operator()(row, col) is unchanged. Note that, unlike the previous in-house + * implementation, Eigen types are NOT zero-initialized by default + * construction; use Matrix::Zero() where a zeroed value is required. + */ + +#include "Vector.h" + +#include + +#include + +/** + * @defgroup FE_MatrixMath Matrix + * @ingroup FE_Math + * @brief Fixed-size matrix type aliases. + */ + +namespace svmp { +namespace FE { +namespace math { + +/** + * @brief Fixed-size matrix for element-level computations + * @ingroup FE_MatrixMath + * @tparam T Scalar type (float, double) + * @tparam M Number of rows + * @tparam N Number of columns + */ +template +using Matrix = Eigen::Matrix(M), static_cast(N)>; + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_MATRIX_H diff --git a/Code/Source/solver/FE/Math/Vector.h b/Code/Source/solver/FE/Math/Vector.h new file mode 100644 index 000000000..8c4e30acd --- /dev/null +++ b/Code/Source/solver/FE/Math/Vector.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SVMP_FE_MATH_VECTOR_H +#define SVMP_FE_MATH_VECTOR_H + +/** + * @file Vector.h + * @brief Fixed-size vector types for FE computations, backed by Eigen. + * + * The FE library standardizes on Eigen for linear algebra. These aliases give + * element-level code a stable vocabulary type without re-exporting all of + * Eigen. Note that, unlike the previous in-house implementation, Eigen types + * are NOT zero-initialized by default construction; use Vector::Zero() where a + * zeroed value is required. + * + * This is a small, fixed-size (compile-time length) vector for element-level FE + * kernels in namespace svmp::FE::math. It is distinct from, and not a replacement + * for, the legacy dynamically sized container in solver/Vector.h: the two differ + * in namespace, size model (compile-time vs runtime), and memory management, and + * coexist deliberately. + */ + +#include + +#include + +/** + * @defgroup FE_Math Math + * @ingroup FE + * @brief Linear algebra vocabulary types and dense utilities for finite-element computations. + * + * @details The Math module defines the fixed-size vector and matrix types + * used in element-level kernels (as aliases of Eigen types) and dense linear + * algebra utilities used by basis construction and local transforms. + * + * @defgroup FE_VectorMath Vector + * @ingroup FE_Math + * @brief Fixed-size vector type aliases. + */ + +namespace svmp { +namespace FE { +namespace math { + +/** + * @brief Fixed-size column vector for element-level computations + * @ingroup FE_VectorMath + * @tparam T Scalar type (float, double) + * @tparam N Vector dimension + */ +template +using Vector = Eigen::Matrix(N), 1>; + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_VECTOR_H diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index e20dcf2f7..8b9191e49 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -84,38 +84,43 @@ std::string missing_xml_attribute_message(tinyxml2::XMLElement* element, } const char* require_xml_attribute(tinyxml2::XMLElement* element, - const char* attribute_name, svmp::SourceLocation location, - const std::string& message = std::string()) + const char* attribute_name, const std::string& message = std::string(), + const char* file = __builtin_FILE(), int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) { const char* value = nullptr; if (element == nullptr || element->QueryStringAttribute(attribute_name, &value) != tinyxml2::XML_SUCCESS || value == nullptr) { - svmp::raise( - location, + svmp::raise(svmp::Diagnostic( message.empty() ? missing_xml_attribute_message(element, attribute_name) - : message); + : message, + file, line, function)); } return value; } const char* require_xml_text(tinyxml2::XMLElement* element, - svmp::SourceLocation location, const std::string& message) + const std::string& message, const char* file = __builtin_FILE(), + int line = __builtin_LINE(), const char* function = __builtin_FUNCTION()) { if (element == nullptr || element->GetText() == nullptr) { - svmp::raise(location, message); + svmp::raise( + svmp::Diagnostic(message, file, line, function)); } return element->GetText(); } template typename MapT::mapped_type require_map_value(const MapT& map, - const typename MapT::key_type& key, svmp::SourceLocation location, - const std::string& message) + const typename MapT::key_type& key, const std::string& message, + const char* file = __builtin_FILE(), int line = __builtin_LINE(), + const char* function = __builtin_FUNCTION()) { auto iter = map.find(key); if (iter == map.end()) { - svmp::raise(location, message); + svmp::raise( + svmp::Diagnostic(message, file, line, function)); } return iter->second; } @@ -140,10 +145,10 @@ void xml_util_set_parameters( std::function(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } @@ -161,18 +166,18 @@ std::string IncludeParametersFile::NAME = "Include_xml"; IncludeParametersFile::IncludeParametersFile(const char* cfile_name) { svmp::check( - cfile_name != nullptr, SVMP_HERE, "Include_xml requires a file name."); + cfile_name != nullptr, "Include_xml requires a file name."); std::string file_name(cfile_name); file_name.erase(std::remove_if(file_name.begin(), file_name.end(), ::isspace), file_name.end()); svmp::check( - !file_name.empty(), SVMP_HERE, "Include_xml requires a non-empty file name."); + !file_name.empty(), "Include_xml requires a non-empty file name."); auto error = document.LoadFile(file_name.c_str()); root_element = document.FirstChildElement(Parameters::FSI_FILE.c_str()); if (error != tinyxml2::XML_SUCCESS || root_element == nullptr) { - svmp::raise(SVMP_HERE, "The following error occurred while reading the XML file '" + + svmp::raise("The following error occurred while reading the XML file '" + file_name + "'.\n" + "[svMultiPhysics] ERROR " + std::string(document.ErrorStr())); } } @@ -229,7 +234,7 @@ void Parameters::read_xml(std::string file_name) auto root_element = doc.FirstChildElement(FSI_FILE.c_str()); if (error != tinyxml2::XML_SUCCESS || root_element == nullptr) { - svmp::raise(SVMP_HERE, "The following error occurred while reading the XML file '" + file_name + "'.\n" + + svmp::raise("The following error occurred while reading the XML file '" + file_name + "'.\n" + "[svMultiPhysics] ERROR " + std::string(doc.ErrorStr())); } @@ -274,7 +279,7 @@ void Parameters::set_equation_values(tinyxml2::XMLElement* root_element) auto add_eq_item = root_element->FirstChildElement(EquationParameters::xml_element_name_.c_str()); while (add_eq_item) { - const char* eq_type = require_xml_attribute(add_eq_item, "type", SVMP_HERE); + const char* eq_type = require_xml_attribute(add_eq_item, "type"); auto eq_params = new EquationParameters(); eq_params->type.set(std::string(eq_type)); @@ -290,7 +295,7 @@ void Parameters::set_mesh_values(tinyxml2::XMLElement* root_element) auto add_mesh_item = root_element->FirstChildElement(MeshParameters::xml_element_name_.c_str()); while (add_mesh_item) { - const char* mesh_name = require_xml_attribute(add_mesh_item, "name", SVMP_HERE); + const char* mesh_name = require_xml_attribute(add_mesh_item, "name"); MeshParameters* mesh_params = new MeshParameters(); mesh_params->name.set(std::string(mesh_name)); @@ -316,7 +321,7 @@ void Parameters::set_projection_values(tinyxml2::XMLElement* root_element) auto add_proj_item = root_element->FirstChildElement(ProjectionParameters::xml_element_name_.c_str()); while (add_proj_item) { - const char* proj_name = require_xml_attribute(add_proj_item, "name", SVMP_HERE); + const char* proj_name = require_xml_attribute(add_proj_item, "name"); ProjectionParameters* proj_params = new ProjectionParameters(); proj_params->name.set(std::string(proj_name)); @@ -333,7 +338,7 @@ void Parameters::set_RIS_projection_values(tinyxml2::XMLElement* root_element) while (add_RIS_proj_item) { const char* RIS_proj_name = - require_xml_attribute(add_RIS_proj_item, "name", SVMP_HERE); + require_xml_attribute(add_RIS_proj_item, "name"); RISProjectionParameters* RIS_proj_params = new RISProjectionParameters(); RIS_proj_params->name.set(std::string(RIS_proj_name)); @@ -350,7 +355,7 @@ void Parameters::set_URIS_mesh_values(tinyxml2::XMLElement* root_element) while (add_URIS_mesh_item) { const char* URIS_mesh_name = - require_xml_attribute(add_URIS_mesh_item, "name", SVMP_HERE); + require_xml_attribute(add_URIS_mesh_item, "name"); URISMeshParameters* URIS_mesh_params = new URISMeshParameters(); URIS_mesh_params->name.set(std::string(URIS_mesh_name)); @@ -410,7 +415,7 @@ void BodyForceParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* smesh = require_xml_attribute(xml_elem, "mesh", SVMP_HERE); + const char* smesh = require_xml_attribute(xml_elem, "mesh"); mesh_name.set(std::string(smesh)); //auto item = xml_elem->FirstChildElement(); @@ -584,7 +589,7 @@ void BoundaryConditionParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'name' from the element. - const char* sname = require_xml_attribute(xml_elem, "name", SVMP_HERE); + const char* sname = require_xml_attribute(xml_elem, "name"); name.set(std::string(sname)); auto item = xml_elem->FirstChildElement(); @@ -601,10 +606,10 @@ void BoundaryConditionParameters::set_values(tinyxml2::XMLElement* xml_elem) try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -938,8 +943,7 @@ void CANNRowParameters::print_parameters() void CANNRowParameters::set_values(tinyxml2::XMLElement* row_elem) { svmp::check_not_null( - row_elem, SVMP_HERE, - "CANNRowParameters::set_values: Received null XML element."); + row_elem, "CANNRowParameters::set_values: Received null XML element."); using namespace tinyxml2; @@ -947,7 +951,7 @@ void CANNRowParameters::set_values(tinyxml2::XMLElement* row_elem) // Set row_name for current row element const char* row_name_input = - require_xml_attribute(row_elem, "row_name", SVMP_HERE); + require_xml_attribute(row_elem, "row_name"); row_name.set(std::string(row_name_input)); auto item = row_elem->FirstChildElement(); @@ -958,13 +962,13 @@ void CANNRowParameters::set_values(tinyxml2::XMLElement* row_elem) auto value = item->GetText(); if (value == nullptr) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } try { set_parameter_value_CANN(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -1008,7 +1012,7 @@ void CANNParameters::set_values(tinyxml2::XMLElement* xml_elem) } if (rows.empty()) { - svmp::raise(SVMP_HERE, error_msg + "Add_row'. No rows found."); + svmp::raise(error_msg + "Add_row'. No rows found."); } value_set = true; @@ -1055,7 +1059,7 @@ void ConstitutiveModelParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); // Check constitutive model type. @@ -1067,7 +1071,7 @@ void ConstitutiveModelParameters::set_values(tinyxml2::XMLElement* xml_elem) } msg_2 += "\n"; auto msg = msg_1 + msg_2; - svmp::raise(SVMP_HERE, msg); + svmp::raise(msg); } auto model_type = constitutive_model_types.at(type.value()); type.set(model_type); @@ -1083,13 +1087,13 @@ void ConstitutiveModelParameters::set_values(tinyxml2::XMLElement* xml_elem) void ConstitutiveModelParameters::check_constitutive_model(const Parameter& eq_type_str) { auto eq_type = require_map_value(consts::equation_name_to_type, eq_type_str.value(), - SVMP_HERE, "Unknown equation type '" + eq_type_str.value() + "'."); + "Unknown equation type '" + eq_type_str.value() + "'."); auto model = require_map_value(consts::constitutive_model_name_to_type, type.value(), - SVMP_HERE, "Unknown constitutive model '" + type.value() + "'."); + "Unknown constitutive model '" + type.value() + "'."); if (eq_type == consts::EquationType::phys_ustruct) { if (! ustruct::constitutive_model_is_valid(model)) { - svmp::raise(SVMP_HERE, "The " + type.value() + " constitutive model is not valid for ustruct equations."); + svmp::raise("The " + type.value() + " constitutive model is not valid for ustruct equations."); } } } @@ -1118,7 +1122,7 @@ void CoupleGenBCParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown Couple_to_genBC type=TYPE XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); auto item = xml_elem->FirstChildElement(); @@ -1208,7 +1212,7 @@ void OutputParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string msg("[OutputParameters::set_values] "); std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); // Get values from XML file. @@ -1220,8 +1224,7 @@ void OutputParameters::set_values(tinyxml2::XMLElement* xml_elem) auto item = xml_elem->FirstChildElement(); while (item != nullptr) { auto name = std::string(item->Name()); - auto value = std::string(require_xml_text(item, SVMP_HERE, - "Output XML element '" + name + "' requires a value.")); + auto value = std::string(require_xml_text(item, "Output XML element '" + name + "' requires a value.")); Parameter param(name, "", false); param.set(value); alias_list.emplace_back(param); @@ -1231,8 +1234,7 @@ void OutputParameters::set_values(tinyxml2::XMLElement* xml_elem) auto item = xml_elem->FirstChildElement(); while (item != nullptr) { auto name = std::string(item->Name()); - auto value = std::string(require_xml_text(item, SVMP_HERE, - "Output XML element '" + name + "' requires a value.")); + auto value = std::string(require_xml_text(item, "Output XML element '" + name + "' requires a value.")); Parameter param(name, false, false); param.set(value); output_list.emplace_back(param); @@ -1291,7 +1293,7 @@ void VariableWallPropsParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* sname = require_xml_attribute(xml_elem, "mesh_name", SVMP_HERE); + const char* sname = require_xml_attribute(xml_elem, "mesh_name"); mesh_name.set(std::string(sname)); auto item = xml_elem->FirstChildElement(); @@ -1456,12 +1458,12 @@ void FluidViscosityParameters::set_values(tinyxml2::XMLElement* xml_elem) { using namespace tinyxml2; - const char* smodel = require_xml_attribute(xml_elem, "model", SVMP_HERE); + const char* smodel = require_xml_attribute(xml_elem, "model"); model.set(std::string(smodel)); // Check fluid_viscosity model name. if (model_names.count(model.value()) == 0) { - svmp::raise(SVMP_HERE, "Unknown fluid viscosity model '" + model.value() + + svmp::raise("Unknown fluid viscosity model '" + model.value() + "' in '" + xml_elem->Name() + "'."); } @@ -1576,12 +1578,12 @@ void SolidViscosityParameters::set_values(tinyxml2::XMLElement* xml_elem) { using namespace tinyxml2; - const char* smodel = require_xml_attribute(xml_elem, "model", SVMP_HERE); + const char* smodel = require_xml_attribute(xml_elem, "model"); model.set(std::string(smodel)); // Check solid viscosity model name. if (model_names.count(model.value()) == 0) { - svmp::raise(SVMP_HERE, "Unknown solid viscosity model '" + model.value() + + svmp::raise("Unknown solid viscosity model '" + model.value() + "' in '" + xml_elem->Name() + "'."); } @@ -1616,7 +1618,7 @@ void IonicInitialStateParameters::print_parameters() const { void IonicInitialStateParameters::set_values( const tinyxml2::XMLElement *xml_elem) { if (xml_elem->Name() != xml_element_name) { - svmp::raise(SVMP_HERE, "Unknown " + xml_element_name + + svmp::raise("Unknown " + xml_element_name + " XML element '"); } @@ -1634,12 +1636,10 @@ void IonicInitialStateParameters::set_values( set_parameter_value(name, value); value_set = true; } catch (const std::bad_function_call &exception) { - svmp::raise(SVMP_HERE, - error_msg_prefix + name + "'."); + svmp::raise(error_msg_prefix + name + "'."); } } else { - svmp::raise(SVMP_HERE, - error_msg_prefix + name + "'."); + svmp::raise(error_msg_prefix + name + "'."); } } @@ -1715,14 +1715,14 @@ void IonicModelParameters::set_values(const tinyxml2::XMLElement *xml_elem) { // The initial values of both state and gating variables must be set. if (initial_X_parameters.required && !initial_X_parameters.defined()) { svmp::raise( - SVMP_HERE, xml_element_name + " requires an '" + + xml_element_name + " requires an '" + initial_X_parameters.xml_element_name + "' XML section."); } if (initial_Xg_parameters.required && !initial_Xg_parameters.defined()) { svmp::raise( - SVMP_HERE, xml_element_name + " requires an '" + + xml_element_name + " requires an '" + initial_Xg_parameters.xml_element_name + "' XML section."); } @@ -1841,7 +1841,7 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e // If not reading from an external xml file then get the 'id' attrribute. // if (!from_external_xml) { - const char* sid = require_xml_attribute(domain_elem, "id", SVMP_HERE); + const char* sid = require_xml_attribute(domain_elem, "id"); id.set(std::string(sid)); } @@ -1877,8 +1877,7 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e if (name == FluidViscosityParameters::xml_element_name_ || name == SolidViscosityParameters::xml_element_name_) { auto eq_type = require_map_value( - consts::equation_name_to_type, equation.value(), SVMP_HERE, - "Unknown equation type '" + equation.value() + + consts::equation_name_to_type, equation.value(), "Unknown equation type '" + equation.value() + "' while parsing viscosity model."); if (eq_type == consts::EquationType::phys_fluid || eq_type == consts::EquationType::phys_CMM || @@ -1891,14 +1890,13 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e item_found = true; } else { svmp::raise( - SVMP_HERE, "Viscosity model not supported for equation '" + + "Viscosity model not supported for equation '" + equation.value() + "'."); } } if (name == include_xml.name()) { - auto value = require_xml_text(item, SVMP_HERE, - "Domain Include_xml requires a file name."); + auto value = require_xml_text(item, "Domain Include_xml requires a file name."); IncludeParametersFile include_parameters(value); set_values(include_parameters.root_element, true); @@ -1911,12 +1909,12 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e set_parameter_value(name, value); item_found = true; } catch (const std::bad_function_call &exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } if (!item_found) - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); item = item->NextSiblingElement(); } @@ -1926,14 +1924,14 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e // Check values for some parameters.. // if (Parameters::constitutive_model_names.count(constitutive_model.value()) - == 0) { svmp::raise(SVMP_HERE, "Unknown constitutive + == 0) { svmp::raise("Unknown constitutive model '" + constitutive_model.value_ + "' for '" + constitutive_model.name_ + "' in '" + domain_params->Name() + "'."); } if (Parameters::equation_names.count(equation.value()) == 0) { - svmp::raise(SVMP_HERE, "Unknown equation name '" + + svmp::raise("Unknown equation name '" + equation.value() + "' for '" + equation.name_ + "' in '" + domain_params->Name() + "'."); } @@ -1988,7 +1986,7 @@ void DirectionalDistributionParameters::validate() const // Empty block is invalid - if block exists, must specify all three if (num_defined == 0) { - svmp::raise(SVMP_HERE, "Directional_distribution block is empty. " + svmp::raise("Directional_distribution block is empty. " "Either remove the block entirely (to use defaults: fiber=1.0, sheet=0.0, normal=0.0) " "or specify all three directions: Fiber_direction, Sheet_direction, Sheet_normal_direction."); } @@ -2003,7 +2001,7 @@ void DirectionalDistributionParameters::validate() const if (!fiber_defined) msg += "Fiber_direction "; if (!sheet_defined) msg += "Sheet_direction "; if (!normal_defined) msg += "Sheet_normal_direction "; - svmp::raise(SVMP_HERE, msg); + svmp::raise(msg); } // All three are specified, validate their values @@ -2015,7 +2013,7 @@ void DirectionalDistributionParameters::validate() const double eta_sum = eta_f + eta_s + eta_n; const double tol = 1.0e-10; if (std::abs(eta_sum - 1.0) > tol) { - svmp::raise(SVMP_HERE, "Directional distribution fractions must sum to 1.0. " + svmp::raise("Directional distribution fractions must sum to 1.0. " "Got: Fiber_direction=" + std::to_string(eta_f) + ", Sheet_direction=" + std::to_string(eta_s) + ", Sheet_normal_direction=" + std::to_string(eta_n) + @@ -2024,7 +2022,7 @@ void DirectionalDistributionParameters::validate() const // Validate that each eta is non-negative if (eta_f < 0.0 || eta_s < 0.0 || eta_n < 0.0) { - svmp::raise(SVMP_HERE, "Directional distribution fractions must be non-negative. " + svmp::raise("Directional distribution fractions must be non-negative. " "Got: Fiber_direction=" + std::to_string(eta_f) + ", Sheet_direction=" + std::to_string(eta_s) + ", Sheet_normal_direction=" + std::to_string(eta_n)); @@ -2071,7 +2069,7 @@ void FiberReinforcementStressParameters::set_values(tinyxml2::XMLElement* xml_el std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element attribute. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); auto item = xml_elem->FirstChildElement(); @@ -2086,10 +2084,10 @@ void FiberReinforcementStressParameters::set_values(tinyxml2::XMLElement* xml_el try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -2148,7 +2146,7 @@ void StimulusParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); auto item = xml_elem->FirstChildElement(); @@ -2277,7 +2275,7 @@ void ContactParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* mname = require_xml_attribute(xml_elem, "model", SVMP_HERE); + const char* mname = require_xml_attribute(xml_elem, "model"); model.set(std::string(mname)); using std::placeholders::_1; @@ -2439,14 +2437,14 @@ void EquationParameters::set_values(tinyxml2::XMLElement* eq_elem, DomainParamet } else if (viscosity_names.count(name)) { auto eq_type = require_map_value(consts::equation_name_to_type, type.value(), - SVMP_HERE, "Unknown equation type '" + type.value() + "' while parsing viscosity model."); + "Unknown equation type '" + type.value() + "' while parsing viscosity model."); if (fluid_eqs.count(eq_type)) { domain->fluid_viscosity.set_values(item); } else if (eq_type == consts::EquationType::phys_struct || eq_type == consts::EquationType::phys_ustruct) { domain->solid_viscosity.set_values(item); } else { - svmp::raise(SVMP_HERE, "Viscosity model not supported for equation '" + type.value() + "'."); + svmp::raise("Viscosity model not supported for equation '" + type.value() + "'."); } } else if (name == ECGLeadsParameters::xml_element_name_) { @@ -2456,8 +2454,7 @@ void EquationParameters::set_values(tinyxml2::XMLElement* eq_elem, DomainParamet variable_wall_properties.set_values(item); } else if (name == include_xml.name()) { - auto value = require_xml_text(item, SVMP_HERE, - "Equation Include_xml requires a file name."); + auto value = require_xml_text(item, "Equation Include_xml requires a file name."); IncludeParametersFile include_parameters(value); set_values(include_parameters.root_element, default_domain); @@ -2473,13 +2470,13 @@ void EquationParameters::set_values(tinyxml2::XMLElement* eq_elem, DomainParamet try { default_domain->set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, "Unknown " + xml_element_name_ + " XML element '" + name + "'."); + svmp::raise("Unknown " + xml_element_name_ + " XML element '" + name + "'."); } } } else { - svmp::raise(SVMP_HERE, "[Equation] Unknown " + xml_element_name_ + " XML element '" + name + "'."); + svmp::raise("[Equation] Unknown " + xml_element_name_ + " XML element '" + name + "'."); } item = item->NextSiblingElement(); @@ -2571,8 +2568,7 @@ void GeneralSimulationParameters::set_values(tinyxml2::XMLElement* xml_element, } else { auto general_params = xml_element->FirstChildElement(xml_element_name.c_str()); if (general_params == nullptr) { - svmp::raise(SVMP_HERE, - "No <" + xml_element_name + "> section found in the solver XML file."); + svmp::raise("No <" + xml_element_name + "> section found in the solver XML file."); } item = general_params->FirstChildElement(); } @@ -2581,19 +2577,17 @@ void GeneralSimulationParameters::set_values(tinyxml2::XMLElement* xml_element, std::string name = std::string(item->Value()); if (name == include_xml.name()) { - auto value = require_xml_text(item, SVMP_HERE, - "GeneralSimulationParameters Include_xml requires a file name."); + auto value = require_xml_text(item, "GeneralSimulationParameters Include_xml requires a file name."); IncludeParametersFile include_parameters(value); set_values(include_parameters.root_element, true); } else { - auto value = require_xml_text(item, SVMP_HERE, - "GeneralSimulationParameters XML element '" + name + "' requires a value."); + auto value = require_xml_text(item, "GeneralSimulationParameters XML element '" + name + "' requires a value."); try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, "Unknown XML GeneralSimulationParameters element '" + name + "."); + svmp::raise("Unknown XML GeneralSimulationParameters element '" + name + "."); } } @@ -2645,7 +2639,7 @@ void FaceParameters::set_values(tinyxml2::XMLElement* face_elem) using namespace tinyxml2; std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - const char* face_name = require_xml_attribute(face_elem, "name", SVMP_HERE); + const char* face_name = require_xml_attribute(face_elem, "name"); name.set(std::string(face_name)); auto item = face_elem->FirstChildElement(); @@ -2654,13 +2648,13 @@ void FaceParameters::set_values(tinyxml2::XMLElement* face_elem) auto value = item->GetText(); if (value == nullptr) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -2703,7 +2697,7 @@ void RemesherParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name + " XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); values_set_ = true; @@ -2717,15 +2711,15 @@ void RemesherParameters::set_values(tinyxml2::XMLElement* xml_elem) if (name == "Max_edge_size") { const char* name; const char* value; - name = require_xml_attribute(item, "name", SVMP_HERE); - value = require_xml_attribute(item, "value", SVMP_HERE); + name = require_xml_attribute(item, "name"); + value = require_xml_attribute(item, "value"); auto svalue = std::string(value); try { double dvalue = std::stod(svalue); max_edge_sizes_[std::string(name)] = dvalue; } catch (...) { - svmp::raise(SVMP_HERE, "VALUE=" + svalue + + svmp::raise("VALUE=" + svalue + " is not a valid float in the XML Remesher element."); } @@ -2734,11 +2728,11 @@ void RemesherParameters::set_values(tinyxml2::XMLElement* xml_elem) try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -2825,15 +2819,13 @@ void MeshParameters::set_values(tinyxml2::XMLElement* mesh_elem, bool from_exter // them as a list of VectorParameter. // } else if (name == "Fiber_direction") { - auto value = require_xml_text(item, SVMP_HERE, - "Mesh Fiber_direction XML element requires a value."); + auto value = require_xml_text(item, "Mesh Fiber_direction XML element requires a value."); VectorParameter dir("Fiber_direction", {}, false, {}); dir.set(value); fiber_directions.push_back(dir); } else if (name == include_xml.name()) { - auto value = require_xml_text(item, SVMP_HERE, - "Mesh Include_xml requires a file name."); + auto value = require_xml_text(item, "Mesh Include_xml requires a file name."); IncludeParametersFile include_parameters(value); set_values(include_parameters.root_element, true); @@ -2843,10 +2835,10 @@ void MeshParameters::set_values(tinyxml2::XMLElement* mesh_elem, bool from_exter try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -2914,7 +2906,7 @@ void ProjectionParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* sname = require_xml_attribute(xml_elem, "name", SVMP_HERE); + const char* sname = require_xml_attribute(xml_elem, "name"); name.set(std::string(sname)); using std::placeholders::_1; @@ -2951,7 +2943,7 @@ void RISProjectionParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; // Get the 'type' from the element. - const char* sname = require_xml_attribute(xml_elem, "name", SVMP_HERE); + const char* sname = require_xml_attribute(xml_elem, "name"); name.set(std::string(sname)); using std::placeholders::_1; @@ -3030,10 +3022,10 @@ void URISMeshParameters::set_values(tinyxml2::XMLElement* mesh_elem) try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } } else { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -3082,7 +3074,7 @@ void URISFaceParameters::set_values(tinyxml2::XMLElement* face_elem) using namespace tinyxml2; std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - const char* face_name = require_xml_attribute(face_elem, "name", SVMP_HERE); + const char* face_name = require_xml_attribute(face_elem, "name"); name.set(std::string(face_name)); auto item = face_elem->FirstChildElement(); @@ -3091,13 +3083,13 @@ void URISFaceParameters::set_values(tinyxml2::XMLElement* face_elem) auto value = item->GetText(); if (value == nullptr) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } try { set_parameter_value(name, value); } catch (const std::bad_function_call& exception) { - svmp::raise(SVMP_HERE, error_msg + name + "'."); + svmp::raise(error_msg + name + "'."); } item = item->NextSiblingElement(); @@ -3152,7 +3144,7 @@ void LinearAlgebraParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name + " XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); // Check Linear_algebra type=TYPE> element. @@ -3163,7 +3155,7 @@ void LinearAlgebraParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string valid_types = ""; std::for_each(LinearAlgebra::name_to_type.begin(), LinearAlgebra::name_to_type.end(), [&valid_types](std::pair p) {valid_types += p.first+" ";}); - svmp::raise(SVMP_HERE, "Unknown TYPE '" + type.value() + + svmp::raise("Unknown TYPE '" + type.value() + "' given in the XML element.\nValid types are: " + valid_types); } @@ -3182,7 +3174,7 @@ void LinearAlgebraParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string valid_types = ""; std::for_each(consts::preconditioner_name_to_type.begin(), consts::preconditioner_name_to_type.end(), [&valid_types](std::pair p) {valid_types += p.first+" ";}); - svmp::raise(SVMP_HERE, "Unknown TYPE '" + preconditioner() + + svmp::raise("Unknown TYPE '" + preconditioner() + "' given in the XML element.\nValid types are: " + valid_types); } @@ -3195,21 +3187,20 @@ void LinearAlgebraParameters::set_values(tinyxml2::XMLElement* xml_elem) void LinearAlgebraParameters::check_input_parameters() { auto linear_algebra_type = require_map_value(LinearAlgebra::name_to_type, type(), - SVMP_HERE, "Unknown TYPE '" + type() + + "Unknown TYPE '" + type() + "' given in the XML element."); auto prec_cond_type = require_map_value(consts::preconditioner_name_to_type, - preconditioner.value(), SVMP_HERE, "Unknown TYPE '" + preconditioner() + + preconditioner.value(), "Unknown TYPE '" + preconditioner() + "' given in the XML element."); auto assembly_type = require_map_value(LinearAlgebra::name_to_type, assembly.value(), - SVMP_HERE, "Unknown TYPE '" + assembly() + + "Unknown TYPE '" + assembly() + "' given in the XML element."); LinearAlgebra* linear_algebra = nullptr; try { linear_algebra = LinearAlgebraFactory::create_interface(linear_algebra_type); if (linear_algebra == nullptr) { - svmp::raise(SVMP_HERE, - "Linear_algebra type '" + type() + "' cannot be used as a solver backend."); + svmp::raise("Linear_algebra type '" + type() + "' cannot be used as a solver backend."); } linear_algebra->check_options(prec_cond_type, assembly_type); delete linear_algebra; @@ -3218,7 +3209,7 @@ void LinearAlgebraParameters::check_input_parameters() throw; } catch (const std::exception& exception) { delete linear_algebra; - svmp::raise(SVMP_HERE, exception.what()); + svmp::raise(exception.what()); } } @@ -3276,7 +3267,7 @@ void LinearSolverParameters::set_values(tinyxml2::XMLElement* xml_elem) std::string error_msg = "Unknown " + xml_element_name + " XML element '"; // Get the 'type' from the element. - const char* stype = require_xml_attribute(xml_elem, "type", SVMP_HERE); + const char* stype = require_xml_attribute(xml_elem, "type"); type.set(std::string(stype)); diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index de383e80f..9f8ab4749 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -140,7 +140,7 @@ class Parameter if (!(str_stream >> value_)) { std::istringstream str_stream(str); if (!(str_stream >> std::boolalpha >> value_)) { - svmp::raise(SVMP_HERE, "Incorrect value '" + str + "' for '" + name_ + "'."); + svmp::raise("Incorrect value '" + str + "' for '" + name_ + "'."); } } @@ -342,7 +342,7 @@ class ParameterLists void set_parameter_value_CANN(const std::string& name, const std::string& value) { if (params_map.count(name) == 0) { - svmp::raise(SVMP_HERE, "Unknown " + xml_element_name + " XML element '" + name + "'."); + svmp::raise("Unknown " + xml_element_name + " XML element '" + name + "'."); } auto& param_variant = params_map[name]; @@ -353,7 +353,7 @@ class ParameterLists (*vec_param)->value_.clear(); // Clear the vector before setting (*vec_param)->set(value); // Set the new value } else { - svmp::raise(SVMP_HERE, "Activation_functions is not a VectorParameter."); + svmp::raise("Activation_functions is not a VectorParameter."); } } // Check for Weights @@ -362,7 +362,7 @@ class ParameterLists (*vec_param)->value_.clear(); // Clear the vector before setting (*vec_param)->set(value); // Set the new value } else { - svmp::raise(SVMP_HERE, "Weights is not a VectorParameter."); + svmp::raise("Weights is not a VectorParameter."); } } // Default: everything else @@ -379,7 +379,7 @@ class ParameterLists void set_parameter_value(const std::string& name, const std::string& value) { if (params_map.count(name) == 0) { - svmp::raise(SVMP_HERE, "Unknown " + xml_element_name + " XML element '" + name + "'."); + svmp::raise("Unknown " + xml_element_name + " XML element '" + name + "'."); } std::visit([value](auto&& p) { p->set(value); }, params_map[name]); @@ -394,7 +394,7 @@ class ParameterLists if (std::visit([](auto&& p) { return !p->check_required_set(); }, param)) { - svmp::raise(SVMP_HERE, xml_element_name + " XML element '" + key + "' has not been set."); + svmp::raise(xml_element_name + " XML element '" + key + "' has not been set."); } } } diff --git a/Code/Source/solver/README.md b/Code/Source/solver/README.md index 252999e8f..d11378e35 100644 --- a/Code/Source/solver/README.md +++ b/Code/Source/solver/README.md @@ -601,7 +601,7 @@ A map type used to set element properties. Computes shape functions and derivatives at given natural coords. -- `set_face_shape_data[face.eType](gaus_pt, face)` +- FE Basis face evaluation for supported mapped face elements. diff --git a/Code/Source/solver/Timer.h b/Code/Source/solver/Timer.h index 6810ae17c..1a55d7516 100644 --- a/Code/Source/solver/Timer.h +++ b/Code/Source/solver/Timer.h @@ -5,27 +5,21 @@ #define TIMER_H #include -#include -#include /// @brief Keep track of time class Timer { public: - double get_elapsed_time() + double get_elapsed_time() const { return get_time() - current_time; } - double get_time() + double get_time() const { - auto now = std::chrono::system_clock::now(); - auto now_ms = std::chrono::time_point_cast(now); - - auto value = now_ms.time_since_epoch(); - auto duration = value.count() / 1000.0; - return static_cast(duration); + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration(now.time_since_epoch()).count(); } void set_time() @@ -33,8 +27,7 @@ class Timer current_time = get_time(); } - double current_time; + double current_time{0.0}; }; #endif - diff --git a/Code/Source/solver/cep_ion.cpp b/Code/Source/solver/cep_ion.cpp index 8c91a54fd..82ad5303e 100644 --- a/Code/Source/solver/cep_ion.cpp +++ b/Code/Source/solver/cep_ion.cpp @@ -331,7 +331,7 @@ void cep_integ_l(CepMod &cep_mod, cepModelType &cep, Vector &X, #endif svmp::check_not_null( - cep.ionic_model, SVMP_HERE, "ionic model was not constructed."); + cep.ionic_model, "ionic model was not constructed."); const double eps = std::numeric_limits::epsilon(); diff --git a/Code/Source/solver/fs.cpp b/Code/Source/solver/fs.cpp index d592a8b96..dce27174d 100644 --- a/Code/Source/solver/fs.cpp +++ b/Code/Source/solver/fs.cpp @@ -5,10 +5,48 @@ #include "fs.h" #include "consts.h" +#include "FE/Common/FEException.h" #include "nn.h" +#include +#include + namespace fs { +namespace { + +namespace fe = svmp::FE; + +std::string element_name(consts::ElementType eType) +{ + const auto iter = consts::element_type_to_string.find(eType); + if (iter != consts::element_type_to_string.end()) { + return iter->second; + } + + return "unknown (" + std::to_string(static_cast(eType)) + ")"; +} + +/// @brief Populate reference-space Hessians (fs.Nxx) at every Gauss point. +/// +/// Element-type support is owned by nn::get_gn_nxx: it evaluates analytic +/// reference Hessians for every element the FE Basis supports. +/// Families without FE Basis Hessian support include (NA/PNT/NRB), +/// their zero-initialized Nxx remain untouched. +void populate_reference_hessians(fsType& fs, const int insd) +{ + if (fs.Nxx.size() == 0) { + return; + } + + const int ind2 = std::max(3 * (insd - 1), 1); + for (int g = 0; g < fs.nG; ++g) { + nn::get_gn_nxx(insd, ind2, fs.eType, fs.eNoN, g, fs.xi, fs.Nxx); + } +} + +} // namespace + /// @brief Allocates arrays within the function space type. Assumes that /// fs%eNoN and fs%nG are already defined @@ -103,6 +141,7 @@ void get_thood_fs(ComMod& com_mod, std::array& fs, const mshType& lM, nn::get_gnn(nsd, fs[1].eType, fs[1].eNoN, g, fs[1].xi, fs[1].N, fs[1].Nx); } nn::get_nn_bnds(nsd, fs[1].eType, fs[1].eNoN, fs[1].xib, fs[1].Nb); + populate_reference_hessians(fs[1], nsd); } else if (iOpt == 2) { fs[1].nG = lM.fs[1].nG; @@ -133,6 +172,7 @@ void get_thood_fs(ComMod& com_mod, std::array& fs, const mshType& lM, nn::get_gnn(nsd, fs[0].eType, fs[0].eNoN, g, fs[0].xi, fs[0].N, fs[0].Nx); } nn::get_nn_bnds(nsd, fs[0].eType, fs[0].eNoN, fs[0].xib, fs[0].Nb); + populate_reference_hessians(fs[0], nsd); } } } @@ -275,14 +315,7 @@ void init_fs_msh(const ComMod& com_mod, mshType& lM) lM.fs[0].Nb = lM.Nb; lM.fs[0].Nx = lM.Nx; } - // Second order derivatives for vector function space - // - if (!lM.fs[0].lShpF) { - int ind2 = std::max(3*(insd-1), 1); - for (int g = 0; g < lM.fs[0].nG; g++) { - nn::get_gn_nxx(insd, ind2, lM.fs[0].eType, lM.fs[0].eNoN, g, lM.fs[0].xi, lM.fs[0].Nxx); - } - } + populate_reference_hessians(lM.fs[0], insd); // Sets Taylor-Hood basis [fluid, stokes, ustruct, FSI) if (lM.nFs == 2) { @@ -291,6 +324,7 @@ void init_fs_msh(const ComMod& com_mod, mshType& lM) // Initialize the function space init_fs(lM.fs[1], nsd, insd); + populate_reference_hessians(lM.fs[1], insd); } } @@ -343,8 +377,8 @@ void set_thood_fs(fsType& fs, consts::ElementType eType) break; default: - throw std::runtime_error("Cannot choose Taylor-Hood basis"); - break; + svmp::raise( + "Cannot choose Taylor-Hood basis", element_name(eType)); } } diff --git a/Code/Source/solver/ionic_model.cpp b/Code/Source/solver/ionic_model.cpp index 9bc846b5c..bc33d0dd6 100644 --- a/Code/Source/solver/ionic_model.cpp +++ b/Code/Source/solver/ionic_model.cpp @@ -41,7 +41,7 @@ void IonicModel::distribute_parameters(const CmMod &cm_mod, const cmType &cm) { void IonicModel::init(Vector &X, Vector &Xg) const { if (initial_X.size() != X.size()) { svmp::raise( - SVMP_HERE, "Initial conditions size for X does not match vector size."); + "Initial conditions size for X does not match vector size."); } for (size_t i = 0; i < initial_X.size(); ++i) @@ -49,7 +49,6 @@ void IonicModel::init(Vector &X, Vector &Xg) const { if (initial_Xg.size() != Xg.size()) { svmp::raise( - SVMP_HERE, "Initial conditions size for Xg does not match vector size."); } @@ -77,7 +76,6 @@ void IonicModel::integ(const odeType &ode_solver_params, const int zone_id, default: svmp::raise( - SVMP_HERE, "Unknown time integration type: " + std::to_string(static_cast(ode_solver_params.tIntType))); } @@ -264,7 +262,7 @@ IonicModelFactory::create_model(const std::string &name) { auto iter = factory_instance.children.find(name); if (iter == factory_instance.children.end()) { svmp::raise( - SVMP_HERE, "No model with name '" + name + + "No model with name '" + name + "' was registered in the ionic model factory."); } @@ -279,4 +277,4 @@ void IonicModelFactory::visit( std::unique_ptr dummy = builder(); f(name, *dummy); } -} \ No newline at end of file +} diff --git a/Code/Source/solver/ionic_model.h b/Code/Source/solver/ionic_model.h index 52526a24b..22c1332fb 100644 --- a/Code/Source/solver/ionic_model.h +++ b/Code/Source/solver/ionic_model.h @@ -405,7 +405,7 @@ class IonicModel { const Vector &X, const Vector &Xg, const double Ksac) const { svmp::raise( - SVMP_HERE, "getj method not implemented for this ionic model."); + "getj method not implemented for this ionic model."); // Dummy return statement to avoid compiler warnings. Array dummy(X.size(), X.size()); @@ -476,7 +476,6 @@ class IonicModelFactory { if (factory_instance.children.find(name) != factory_instance.children.end()) { svmp::raise( - SVMP_HERE, "A model with name '" + name + "' was already registered in the ionic model factory."); } diff --git a/Code/Source/solver/load_msh.cpp b/Code/Source/solver/load_msh.cpp index c7c5a62ba..05648b52d 100644 --- a/Code/Source/solver/load_msh.cpp +++ b/Code/Source/solver/load_msh.cpp @@ -300,4 +300,3 @@ void read_sv(Simulation* simulation, mshType& mesh, const MeshParameters* mesh_p } } }; - diff --git a/Code/Source/solver/mat_fun.h b/Code/Source/solver/mat_fun.h index db50da6cb..22b03896e 100644 --- a/Code/Source/solver/mat_fun.h +++ b/Code/Source/solver/mat_fun.h @@ -52,7 +52,8 @@ namespace mat_fun { if ((mat.rows() != dest.nrows()) || (mat.cols() != dest.ncols())) { auto mat_dims = (std::stringstream() << "(" << mat.rows() << "x" << mat.cols() << ")").str(); auto dest_dims = (std::stringstream() << "(" << dest.nrows() << "x" << dest.ncols() << ")").str(); - svmp::raise( SVMP_HERE, "The 'mat" + mat_dims + "' and 'dest" + dest_dims + + svmp::raise( + "The 'mat" + mat_dims + "' and 'dest" + dest_dims + "' arrays have incompatible sizes."); } @@ -258,4 +259,3 @@ namespace mat_fun { }; #endif - diff --git a/Code/Source/solver/nn.cpp b/Code/Source/solver/nn.cpp index 652923cf2..54c2d973c 100644 --- a/Code/Source/solver/nn.cpp +++ b/Code/Source/solver/nn.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. // SPDX-License-Identifier: BSD-3-Clause -// The functions defined here replicate the Fortran functions defined in NN.f. +// Solver-facing element setup, Gauss integration, FE Basis evaluation, and +// shape-function bounds. // // The functions are used to // @@ -15,15 +16,26 @@ #include "Array.h" #include "Vector.h" +#include "FE/Basis/BasisExceptions.h" +#include "FE/Basis/BasisFactory.h" +#include "FE/Common/FEException.h" + #include "consts.h" #include "mat_fun.h" #include "utils.h" #include "lapack_defs.h" +#include #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include namespace nn { @@ -37,22 +49,392 @@ using namespace consts; // Define maps used to set element Gauss integration data. #include "nn_elem_gip.h" -// Define maps used to set element shape function data. -#include "nn_elem_gnn.h" - -// Define maps used to get element shape function 2nd derivative data. -#include "nn_elem_gnnxx.h" - // Define a map type used to set the bounds of element shape functions. #include "nn_elem_nn_bnds.h" +namespace { + +namespace fe = svmp::FE; +namespace febasis = svmp::FE::basis; + +std::string solver_element_name(consts::ElementType eType) +{ + auto it = consts::element_type_to_string.find(eType); + if (it != consts::element_type_to_string.end()) { + return it->second + " (" + std::to_string(static_cast(eType)) + ")"; + } + return "unknown (" + std::to_string(static_cast(eType)) + ")"; +} + +/// Translate a solver element type into its FE library counterpart. This is a +/// pure renaming between the two enum vocabularies: the FE library owns the +/// choice of basis family and polynomial order for each element type +/// (basis_factory::default_basis_request). The switch deliberately has no +/// default case so that compilers building with -Wswitch flag any newly added +/// solver element type that is missing a mapping here. Returns std::nullopt for +/// element types the FE Basis does not implement (NA/PNT/NRB); callers test FE +/// Basis support with has_value(). +std::optional to_fe_element_type(consts::ElementType eType) +{ + switch (eType) { + case consts::ElementType::LIN1: return fe::ElementType::Line2; + case consts::ElementType::LIN2: return fe::ElementType::Line3; + case consts::ElementType::TRI3: return fe::ElementType::Triangle3; + case consts::ElementType::TRI6: return fe::ElementType::Triangle6; + case consts::ElementType::QUD4: return fe::ElementType::Quad4; + case consts::ElementType::QUD8: return fe::ElementType::Quad8; + case consts::ElementType::QUD9: return fe::ElementType::Quad9; + case consts::ElementType::TET4: return fe::ElementType::Tetra4; + case consts::ElementType::TET10: return fe::ElementType::Tetra10; + case consts::ElementType::HEX8: return fe::ElementType::Hex8; + case consts::ElementType::HEX20: return fe::ElementType::Hex20; + case consts::ElementType::HEX27: return fe::ElementType::Hex27; + case consts::ElementType::WDG: return fe::ElementType::Wedge6; + + // No FE basis mapping: points use dedicated shape data in get_gnn and + // NURBS are outside the current FE Basis scope. + case consts::ElementType::NA: + case consts::ElementType::PNT: + case consts::ElementType::NRB: + return std::nullopt; + + // A solver element type with no case above is a missing mapping, not a + // deliberately unsupported type; fail loudly instead of relying on the + // unhandled-enum compiler warning being enabled. + default: + svmp::raise("to_fe_element_type: unhandled solver element type " + + std::to_string(static_cast(eType))); + } +} + +/// Whether the FE Basis face adapter can evaluate face shape functions for +/// eType. An element face is always a point, line, or surface topology, so the +/// switch restricts support to those types (a volume element never appears as a +/// face); it then defers to to_fe_element_type to confirm the FE Basis library +/// actually provides a mapping for that face type. The face get_gnn uses this +/// to choose between the FE Basis path and the explicit paths. +bool supports_face_basis_adapter_for(consts::ElementType eType) +{ + switch (eType) { + case consts::ElementType::LIN1: + case consts::ElementType::LIN2: + case consts::ElementType::TRI3: + case consts::ElementType::TRI6: + case consts::ElementType::QUD4: + case consts::ElementType::QUD8: + case consts::ElementType::QUD9: + return to_fe_element_type(eType).has_value(); + default: + return false; + } +} + +/// Return the shared FE basis for a solver element type, constructing it on +/// first use. Basis construction is not free (node-lattice generation, and a +/// Vandermonde inversion for quadrilateral serendipity), while callers invoke +/// this per Gauss point or per probe point, so instances are cached per +/// element type. Sharing is safe: bases are immutable after construction, +/// evaluation is const, and BasisFunction scratch state is thread_local. +const febasis::BasisFunction& basis_for_solver_element(consts::ElementType eType) +{ + static std::mutex cache_mutex; + static std::map> cache; + + const auto fe_type = to_fe_element_type(eType); + if (!fe_type) { + svmp::raise("No FE Basis selection for solver element " + solver_element_name(eType)); + } + + const std::lock_guard lock(cache_mutex); + auto it = cache.find(eType); + if (it == cache.end()) { + it = cache.emplace(eType, febasis::basis_factory::create_default_for(*fe_type)).first; + } + return *it->second; +} + +/// Permutation from a solver element's local node ordering to the FE Basis +/// ReferenceNodeLayout ordering, indexed by the solver-local node number: +/// map[solver_node] is the matching FE Basis node. The solver and the FE Basis +/// library number element nodes with different conventions, so this table +/// reconciles them at the adapter boundary. An empty span means the two +/// orderings already coincide (identity) and no permutation is applied, which +/// holds for the line, Quad4/8/9, and the entire hex family (Hex8/20/27): the FE +/// Basis exposes those in the same VTK-based ordering the solver ingests from +/// .vtu meshes. Only the simplex families need a permutation, because the solver +/// labels simplex corners origin-last while the FE Basis lattice is origin-first; +/// Wedge6 (WDG) reuses the Triangle6 table, since its two triangular node triples +/// are reordered exactly like a 6-node triangle. +/// \note These tables must stay consistent with the FE Basis lattice ordering; +/// a mismatch would silently assign shape functions to the wrong nodes. +std::span solver_to_basis_node_map(consts::ElementType eType) +{ + static constexpr std::array tri3{1, 2, 0}; + static constexpr std::array tri6{1, 2, 0, 4, 5, 3}; + static constexpr std::array tet4{1, 2, 3, 0}; + static constexpr std::array tet10{1, 2, 3, 0, 5, 9, 8, 4, 6, 7}; + + switch (eType) { + case consts::ElementType::TRI3: + return tri3; + case consts::ElementType::TRI6: + case consts::ElementType::WDG: + return tri6; + case consts::ElementType::TET4: + return tet4; + case consts::ElementType::TET10: + return tet10; + default: + return {}; + } +} + +/// Map a single solver-local node index to its FE Basis node index for eType by +/// applying solver_to_basis_node_map (identity when no permutation is +/// registered). Throws BasisNodeOrderingException when solver_node is negative +/// or falls outside the element's node map. +std::size_t basis_index_for_solver_node(consts::ElementType eType, const int solver_node) +{ + if (solver_node < 0) { + svmp::raise("Solver node " + std::to_string(solver_node) + + " is outside node map for " + solver_element_name(eType)); + } + + const auto node = static_cast(solver_node); + const auto map = solver_to_basis_node_map(eType); + if (map.empty()) { + return node; + } + if (node < map.size()) { + return map[node]; + } + svmp::raise("Solver node " + std::to_string(solver_node) + + " is outside node map for " + solver_element_name(eType)); +} + +/// Build a 3-component FE Basis reference coordinate from column g of the solver +/// xi array, zero-filling the trailing components that are inactive for +/// lower-dimensional elements. Throws BasisConfigurationException when xi has +/// fewer rows than the basis reference dimension. +fe::math::Vector make_basis_point(const febasis::BasisFunction& basis, + const int g, + const Array& xi) +{ + if (xi.nrows() < basis.dimension()) { + svmp::raise("xi has " + std::to_string(xi.nrows()) + + " rows but FE Basis element requires " + std::to_string(basis.dimension()) + + " reference coordinates"); + } + + // Inactive trailing components must be zero for lower-dimensional elements; + // Eigen-backed vectors are not zero-initialized by default construction. + fe::math::Vector point = fe::math::Vector::Zero(); + for (int d = 0; d < basis.dimension(); ++d) { + point[static_cast(d)] = xi(d, g); + } + return point; +} + +/// Scatter FE Basis values and gradients (in ReferenceNodeLayout order) into the +/// solver N and Nx arrays at Gauss point g, permuting into solver node order via +/// basis_index_for_solver_node. Validates the value and gradient counts against +/// eNoN and zeroes unused gradient rows. +void copy_basis_values_to_solver_arrays(consts::ElementType eType, + const int eNoN, + const int g, + const std::vector& values, + const std::vector& gradients, + Array& N, + Array3& Nx) +{ + if (values.size() != static_cast(eNoN)) { + svmp::raise("FE Basis value count " + std::to_string(values.size()) + + " does not match solver eNoN " + std::to_string(eNoN)); + } + if (gradients.size() != static_cast(eNoN)) { + svmp::raise("FE Basis gradient count " + std::to_string(gradients.size()) + + " does not match solver eNoN " + std::to_string(eNoN)); + } + + for (int a = 0; a < eNoN; ++a) { + const auto basis_index = basis_index_for_solver_node(eType, a); + if (basis_index >= values.size() || basis_index >= gradients.size()) { + svmp::raise("Solver node " + std::to_string(a) + " maps to FE Basis node " + + std::to_string(basis_index) + " outside basis output for " + + solver_element_name(eType)); + } + + N(a, g) = values[basis_index]; + + for (int d = 0; d < Nx.nrows(); ++d) { + Nx(d, a, g) = 0.0; + } + const int ndim = std::min(Nx.nrows(), 3); + for (int d = 0; d < ndim; ++d) { + Nx(d, a, g) = gradients[basis_index][static_cast(d)]; + } + } +} + +/// Evaluate the cached FE Basis for eType at Gauss point g and write the solver +/// N and Nx arrays. Nx holds reference-space gradients only; physical-coordinate +/// derivatives are formed later by the solver from the mapping Jacobian. +void evaluate_basis_values_and_gradients(const int insd, + consts::ElementType eType, + const int eNoN, + const int g, + Array& xi, + Array& N, + Array3& Nx) +{ + const auto& basis = basis_for_solver_element(eType); + if (insd < basis.dimension()) { + svmp::raise("solver insd " + std::to_string(insd) + + " is smaller than FE Basis reference dimension " + std::to_string(basis.dimension())); + } + + const auto point = make_basis_point(basis, g, xi); + std::vector values; + std::vector gradients; + basis.evaluate_values(point, values); + basis.evaluate_gradients(point, gradients); + + // FE Basis owns the formulas; fsType and mshType remain the solver-facing storage contract. + copy_basis_values_to_solver_arrays(eType, eNoN, g, values, gradients, N, Nx); +} + +/// evaluate_basis_values_and_gradients specialized to a faceType, using the +/// face's own reference dimension (xi rows) and N/Nx storage. +void evaluate_face_basis_values_and_gradients(const int gaus_pt, faceType& face) +{ + evaluate_basis_values_and_gradients( + face.xi.nrows(), + face.eType, + face.eNoN, + gaus_pt, + face.xi, + face.N, + face.Nx); +} + +/// Number of packed second-derivative components the solver Nxx stores for a +/// given reference dimension: 1 in 1D, 3 in 2D, 6 in 3D. Throws +/// BasisConfigurationException for any other dimension. +int required_nxx_components_for_dimension(const int dimension) +{ + switch (dimension) { + case 1: + return 1; + case 2: + return 3; + case 3: + return 6; + default: + svmp::raise("Unsupported FE Basis reference dimension " + std::to_string(dimension)); + } +} + +/// Scatter FE Basis Hessians (in ReferenceNodeLayout order) into the packed +/// solver Nxx array at Gauss point g, permuting into solver node order. Packing +/// is [dxx, dyy, dxy] in 2D and [dxx, dyy, dzz, dxy, dyz, dxz] in 3D. Validates +/// the Hessian count against eNoN and the Nxx row count against the dimension. +void copy_basis_hessians_to_solver_nxx(consts::ElementType eType, + const int eNoN, + const int g, + const int dimension, + const std::vector& hessians, + Array3& Nxx) +{ + if (hessians.size() != static_cast(eNoN)) { + svmp::raise("FE Basis Hessian count " + std::to_string(hessians.size()) + + " does not match solver eNoN " + std::to_string(eNoN)); + } + + const int required_components = required_nxx_components_for_dimension(dimension); + if (Nxx.nrows() < required_components) { + svmp::raise("solver Nxx has " + std::to_string(Nxx.nrows()) + + " rows but FE Basis Hessian packing requires " + std::to_string(required_components)); + } + + for (int a = 0; a < eNoN; ++a) { + for (int i = 0; i < Nxx.nrows(); ++i) { + Nxx(i, a, g) = 0.0; + } + + const auto basis_index = basis_index_for_solver_node(eType, a); + if (basis_index >= hessians.size()) { + svmp::raise("Solver node " + std::to_string(a) + " maps to FE Basis Hessian node " + + std::to_string(basis_index) + " outside basis output for " + + solver_element_name(eType)); + } + + const auto& hessian = hessians[basis_index]; + Nxx(0, a, g) = hessian(0, 0); + if (dimension >= 2) { + Nxx(1, a, g) = hessian(1, 1); + Nxx(2, a, g) = hessian(0, 1); + } + if (dimension >= 3) { + Nxx(2, a, g) = hessian(2, 2); + Nxx(3, a, g) = hessian(0, 1); + Nxx(4, a, g) = hessian(1, 2); + Nxx(5, a, g) = hessian(0, 2); + } + } +} + +/// Evaluate the cached FE Basis Hessians for eType at Gauss point gaus_pt and +/// write the packed solver Nxx array. Validates insd and ind2 against the basis +/// reference dimension and the required packed-component count. +void evaluate_basis_hessians(const int insd, + const int ind2, + consts::ElementType eType, + const int eNoN, + const int gaus_pt, + const Array& xi, + Array3& Nxx) +{ + const auto& basis = basis_for_solver_element(eType); + if (insd < basis.dimension()) { + svmp::raise("solver insd " + std::to_string(insd) + + " is smaller than FE Basis reference dimension " + std::to_string(basis.dimension())); + } + + const int required_components = required_nxx_components_for_dimension(basis.dimension()); + if (ind2 < required_components) { + svmp::raise("solver ind2 " + std::to_string(ind2) + + " is smaller than packed Hessian component count " + std::to_string(required_components)); + } + + const auto point = make_basis_point(basis, gaus_pt, xi); + std::vector hessians; + basis.evaluate_hessians(point, hessians); + + // Solver Nxx packing is dxx, dyy, dxy in 2D and dxx, dyy, dzz, dxy, dyz, dxz in 3D. + copy_basis_hessians_to_solver_nxx(eType, eNoN, gaus_pt, basis.dimension(), hessians, Nxx); +} + +/// Shape data for a point (0-D) face: a single unit basis value with zero +/// derivatives. Used for the PNT face case, which has no FE Basis evaluator. +void set_point_face_shape_data(const int gaus_pt, faceType& face) +{ + face.N(0, gaus_pt) = 1.0; + for (int row = 0; row < face.Nx.nrows(); ++row) { + for (int col = 0; col < face.Nx.ncols(); ++col) { + face.Nx(row, col, gaus_pt) = 0.0; + } + } +} + +} // namespace + void get_gip(const int insd, consts::ElementType eType, const int nG, Vector& w, Array& xi) { try { get_element_gauss_int_data[eType](insd, nG, w, xi); } catch (const std::bad_function_call& exception) { - throw std::runtime_error("No support for element etype " + std::to_string(static_cast(eType)) + - " in 'get_element_gauss_int_data'."); + svmp::raise("No support in 'get_element_gauss_int_data'", + solver_element_name(eType)); } } @@ -65,7 +447,8 @@ void get_gip(mshType& mesh) try { set_element_gauss_int_data[mesh.eType](mesh); } catch (const std::bad_function_call& exception) { - throw std::runtime_error("No support for mesh etype " + std::to_string(static_cast(mesh.eType)) + " in 'set_element_gauss_int_data'."); + svmp::raise("No support in 'set_element_gauss_int_data'", + solver_element_name(mesh.eType)); } } @@ -74,7 +457,8 @@ void get_gip(Simulation* simulation, faceType& face) try { set_face_gauss_int_data[face.eType](face); } catch (const std::bad_function_call& exception) { - throw std::runtime_error("No support for face type " + std::to_string(static_cast(face.eType)) + " in 'set_face_gauss_int_data'."); + svmp::raise("No support in 'set_face_gauss_int_data'", + solver_element_name(face.eType)); } } @@ -83,15 +467,14 @@ void get_gip(Simulation* simulation, faceType& face) void get_gnn(const int insd, consts::ElementType eType, const int eNoN, const int g, Array& xi, Array& N, Array3& Nx) { - try { - get_element_shape_data[eType](insd, eNoN, g, xi, N, Nx); - } catch (const std::bad_function_call& exception) { - throw std::runtime_error("[get_gnn] No support for element type " + std::to_string(static_cast(eType)) + " in 'get_element_shape_data'."); + if (!to_fe_element_type(eType).has_value()) { + svmp::raise("[get_gnn] FE Basis does not support solver element " + solver_element_name(eType)); } + + evaluate_basis_values_and_gradients(insd, eType, eNoN, g, xi, N, Nx); } -/// @brief A big fat hack because the Fortran GETNN() operates on primitive types but -/// the C++ version does not, uses Array and Vector objects. +/// @brief Adapter overload for vector-style callers. // void get_gnn(const int nsd, consts::ElementType eType, const int eNoN, Vector& xi, Vector& N, Array& Nx) @@ -111,44 +494,49 @@ void get_gnn(const int nsd, consts::ElementType eType, const int eNoN, Vector(mesh.eType)) + " in 'set_element_shape_data'."); - } + nn::get_gnn(mesh.xi.nrows(), mesh.eType, mesh.eNoN, gaus_pt, mesh.xi, mesh.N, mesh.Nx); } void get_gnn(Simulation* simulation, int gaus_pt, faceType& face) { - try { - set_face_shape_data[face.eType](gaus_pt, face); - } catch (const std::bad_function_call& exception) { - throw std::runtime_error("No support for face type " + std::to_string(static_cast(face.eType)) + " in 'set_face_shape_data'."); + using consts::ElementType; + + svmp::throw_if(face.eType == ElementType::NRB, "[get_gnn(face)] NRB face shape functions are unsupported by FE Basis"); + + if (face.eType == ElementType::PNT) { + set_point_face_shape_data(gaus_pt, face); + return; + } + + if (supports_face_basis_adapter_for(face.eType)) { + // FE Basis owns mapped face N/Nx formulas; faceType remains the solver-facing storage contract. + evaluate_face_basis_values_and_gradients(gaus_pt, face); + return; } + + svmp::raise("[get_gnn(face)] FE Basis does not support face element " + solver_element_name(face.eType)); } -/// @brief Returns second order derivatives at given natural coords -/// -/// Replicates 'SUBROUTINE GETGNNxx(insd, ind2, eType, eNoN, xi, Nxx)'. +/// @brief Returns second order derivatives at given natural coords. // void get_gn_nxx(const int insd, const int ind2, consts::ElementType eType, const int eNoN, const int gaus_pt, const Array& xi, Array3& Nxx) { using namespace consts; - // Element types that don't have 2nd derivatives computed for them. - static std::set no_derivs{ElementType::NRB, ElementType::QUD4, ElementType::HEX8, - ElementType::HEX20, ElementType::HEX27}; - - if (no_derivs.count(eType) != 0) { + // NA/NRB/PNT have no FE Basis Hessian support (NA is unassigned; NRB/PNT are + // outside the current FE Basis scope). Leave Nxx at its zero-initialized + // state so callers may invoke this for every element type unconditionally. + if (eType == ElementType::NA || eType == ElementType::NRB || eType == ElementType::PNT) { return; } - try { - get_element_2nd_derivs[eType](insd, ind2, eNoN, gaus_pt, xi, Nxx); - } catch (const std::bad_function_call& exception) { - throw std::runtime_error("[get_gn_nxx] No support for element type " + std::to_string(static_cast(eType)) + " in 'get_element_2nd_derivs'."); + if (!to_fe_element_type(eType).has_value()) { + svmp::raise("[get_gn_nxx] FE Basis Hessian evaluation does not support solver element " + + solver_element_name(eType)); } + + evaluate_basis_hessians(insd, ind2, eType, eNoN, gaus_pt, xi, Nxx); } /// @brief Sets bounds on Gauss integration points in parametric space and @@ -332,9 +720,7 @@ void get_nnx(const int nsd, const consts::ElementType eType, const int eNoN, con l1 = (l1 && l2 && l3 && l4); - if (!l1) { - throw std::runtime_error("Error in computing shape functions"); - } + svmp::throw_if(!l1, "Error in computing shape functions"); } /// @brief Inverse maps {xp} to {$\xi$} in an element with coordinates {xl} using Newton's method @@ -582,8 +968,9 @@ void gnnb(const ComMod& com_mod, const faceType& lFa, const int e, const int g, } if (!found_node) { - throw std::runtime_error("[svMultiPhysics::gnnb] ERROR: The '" + lFa.name + "' face node " + std::to_string(Ac) + - " could not be matched to a node in the '" + msh.name + "' volume mesh."); + svmp::raise("[svMultiPhysics::gnnb] ERROR: The '" + lFa.name + "' face node " + + std::to_string(Ac) + " could not be matched to a node in the '" + + msh.name + "' volume mesh."); } ptr(a) = b; @@ -632,7 +1019,7 @@ void gnnb(const ComMod& com_mod, const faceType& lFa, const int e, const int g, } break; default: - throw std::runtime_error("gnnb: invalid MechanicalConfigurationType provided"); + svmp::raise("gnnb: invalid MechanicalConfigurationType provided"); } } } @@ -820,9 +1207,7 @@ void gn_nxx(const int l, const int eNoN, const int nsd, const int insd, Array(INFO != 0, "[gn_nxx] Error in Lapack", "LAPACK dgesv", INFO); Nxx = B; @@ -891,9 +1276,7 @@ void gn_nxx(const int l, const int eNoN, const int nsd, const int insd, Array(INFO != 0, "[gn_nxx] Error in Lapack", "LAPACK dgesv", INFO); Nxx = B; } @@ -940,8 +1323,9 @@ void select_ele(const ComMod& com_mod, mshType& mesh) set_1d_element_props[mesh.eNoN](insd, mesh); } } catch (const std::bad_function_call& exception) { - throw std::runtime_error("[select_ele] No support for " + std::to_string(mesh.eNoN) + " noded " + - std::to_string(insd) + "D elements."); + svmp::raise("[select_ele] No support for " + std::to_string(mesh.eNoN) + + " noded " + std::to_string(insd) + "D elements.", + solver_element_name(mesh.eType)); } // Set mesh 'w' and 'xi' arrays used for Gauss integration. @@ -997,8 +1381,9 @@ void select_eleb(Simulation* simulation, mshType& mesh, faceType& face) try { set_face_element_props[face.eNoN](insd, face); } catch (const std::bad_function_call& exception) { - throw std::runtime_error("No support for " + std::to_string(face.eNoN) + " noded " + - std::to_string(insd) + "D elements in 'set_face_element_props'."); + svmp::raise("No support for " + std::to_string(face.eNoN) + " noded " + + std::to_string(insd) + "D elements in 'set_face_element_props'.", + solver_element_name(face.eType)); } // Set face 'w' and 'xi' arrays used for Gauss integration. @@ -1015,4 +1400,3 @@ void select_eleb(Simulation* simulation, mshType& mesh, faceType& face) } }; - diff --git a/Code/Source/solver/nn_elem_gnn.h b/Code/Source/solver/nn_elem_gnn.h deleted file mode 100644 index 33564d45b..000000000 --- a/Code/Source/solver/nn_elem_gnn.h +++ /dev/null @@ -1,1586 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. -// SPDX-License-Identifier: BSD-3-Clause - -/// @brief Define a map type used to set element shape function data. -/// -/// Reproduces the Fortran 'GETGNN' subroutine. -// -using GetElementShapeMapType = std::map&, Array&, Array3&)>>; - -GetElementShapeMapType get_element_shape_data = { - - {ElementType::HEX8, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - N(0,g) = lx*ly*lz/8.0; - N(1,g) = ux*ly*lz/8.0; - N(2,g) = ux*uy*lz/8.0; - N(3,g) = lx*uy*lz/8.0; - N(4,g) = lx*ly*uz/8.0; - N(5,g) = ux*ly*uz/8.0; - N(6,g) = ux*uy*uz/8.0; - N(7,g) = lx*uy*uz/8.0; - - Nx(0,0,g) = -ly*lz/8.0; - Nx(1,0,g) = -lx*lz/8.0; - Nx(2,0,g) = -lx*ly/8.0; - - Nx(0,1,g) = ly*lz/8.0; - Nx(1,1,g) = -ux*lz/8.0; - Nx(2,1,g) = -ux*ly/8.0; - - Nx(0,2,g) = uy*lz/8.0; - Nx(1,2,g) = ux*lz/8.0; - Nx(2,2,g) = -ux*uy/8.0; - - Nx(0,3,g) = -uy*lz/8.0; - Nx(1,3,g) = lx*lz/8.0; - Nx(2,3,g) = -lx*uy/8.0; - - Nx(0,4,g) = -ly*uz/8.0; - Nx(1,4,g) = -lx*uz/8.0; - Nx(2,4,g) = lx*ly/8.0; - - Nx(0,5,g) = ly*uz/8.0; - Nx(1,5,g) = -ux*uz/8.0; - Nx(2,5,g) = ux*ly/8.0; - - Nx(0,6,g) = uy*uz/8.0; - Nx(1,6,g) = ux*uz/8.0; - Nx(2,6,g) = ux*uy/8.0; - - Nx(0,7,g) = -uy*uz/8.0; - Nx(1,7,g) = lx*uz/8.0; - Nx(2,7,g) = lx*uy/8.0; - } - }, - - {ElementType::HEX20, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - double mx = lx*ux; - double my = ly*uy; - double mz = lz*uz; - - N(0, g) = lx*ly*lz*(lx+ly+lz-5.0)/8.0; - N(1, g) = ux*ly*lz*(ux+ly+lz-5.0)/8.0; - N(2, g) = ux*uy*lz*(ux+uy+lz-5.0)/8.0; - N(3, g) = lx*uy*lz*(lx+uy+lz-5.0)/8.0; - N(4, g) = lx*ly*uz*(lx+ly+uz-5.0)/8.0; - N(5, g) = ux*ly*uz*(ux+ly+uz-5.0)/8.0; - N(6, g) = ux*uy*uz*(ux+uy+uz-5.0)/8.0; - N(7, g) = lx*uy*uz*(lx+uy+uz-5.0)/8.0; - N(8, g) = mx*ly*lz/4.0; - N(9, g) = ux*my*lz/4.0; - N(10, g) = mx*uy*lz/4.0; - N(11, g) = lx*my*lz/4.0; - N(12, g) = mx*ly*uz/4.0; - N(13, g) = ux*my*uz/4.0; - N(14, g) = mx*uy*uz/4.0; - N(15, g) = lx*my*uz/4.0; - N(16, g) = lx*ly*mz/4.0; - N(17, g) = ux*ly*mz/4.0; - N(18, g) = ux*uy*mz/4.0; - N(19, g) = lx*uy*mz/4.0; - - // N(1) = lx*ly*lz*(lx+ly+lz-5.0)/8.0; - int n = 0; - Nx(0,n,g) = -ly*lz*(lx+ly+lz-5.0+lx)/8.0; - Nx(1,n,g) = -lx*lz*(lx+ly+lz-5.0+ly)/8.0; - Nx(2,n,g) = -lx*ly*(lx+ly+lz-5.0+lz)/8.0; - -//c N(n,g) = ux*ly*lz*(ux+ly+lz-5.0)/8.0; - n += 1; - Nx(0,n,g) = ly*lz*(ux+ly+lz-5.0+ux)/8.0; - Nx(1,n,g) = -ux*lz*(ux+ly+lz-5.0+ly)/8.0; - Nx(2,n,g) = -ux*ly*(ux+ly+lz-5.0+lz)/8.0; - -//c N(n,g) = ux*uy*lz*(ux+uy+lz-5.0)/8.0 - n += 1; - Nx(0,n,g) = uy*lz*(ux+uy+lz-5.0+ux)/8.0; - Nx(1,n,g) = ux*lz*(ux+uy+lz-5.0+uy)/8.0; - Nx(2,n,g) = -ux*uy*(ux+uy+lz-5.0+lz)/8.0; - -//c N(n,g) = lx*uy*lz*(lx+uy+lz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -uy*lz*(lx+uy+lz-5.0+lx)/8.0; - Nx(1,n,g) = lx*lz*(lx+uy+lz-5.0+uy)/8.0; - Nx(2,n,g) = -lx*uy*(lx+uy+lz-5.0+lz)/8.0; - -//c N(n,g) = lx*ly*uz*(lx+ly+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -ly*uz*(lx+ly+uz-5.0+lx)/8.0; - Nx(1,n,g) = -lx*uz*(lx+ly+uz-5.0+ly)/8.0; - Nx(2,n,g) = lx*ly*(lx+ly+uz-5.0+uz)/8.0; - -//c N(n,g) = ux*ly*uz*(ux+ly+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = ly*uz*(ux+ly+uz-5.0+ux)/8.0; - Nx(1,n,g) = -ux*uz*(ux+ly+uz-5.0+ly)/8.0; - Nx(2,n,g) = ux*ly*(ux+ly+uz-5.0+uz)/8.0; - -//c N(n,g) = ux*uy*uz*(ux+uy+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = uy*uz*(ux+uy+uz-5.0+ux)/8.0; - Nx(1,n,g) = ux*uz*(ux+uy+uz-5.0+uy)/8.0; - Nx(2,n,g) = ux*uy*(ux+uy+uz-5.0+uz)/8.0; - -//c N(n,g) = lx*uy*uz*(lx+uy+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -uy*uz*(lx+uy+uz-5.0+lx)/8.0; - Nx(1,n,g) = lx*uz*(lx+uy+uz-5.0+uy)/8.0; - Nx(2,n,g) = lx*uy*(lx+uy+uz-5.0+uz)/8.0; - -//c N(n,g) = mx*ly*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*ly*lz/4.0; - Nx(1,n,g) = -mx*lz/4.0; - Nx(2,n,g) = -mx*ly/4.0; - -//c N(0n,g) = ux*my*lz/4.0 - n += 1; - Nx(0,n,g) = my*lz/4.0; - Nx(1,n,g) = (ly - uy)*ux*lz/4.0; - Nx(2,n,g) = -ux*my/4.0; - -//c N(0n,g) = mx*uy*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*uy*lz/4.0; - Nx(1,n,g) = mx*lz/4.0; - Nx(2,n,g) = -mx*uy/4.0; - -//c N(0n,g) = lx*my*lz/4.0 - n += 1; - Nx(0,n,g) = -my*lz/4.0; - Nx(1,n,g) = (ly - uy)*lx*lz/4.0; - Nx(2,n,g) = -lx*my/4.0; - -//c N(0n,g) = mx*ly*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*ly*uz/4.0; - Nx(1,n,g) = -mx*uz/4.0; - Nx(2,n,g) = mx*ly/4.0; - -//c N(0n,g) = ux*my*uz/4.0 - n += 1; - Nx(0,n,g) = my*uz/4.0; - Nx(1,n,g) = (ly - uy)*ux*uz/4.0; - Nx(2,n,g) = ux*my/4.0; - -//c N(0n,g) = mx*uy*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*uy*uz/4.0; - Nx(1,n,g) = mx*uz/4.0; - Nx(2,n,g) = mx*uy/4.0; - -//c N(0n,g) = lx*my*uz/4.0 - n += 1; - Nx(0,n,g) = -my*uz/4.0; - Nx(1,n,g) = (ly - uy)*lx*uz/4.0; - Nx(2,n,g) = lx*my/4.0; - -//c N(0n,g) = lx*ly*mz/4.0 - n += 1; - Nx(0,n,g) = -ly*mz/4.0; - Nx(1,n,g) = -lx*mz/4.0; - Nx(2,n,g) = (lz - uz)*lx*ly/4.0; - -//c N(0n,g) = ux*ly*mz/4.0 - n += 1; - Nx(0,n,g) = ly*mz/4.0; - Nx(1,n,g) = -ux*mz/4.0; - Nx(2,n,g) = (lz - uz)*ux*ly/4.0; - -//c N(0n,g) = ux*uy*mz/4.0 - n += 1; - Nx(0,n,g) = uy*mz/4.0; - Nx(1,n,g) = ux*mz/4.0; - Nx(2,n,g) = (lz - uz)*ux*uy/4.0; - -//c N(n,g) = lx*uy*mz/4.0 - n += 1; - Nx(0,n,g) = -uy*mz/4.0; - Nx(1,n,g) = lx*mz/4.0; - Nx(2,n,g) = (lz - uz)*lx*uy/4.0; - } - }, - - {ElementType::HEX27, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - double mx = xi(0,g); - double my = xi(1,g); - double mz = xi(2,g); - - N(0,g) = -mx*lx*my*ly*mz*lz/8.0; - N(1,g) = mx*ux*my*ly*mz*lz/8.0; - N(2,g) = -mx*ux*my*uy*mz*lz/8.0; - N(3,g) = mx*lx*my*uy*mz*lz/8.0; - N(4,g) = mx*lx*my*ly*mz*uz/8.0; - N(5,g) = -mx*ux*my*ly*mz*uz/8.0; - N(6,g) = mx*ux*my*uy*mz*uz/8.0; - N(7,g) = -mx*lx*my*uy*mz*uz/8.0; - N(8,g) = lx*ux*my*ly*mz*lz/4.0; - N(9,g) = -mx*ux*ly*uy*mz*lz/4.0; - N(10,g) = -lx*ux*my*uy*mz*lz/4.0; - N(11,g) = mx*lx*ly*uy*mz*lz/4.0; - N(12,g) = -lx*ux*my*ly*mz*uz/4.0; - N(13,g) = mx*ux*ly*uy*mz*uz/4.0; - N(14,g) = lx*ux*my*uy*mz*uz/4.0; - N(15,g) = -mx*lx*ly*uy*mz*uz/4.0; - N(16,g) = mx*lx*my*ly*lz*uz/4.0; - N(17,g) = -mx*ux*my*ly*lz*uz/4.0; - N(18,g) = mx*ux*my*uy*lz*uz/4.0; - N(19,g) = -mx*lx*my*uy*lz*uz/4.0; - - N(20,g) = -mx*lx*ly*uy*lz*uz/2.0; - N(21,g) = mx*ux*ly*uy*lz*uz/2.0; - N(22,g) = -lx*ux*my*ly*lz*uz/2.0; - N(23,g) = lx*ux*my*uy*lz*uz/2.0; - N(24,g) = -lx*ux*ly*uy*mz*lz/2.0; - N(25,g) = lx*ux*ly*uy*mz*uz/2.0; - - N(26,g) = lx*ux*ly*uy*lz*uz; - - // N(0) = -mx*lx*my*ly*mz*lz/8.0 - int n = 0; - Nx(0,n,g) = -(lx - mx)*my*ly*mz*lz/8.0; - Nx(1,n,g) = -(ly - my)*mx*lx*mz*lz/8.0; - Nx(2,n,g) = -(lz - mz)*mx*lx*my*ly/8.0; - - // N(n,g) = mx*ux*my*ly*mz*lz/8.0 - n += 1; - Nx(0,n,g) = (mx + ux)*my*ly*mz*lz/8.0; - Nx(1,n,g) = (ly - my)*mx*ux*mz*lz/8.0; - Nx(2,n,g) = (lz - mz)*mx*ux*my*ly/8.0; - - // N(n,g) = -mx*ux*my*uy*mz*lz/8.0 - n += 1; - Nx(0,n,g) = -(mx + ux)*my*uy*mz*lz/8.0; - Nx(1,n,g) = -(my + uy)*mx*ux*mz*lz/8.0; - Nx(2,n,g) = -(lz - mz)*mx*ux*my*uy/8.0; - - // N(n,g) = mx*lx*my*uy*mz*lz/8.0 - n += 1; - Nx(0,n,g) = (lx - mx)*my*uy*mz*lz/8.0; - Nx(1,n,g) = (my + uy)*mx*lx*mz*lz/8.0; - Nx(2,n,g) = (lz - mz)*mx*lx*my*uy/8.0; - - // N(n,g) = mx*lx*my*ly*mz*uz/8.0 - n += 1; - Nx(0,n,g) = (lx - mx)*my*ly*mz*uz/8.0; - Nx(1,n,g) = (ly - my)*mx*lx*mz*uz/8.0; - Nx(2,n,g) = (mz + uz)*mx*lx*my*ly/8.0; - - // N(n,g) = -mx*ux*my*ly*mz*uz/8.0 - n += 1; - Nx(0,n,g) = -(mx + ux)*my*ly*mz*uz/8.0; - Nx(1,n,g) = -(ly - my)*mx*ux*mz*uz/8.0; - Nx(2,n,g) = -(mz + uz)*mx*ux*my*ly/8.0; - - // N(n,g) = mx*ux*my*uy*mz*uz/8.0 - n += 1; - Nx(0,n,g) = (mx + ux)*my*uy*mz*uz/8.0; - Nx(1,n,g) = (my + uy)*mx*ux*mz*uz/8.0; - Nx(2,n,g) = (mz + uz)*mx*ux*my*uy/8.0; - - // N(n,g) = -mx*lx*my*uy*mz*uz/8.0 - n += 1; - Nx(0,n,g) = -(lx - mx)*my*uy*mz*uz/8.0; - Nx(1,n,g) = -(my + uy)*mx*lx*mz*uz/8.0; - Nx(2,n,g) = -(mz + uz)*mx*lx*my*uy/8.0; - - // N(n,g) = lx*ux*my*ly*mz*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*my*ly*mz*lz/4.0; - Nx(1,n,g) = (ly - my)*lx*ux*mz*lz/4.0; - Nx(2,n,g) = (lz - mz)*lx*ux*my*ly/4.0; - - // N(n,g) = -mx*ux*ly*uy*mz*lz/4.0 - n += 1; - Nx(0,n,g) = -(mx + ux)*ly*uy*mz*lz/4.0; - Nx(1,n,g) = -(ly - uy)*mx*ux*mz*lz/4.0; - Nx(2,n,g) = -(lz - mz)*mx*ux*ly*uy/4.0; - - // N(n,g) = -lx*ux*my*uy*mz*lz/4.0 - n += 1; - Nx(0,n,g) = -(lx - ux)*my*uy*mz*lz/4.0; - Nx(1,n,g) = -(my + uy)*lx*ux*mz*lz/4.0; - Nx(2,n,g) = -(lz - mz)*lx*ux*my*uy/4.0; - - // N(n,g) = mx*lx*ly*uy*mz*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - mx)*ly*uy*mz*lz/4.0; - Nx(1,n,g) = (ly - uy)*mx*lx*mz*lz/4.0; - Nx(2,n,g) = (lz - mz)*mx*lx*ly*uy/4.0; - - // N(n,g) = -lx*ux*my*ly*mz*uz/4.0 - n += 1; - Nx(0,n,g) = -(lx - ux)*my*ly*mz*uz/4.0; - Nx(1,n,g) = -(ly - my)*lx*ux*mz*uz/4.0; - Nx(2,n,g) = -(mz + uz)*lx*ux*my*ly/4.0; - - // N(n,g) = mx*ux*ly*uy*mz*uz/4.0 - n += 1; - Nx(0,n,g) = (mx + ux)*ly*uy*mz*uz/4.0; - Nx(1,n,g) = (ly - uy)*mx*ux*mz*uz/4.0; - Nx(2,n,g) = (mz + uz)*mx*ux*ly*uy/4.0; - - // N(n,g) = lx*ux*my*uy*mz*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*my*uy*mz*uz/4.0; - Nx(1,n,g) = (my + uy)*lx*ux*mz*uz/4.0; - Nx(2,n,g) = (mz + uz)*lx*ux*my*uy/4.0; - - // N(n,g) = -mx*lx*ly*uy*mz*uz/4.0 - n += 1; - Nx(0,n,g) = -(lx - mx)*ly*uy*mz*uz/4.0; - Nx(1,n,g) = -(ly - uy)*mx*lx*mz*uz/4.0; - Nx(2,n,g) = -(mz + uz)*mx*lx*ly*uy/4.0; - - // N(n,g) = mx*lx*my*ly*lz*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - mx)*my*ly*lz*uz/4.0; - Nx(1,n,g) = (ly - my)*mx*lx*lz*uz/4.0; - Nx(2,n,g) = (lz - uz)*mx*lx*my*ly/4.0; - - // N(n,g) = -mx*ux*my*ly*lz*uz/4.0 - n += 1; - Nx(0,n,g) = -(mx + ux)*my*ly*lz*uz/4.0; - Nx(1,n,g) = -(ly - my)*mx*ux*lz*uz/4.0; - Nx(2,n,g) = -(lz - uz)*mx*ux*my*ly/4.0; - - // N(n,g) = mx*ux*my*uy*lz*uz/4.0 - n += 1; - Nx(0,n,g) = (mx + ux)*my*uy*lz*uz/4.0; - Nx(1,n,g) = (my + uy)*mx*ux*lz*uz/4.0; - Nx(2,n,g) = (lz - uz)*mx*ux*my*uy/4.0; - - // N(n,g) = -mx*lx*my*uy*lz*uz/4.0 - n += 1; - Nx(0,n,g) = -(lx - mx)*my*uy*lz*uz/4.0; - Nx(1,n,g) = -(my + uy)*mx*lx*lz*uz/4.0; - Nx(2,n,g) = -(lz - uz)*mx*lx*my*uy/4.0; - - // N(n,g) = -mx*lx*ly*uy*lz*uz/2.0 - n += 1; - Nx(0,n,g) = -(lx - mx)*ly*uy*lz*uz/2.0; - Nx(1,n,g) = -(ly - uy)*mx*lx*lz*uz/2.0; - Nx(2,n,g) = -(lz - uz)*mx*lx*ly*uy/2.0; - - // N(n,g) = mx*ux*ly*uy*lz*uz/2.0 - n += 1; - Nx(0,n,g) = (mx + ux)*ly*uy*lz*uz/2.0; - Nx(1,n,g) = (ly - uy)*mx*ux*lz*uz/2.0; - Nx(2,n,g) = (lz - uz)*mx*ux*ly*uy/2.0; - - // N(n,g) = -lx*ux*my*ly*lz*uz/2.0 - n += 1; - Nx(0,n,g) = -(lx - ux)*my*ly*lz*uz/2.0; - Nx(1,n,g) = -(ly - my)*lx*ux*lz*uz/2.0; - Nx(2,n,g) = -(lz - uz)*lx*ux*my*ly/2.0; - - // N(n,g) = lx*ux*my*uy*lz*uz/2.0 - n += 1; - Nx(0,n,g) = (lx - ux)*my*uy*lz*uz/2.0; - Nx(1,n,g) = (my + uy)*lx*ux*lz*uz/2.0; - Nx(2,n,g) = (lz - uz)*lx*ux*my*uy/2.0; - - // N(n,g) = -lx*ux*ly*uy*mz*lz/2.0 - n += 1; - Nx(0,n,g) = -(lx - ux)*ly*uy*mz*lz/2.0; - Nx(1,n,g) = -(ly - uy)*lx*ux*mz*lz/2.0; - Nx(2,n,g) = -(lz - mz)*lx*ux*ly*uy/2.0; - - // N(n,g) = lx*ux*ly*uy*mz*uz/2.0 - n += 1; - Nx(0,n,g) = (lx - ux)*ly*uy*mz*uz/2.0; - Nx(1,n,g) = (ly - uy)*lx*ux*mz*uz/2.0; - Nx(2,n,g) = (mz + uz)*lx*ux*ly*uy/2.0; - - // N(n,g) = lx*ux*ly*uy*lz*uz - n += 1; - Nx(0,n,g) = (lx - ux)*ly*uy*lz*uz; - Nx(1,n,g) = (ly - uy)*lx*ux*lz*uz; - Nx(2,n,g) = (lz - uz)*lx*ux*ly*uy; - } - }, - - {ElementType::LIN1, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - N(0,g) = (1.0 - xi(0,g))*0.5; - N(1,g) = (1.0 + xi(0,g))*0.5; - - Nx(0,0,g) = -0.5; - Nx(0,1,g) = 0.5; - } - }, - - {ElementType::LIN2, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - N(0,g) = -xi(0,g)*(1.0 - xi(0,g))*0.50; - N(1,g) = xi(0,g)*(1.0 + xi(0,g))*0.50; - N(2,g) = (1.0 - xi(0,g))*(1.0 + xi(0,g)); - - Nx(0,0,g) = -0.50 + xi(0,g); - Nx(0,1,g) = 0.50 + xi(0,g); - Nx(0,2,g) = -2.0*xi(0,g); - } - }, - - {ElementType::QUD4, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - - N(0,g) = lx*ly / 4.0; - N(1,g) = ux*ly / 4.0; - N(2,g) = ux*uy / 4.0; - N(3,g) = lx*uy / 4.0; - - Nx(0,0,g) = -ly / 4.0; - Nx(1,0,g) = -lx / 4.0; - Nx(0,1,g) = ly / 4.0; - Nx(1,1,g) = -ux / 4.0; - Nx(0,2,g) = uy / 4.0; - Nx(1,2,g) = ux / 4.0; - Nx(0,3,g) = -uy / 4.0; - Nx(1,3,g) = lx / 4.0; - } - }, - - {ElementType::QUD9, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double mx = xi(0,g); - double my = xi(1,g); - - N(0,g) = mx*lx*my*ly/4.0; - N(1,g) = -mx*ux*my*ly/4.0; - N(2,g) = mx*ux*my*uy/4.0; - N(3,g) = -mx*lx*my*uy/4.0; - N(4,g) = -lx*ux*my*ly*0.50; - N(5,g) = mx*ux*ly*uy*0.50; - N(6,g) = lx*ux*my*uy*0.50; - N(7,g) = -mx*lx*ly*uy*0.50; - N(8,g) = lx*ux*ly*uy; - - Nx(0,0,g) = (lx - mx)*my*ly/4.0; - Nx(1,0,g) = (ly - my)*mx*lx/4.0; - Nx(0,1,g) = -(ux + mx)*my*ly/4.0; - Nx(1,1,g) = -(ly - my)*mx*ux/4.0; - Nx(0,2,g) = (ux + mx)*my*uy/4.0; - Nx(1,2,g) = (uy + my)*mx*ux/4.0; - Nx(0,3,g) = -(lx - mx)*my*uy/4.0; - Nx(1,3,g) = -(uy + my)*mx*lx/4.0; - Nx(0,4,g) = -(lx - ux)*my*ly*0.50; - Nx(1,4,g) = -(ly - my)*lx*ux*0.50; - Nx(0,5,g) = (ux + mx)*ly*uy*0.50; - Nx(1,5,g) = (ly - uy)*mx*ux*0.50; - Nx(0,6,g) = (lx - ux)*my*uy*0.50; - Nx(1,6,g) = (uy + my)*lx*ux*0.50; - Nx(0,7,g) = -(lx - mx)*ly*uy*0.50; - Nx(1,7,g) = -(ly - uy)*mx*lx*0.50; - Nx(0,8,g) = (lx - ux)*ly*uy; - Nx(1,8,g) = (ly - uy)*lx*ux; - } - }, - - {ElementType::TET4, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - //std::cout << "[get_element_shape_data] TET4 " << std::endl; - - N(0,g) = xi(0,g); - N(1,g) = xi(1,g); - N(2,g) = xi(2,g); - N(3,g) = 1.0 - xi(0,g) - xi(1,g) - xi(2,g); - - Nx(0,0,g) = 1.0; - Nx(1,0,g) = 0.0; - Nx(2,0,g) = 0.0; - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 1.0; - Nx(2,1,g) = 0.0; - Nx(0,2,g) = 0.0; - Nx(1,2,g) = 0.0; - Nx(2,2,g) = 1.0; - Nx(0,3,g) = -1.0; - Nx(1,3,g) = -1.0; - Nx(2,3,g) = -1.0; - } - }, - - {ElementType::TET10, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double s = 1.0 - xi(0,g) - xi(1,g) - xi(2,g); - N(0,g) = xi(0,g)*(2.0*xi(0,g) - 1.0); - N(1,g) = xi(1,g)*(2.0*xi(1,g) - 1.0); - N(2,g) = xi(2,g)*(2.0*xi(2,g) - 1.0); - N(3,g) = s * (2.0*s - 1.0); - N(4,g) = 4.0*xi(0,g)*xi(1,g); - N(5,g) = 4.0*xi(1,g)*xi(2,g); - N(6,g) = 4.0*xi(0,g)*xi(2,g); - N(7,g) = 4.0*xi(0,g)*s; - N(8,g) = 4.0*xi(1,g)*s; - N(9,g) = 4.0*xi(2,g)*s; - - Nx(0,0,g) = 4.0*xi(0,g) - 1.0; - Nx(1,0,g) = 0.0; - Nx(2,0,g) = 0.0; - - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 4.0*xi(1,g) - 1.0; - Nx(2,1,g) = 0.0; - - Nx(0,2,g) = 0.0; - Nx(1,2,g) = 0.0; - Nx(2,2,g) = 4.0*xi(2,g) - 1.0; - - Nx(0,3,g) = 1.0 - 4.0*s; - Nx(1,3,g) = 1.0 - 4.0*s; - Nx(2,3,g) = 1.0 - 4.0*s; - - Nx(0,4,g) = 4.0*xi(1,g); - Nx(1,4,g) = 4.0*xi(0,g); - Nx(2,4,g) = 0.0; - - Nx(0,5,g) = 0.0; - Nx(1,5,g) = 4.0*xi(2,g); - Nx(2,5,g) = 4.0*xi(1,g); - - Nx(0,6,g) = 4.0*xi(2,g); - Nx(1,6,g) = 0.0; - Nx(2,6,g) = 4.0*xi(0,g); - - Nx(0,7,g) = 4.0*( s - xi(0,g)); - Nx(1,7,g) = -4.0*xi(0,g); - Nx(2,7,g) = -4.0*xi(0,g); - - Nx(0,8,g) = -4.0*xi(1,g); - Nx(1,8,g) = 4.0*( s - xi(1,g)); - Nx(2,8,g) = -4.0*xi(1,g); - - Nx(0,9,g) = -4.0*xi(2,g); - Nx(1,9,g) = -4.0*xi(2,g); - Nx(2,9,g) = 4.0*( s - xi(2,g)); - } - }, - - {ElementType::TRI3, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - //std::cout << "[get_element_shape_data] TRI3 " << std::endl; - N(0,g) = xi(0,g); - N(1,g) = xi(1,g); - N(2,g) = 1.0 - xi(0,g) - xi(1,g); - - Nx(0,0,g) = 1.0; - Nx(1,0,g) = 0.0; - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 1.0; - Nx(0,2,g) = -1.0; - Nx(1,2,g) = -1.0; - } - }, - - {ElementType::TRI6, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double s = 1.0 - xi(0,g) - xi(1,g); - N(0,g) = xi(0,g) * (2.0*xi(0,g) - 1.0); - N(1,g) = xi(1,g) * (2.0*xi(1,g) - 1.0); - N(2,g) = s * (2.0*s - 1.0); - N(3,g) = 4.0*xi(0,g)*xi(1,g); - N(4,g) = 4.0*xi(1,g)*s; - N(5,g) = 4.0*xi(0,g)*s; - - Nx(0,0,g) = 4.0*xi(0,g) - 1.0; - Nx(1,0,g) = 0.0; - - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 4.0*xi(1,g) - 1.0; - - Nx(0,2,g) = 1.0 - 4.0*s; - Nx(1,2,g) = 1.0 - 4.0*s; - - Nx(0,3,g) = 4.0*xi(1,g); - Nx(1,3,g) = 4.0*xi(0,g); - - Nx(0,4,g) = -4.0*xi(1,g); - Nx(1,4,g) = 4.0*( s - xi(1,g) ); - - Nx(0,5,g) = 4.0*( s - xi(0,g) ); - Nx(1,5,g) = -4.0*xi(0,g); - } - }, - - {ElementType::WDG, [](const int insd, const int eNoN, const int g, Array& xi, Array& N, - Array3& Nx) -> void - { - double ux = xi(0,g); - double uy = xi(1,g); - double uz = 1.0 - ux - uy; - double s = (1.0 + xi(2,g))*0.5; - double t = (1.0 - xi(2,g))*0.5; - N(0,g) = ux*t; - N(1,g) = uy*t; - N(2,g) = uz*t; - N(3,g) = ux*s; - N(4,g) = uy*s; - N(5,g) = uz*s; - - Nx(0,0,g) = t; - Nx(1,0,g) = 0.0; - Nx(2,0,g) = -ux*0.50; - - Nx(0,1,g) = 0.0; - Nx(1,1,g) = t; - Nx(2,1,g) = -uy*0.50; - - Nx(0,2,g) = -t; - Nx(1,2,g) = -t; - Nx(2,2,g) = -uz*0.50; - - Nx(0,3,g) = s; - Nx(1,3,g) = 0.0; - Nx(2,3,g) = ux*0.50; - - Nx(0,4,g) = 0.0; - Nx(1,4,g) = s; - Nx(2,4,g) = uy*0.50; - - Nx(0,5,g) = -s; - Nx(1,5,g) = -s; - Nx(2,5,g) = uz*0.50; - } - }, - - - -}; - - -//------------------------ -// set_element_shape_data -//------------------------ -// Replicates 'SUBROUTINE GETGNN(insd, eType, eNoN, xi, N, Nxi)' defined in NN.f. -// -using SetElementShapeMapType = std::map>; - -SetElementShapeMapType set_element_shape_data = { - - {ElementType::HEX8, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - auto& N = mesh.N; - N(0,g) = lx*ly*lz/8.0; - N(1,g) = ux*ly*lz/8.0; - N(2,g) = ux*uy*lz/8.0; - N(3,g) = lx*uy*lz/8.0; - N(4,g) = lx*ly*uz/8.0; - N(5,g) = ux*ly*uz/8.0; - N(6,g) = ux*uy*uz/8.0; - N(7,g) = lx*uy*uz/8.0; - - auto& Nx = mesh.Nx; - Nx(0,0,g) = -ly*lz/8.0; - Nx(1,0,g) = -lx*lz/8.0; - Nx(2,0,g) = -lx*ly/8.0; - - Nx(0,1,g) = ly*lz/8.0; - Nx(1,1,g) = -ux*lz/8.0; - Nx(2,1,g) = -ux*ly/8.0; - - Nx(0,2,g) = uy*lz/8.0; - Nx(1,2,g) = ux*lz/8.0; - Nx(2,2,g) = -ux*uy/8.0; - - Nx(0,3,g) = -uy*lz/8.0; - Nx(1,3,g) = lx*lz/8.0; - Nx(2,3,g) = -lx*uy/8.0; - - Nx(0,4,g) = -ly*uz/8.0; - Nx(1,4,g) = -lx*uz/8.0; - Nx(2,4,g) = lx*ly/8.0; - - Nx(0,5,g) = ly*uz/8.0; - Nx(1,5,g) = -ux*uz/8.0; - Nx(2,5,g) = ux*ly/8.0; - - Nx(0,6,g) = uy*uz/8.0; - Nx(1,6,g) = ux*uz/8.0; - Nx(2,6,g) = ux*uy/8.0; - - Nx(0,7,g) = -uy*uz/8.0; - Nx(1,7,g) = lx*uz/8.0; - Nx(2,7,g) = lx*uy/8.0; - } - }, - - {ElementType::HEX20, [](int g, mshType& mesh) -> void { - - auto& xi = mesh.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - double mx = lx*ux; - double my = ly*uy; - double mz = lz*uz; - - auto& N = mesh.N; - N(0, g) = lx*ly*lz*(lx+ly+lz-5.0)/8.0; - N(1, g) = ux*ly*lz*(ux+ly+lz-5.0)/8.0; - N(2, g) = ux*uy*lz*(ux+uy+lz-5.0)/8.0; - N(3, g) = lx*uy*lz*(lx+uy+lz-5.0)/8.0; - N(4, g) = lx*ly*uz*(lx+ly+uz-5.0)/8.0; - N(5, g) = ux*ly*uz*(ux+ly+uz-5.0)/8.0; - N(6, g) = ux*uy*uz*(ux+uy+uz-5.0)/8.0; - N(7, g) = lx*uy*uz*(lx+uy+uz-5.0)/8.0; - N(8, g) = mx*ly*lz/4.0; - N(9, g) = ux*my*lz/4.0; - N(10, g) = mx*uy*lz/4.0; - N(11, g) = lx*my*lz/4.0; - N(12, g) = mx*ly*uz/4.0; - N(13, g) = ux*my*uz/4.0; - N(14, g) = mx*uy*uz/4.0; - N(15, g) = lx*my*uz/4.0; - N(16, g) = lx*ly*mz/4.0; - N(17, g) = ux*ly*mz/4.0; - N(18, g) = ux*uy*mz/4.0; - N(19, g) = lx*uy*mz/4.0; - - // N(1) = lx*ly*lz*(lx+ly+lz-5.0)/8.0; - auto& Nx = mesh.Nx; - int n = 0; - Nx(0,n,g) = -ly*lz*(lx+ly+lz-5.0+lx)/8.0; - Nx(1,n,g) = -lx*lz*(lx+ly+lz-5.0+ly)/8.0; - Nx(2,n,g) = -lx*ly*(lx+ly+lz-5.0+lz)/8.0; - -//c N(n,g) = ux*ly*lz*(ux+ly+lz-5.0)/8.0; - n += 1; - Nx(0,n,g) = ly*lz*(ux+ly+lz-5.0+ux)/8.0; - Nx(1,n,g) = -ux*lz*(ux+ly+lz-5.0+ly)/8.0; - Nx(2,n,g) = -ux*ly*(ux+ly+lz-5.0+lz)/8.0; - -//c N(n,g) = ux*uy*lz*(ux+uy+lz-5.0)/8.0 - n += 1; - Nx(0,n,g) = uy*lz*(ux+uy+lz-5.0+ux)/8.0; - Nx(1,n,g) = ux*lz*(ux+uy+lz-5.0+uy)/8.0; - Nx(2,n,g) = -ux*uy*(ux+uy+lz-5.0+lz)/8.0; - -//c N(n,g) = lx*uy*lz*(lx+uy+lz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -uy*lz*(lx+uy+lz-5.0+lx)/8.0; - Nx(1,n,g) = lx*lz*(lx+uy+lz-5.0+uy)/8.0; - Nx(2,n,g) = -lx*uy*(lx+uy+lz-5.0+lz)/8.0; - -//c N(n,g) = lx*ly*uz*(lx+ly+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -ly*uz*(lx+ly+uz-5.0+lx)/8.0; - Nx(1,n,g) = -lx*uz*(lx+ly+uz-5.0+ly)/8.0; - Nx(2,n,g) = lx*ly*(lx+ly+uz-5.0+uz)/8.0; - -//c N(n,g) = ux*ly*uz*(ux+ly+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = ly*uz*(ux+ly+uz-5.0+ux)/8.0; - Nx(1,n,g) = -ux*uz*(ux+ly+uz-5.0+ly)/8.0; - Nx(2,n,g) = ux*ly*(ux+ly+uz-5.0+uz)/8.0; - -//c N(n,g) = ux*uy*uz*(ux+uy+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = uy*uz*(ux+uy+uz-5.0+ux)/8.0; - Nx(1,n,g) = ux*uz*(ux+uy+uz-5.0+uy)/8.0; - Nx(2,n,g) = ux*uy*(ux+uy+uz-5.0+uz)/8.0; - -//c N(n,g) = lx*uy*uz*(lx+uy+uz-5.0)/8.0 - n += 1; - Nx(0,n,g) = -uy*uz*(lx+uy+uz-5.0+lx)/8.0; - Nx(1,n,g) = lx*uz*(lx+uy+uz-5.0+uy)/8.0; - Nx(2,n,g) = lx*uy*(lx+uy+uz-5.0+uz)/8.0; - -//c N(n,g) = mx*ly*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*ly*lz/4.0; - Nx(1,n,g) = -mx*lz/4.0; - Nx(2,n,g) = -mx*ly/4.0; - -//c N(0n,g) = ux*my*lz/4.0 - n += 1; - Nx(0,n,g) = my*lz/4.0; - Nx(1,n,g) = (ly - uy)*ux*lz/4.0; - Nx(2,n,g) = -ux*my/4.0; - -//c N(0n,g) = mx*uy*lz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*uy*lz/4.0; - Nx(1,n,g) = mx*lz/4.0; - Nx(2,n,g) = -mx*uy/4.0; - -//c N(0n,g) = lx*my*lz/4.0 - n += 1; - Nx(0,n,g) = -my*lz/4.0; - Nx(1,n,g) = (ly - uy)*lx*lz/4.0; - Nx(2,n,g) = -lx*my/4.0; - -//c N(0n,g) = mx*ly*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*ly*uz/4.0; - Nx(1,n,g) = -mx*uz/4.0; - Nx(2,n,g) = mx*ly/4.0; - -//c N(0n,g) = ux*my*uz/4.0 - n += 1; - Nx(0,n,g) = my*uz/4.0; - Nx(1,n,g) = (ly - uy)*ux*uz/4.0; - Nx(2,n,g) = ux*my/4.0; - -//c N(0n,g) = mx*uy*uz/4.0 - n += 1; - Nx(0,n,g) = (lx - ux)*uy*uz/4.0; - Nx(1,n,g) = mx*uz/4.0; - Nx(2,n,g) = mx*uy/4.0; - -//c N(0n,g) = lx*my*uz/4.0 - n += 1; - Nx(0,n,g) = -my*uz/4.0; - Nx(1,n,g) = (ly - uy)*lx*uz/4.0; - Nx(2,n,g) = lx*my/4.0; - -//c N(0n,g) = lx*ly*mz/4.0 - n += 1; - Nx(0,n,g) = -ly*mz/4.0; - Nx(1,n,g) = -lx*mz/4.0; - Nx(2,n,g) = (lz - uz)*lx*ly/4.0; - -//c N(0n,g) = ux*ly*mz/4.0 - n += 1; - Nx(0,n,g) = ly*mz/4.0; - Nx(1,n,g) = -ux*mz/4.0; - Nx(2,n,g) = (lz - uz)*ux*ly/4.0; - -//c N(0n,g) = ux*uy*mz/4.0 - n += 1; - Nx(0,n,g) = uy*mz/4.0; - Nx(1,n,g) = ux*mz/4.0; - Nx(2,n,g) = (lz - uz)*ux*uy/4.0; - -//c N(n,g) = lx*uy*mz/4.0 - n += 1; - Nx(0,n,g) = -uy*mz/4.0; - Nx(1,n,g) = lx*mz/4.0; - Nx(2,n,g) = (lz - uz)*lx*uy/4.0; - } - }, - - {ElementType::HEX27, [](int g, mshType& mesh) -> void { - - auto& xi = mesh.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double lz = 1.0 - xi(2,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double uz = 1.0 + xi(2,g); - - double mx = xi(0,g); - double my = xi(1,g); - double mz = xi(2,g); - - auto& N = mesh.N; - N(0,g) = -mx*lx*my*ly*mz*lz/8.0; - N(1,g) = mx*ux*my*ly*mz*lz/8.0; - N(2,g) = -mx*ux*my*uy*mz*lz/8.0; - N(3,g) = mx*lx*my*uy*mz*lz/8.0; - N(4,g) = mx*lx*my*ly*mz*uz/8.0; - N(5,g) = -mx*ux*my*ly*mz*uz/8.0; - N(6,g) = mx*ux*my*uy*mz*uz/8.0; - N(7,g) = -mx*lx*my*uy*mz*uz/8.0; - N(8,g) = lx*ux*my*ly*mz*lz/4.0; - N(9,g) = -mx*ux*ly*uy*mz*lz/4.0; - N(10,g) = -lx*ux*my*uy*mz*lz/4.0; - N(11,g) = mx*lx*ly*uy*mz*lz/4.0; - N(12,g) = -lx*ux*my*ly*mz*uz/4.0; - N(13,g) = mx*ux*ly*uy*mz*uz/4.0; - N(14,g) = lx*ux*my*uy*mz*uz/4.0; - N(15,g) = -mx*lx*ly*uy*mz*uz/4.0; - N(16,g) = mx*lx*my*ly*lz*uz/4.0; - N(17,g) = -mx*ux*my*ly*lz*uz/4.0; - N(18,g) = mx*ux*my*uy*lz*uz/4.0; - N(19,g) = -mx*lx*my*uy*lz*uz/4.0; - - N(20,g) = -mx*lx*ly*uy*lz*uz/2.0; - N(21,g) = mx*ux*ly*uy*lz*uz/2.0; - N(22,g) = -lx*ux*my*ly*lz*uz/2.0; - N(23,g) = lx*ux*my*uy*lz*uz/2.0; - N(24,g) = -lx*ux*ly*uy*mz*lz/2.0; - N(25,g) = lx*ux*ly*uy*mz*uz/2.0; - - N(26,g) = lx*ux*ly*uy*lz*uz; - - auto& Nxi = mesh.Nx; - int n = 0; - Nxi(0,n,g) = -(lx - mx)*my*ly*mz*lz/8.0; - Nxi(1,n,g) = -(ly - my)*mx*lx*mz*lz/8.0; - Nxi(2,n,g) = -(lz - mz)*mx*lx*my*ly/8.0; - - n += 1; - Nxi(0,n,g) = (mx + ux)*my*ly*mz*lz/8.0; - Nxi(1,n,g) = (ly - my)*mx*ux*mz*lz/8.0; - Nxi(2,n,g) = (lz - mz)*mx*ux*my*ly/8.0; - - n += 1; - Nxi(0,n,g) = -(mx + ux)*my*uy*mz*lz/8.0; - Nxi(1,n,g) = -(my + uy)*mx*ux*mz*lz/8.0; - Nxi(2,n,g) = -(lz - mz)*mx*ux*my*uy/8.0; - - n += 1; - Nxi(0,n,g) = (lx - mx)*my*uy*mz*lz/8.0; - Nxi(1,n,g) = (my + uy)*mx*lx*mz*lz/8.0; - Nxi(2,n,g) = (lz - mz)*mx*lx*my*uy/8.0; - - n += 1; - Nxi(0,n,g) = (lx - mx)*my*ly*mz*uz/8.0; - Nxi(1,n,g) = (ly - my)*mx*lx*mz*uz/8.0; - Nxi(2,n,g) = (mz + uz)*mx*lx*my*ly/8.0; - - n += 1; - Nxi(0,n,g) = -(mx + ux)*my*ly*mz*uz/8.0; - Nxi(1,n,g) = -(ly - my)*mx*ux*mz*uz/8.0; - Nxi(2,n,g) = -(mz + uz)*mx*ux*my*ly/8.0; - - n += 1; - Nxi(0,n,g) = (mx + ux)*my*uy*mz*uz/8.0; - Nxi(1,n,g) = (my + uy)*mx*ux*mz*uz/8.0; - Nxi(2,n,g) = (mz + uz)*mx*ux*my*uy/8.0; - - n += 1; - Nxi(0,n,g) = -(lx - mx)*my*uy*mz*uz/8.0; - Nxi(1,n,g) = -(my + uy)*mx*lx*mz*uz/8.0; - Nxi(2,n,g) = -(mz + uz)*mx*lx*my*uy/8.0; - - n += 1; - Nxi(0,n,g) = (lx - ux)*my*ly*mz*lz/4.0; - Nxi(1,n,g) = (ly - my)*lx*ux*mz*lz/4.0; - Nxi(2,n,g) = (lz - mz)*lx*ux*my*ly/4.0; - - n += 1; - Nxi(0,n,g) = -(mx + ux)*ly*uy*mz*lz/4.0; - Nxi(1,n,g) = -(ly - uy)*mx*ux*mz*lz/4.0; - Nxi(2,n,g) = -(lz - mz)*mx*ux*ly*uy/4.0; - - n += 1; - Nxi(0,n,g) = -(lx - ux)*my*uy*mz*lz/4.0; - Nxi(1,n,g) = -(my + uy)*lx*ux*mz*lz/4.0; - Nxi(2,n,g) = -(lz - mz)*lx*ux*my*uy/4.0; - - n += 1; - Nxi(0,n,g) = (lx - mx)*ly*uy*mz*lz/4.0; - Nxi(1,n,g) = (ly - uy)*mx*lx*mz*lz/4.0; - Nxi(2,n,g) = (lz - mz)*mx*lx*ly*uy/4.0; - - n += 1; - Nxi(0,n,g) = -(lx - ux)*my*ly*mz*uz/4.0; - Nxi(1,n,g) = -(ly - my)*lx*ux*mz*uz/4.0; - Nxi(2,n,g) = -(mz + uz)*lx*ux*my*ly/4.0; - - n += 1; - Nxi(0,n,g) = (mx + ux)*ly*uy*mz*uz/4.0; - Nxi(1,n,g) = (ly - uy)*mx*ux*mz*uz/4.0; - Nxi(2,n,g) = (mz + uz)*mx*ux*ly*uy/4.0; - - n += 1; - Nxi(0,n,g) = (lx - ux)*my*uy*mz*uz/4.0; - Nxi(1,n,g) = (my + uy)*lx*ux*mz*uz/4.0; - Nxi(2,n,g) = (mz + uz)*lx*ux*my*uy/4.0; - - n += 1; - Nxi(0,n,g) = -(lx - mx)*ly*uy*mz*uz/4.0; - Nxi(1,n,g) = -(ly - uy)*mx*lx*mz*uz/4.0; - Nxi(2,n,g) = -(mz + uz)*mx*lx*ly*uy/4.0; - - n += 1; - Nxi(0,n,g) = (lx - mx)*my*ly*lz*uz/4.0; - Nxi(1,n,g) = (ly - my)*mx*lx*lz*uz/4.0; - Nxi(2,n,g) = (lz - uz)*mx*lx*my*ly/4.0; - - n += 1; - Nxi(0,n,g) = -(mx + ux)*my*ly*lz*uz/4.0; - Nxi(1,n,g) = -(ly - my)*mx*ux*lz*uz/4.0; - Nxi(2,n,g) = -(lz - uz)*mx*ux*my*ly/4.0; - - n += 1; - Nxi(0,n,g) = (mx + ux)*my*uy*lz*uz/4.0; - Nxi(1,n,g) = (my + uy)*mx*ux*lz*uz/4.0; - Nxi(2,n,g) = (lz - uz)*mx*ux*my*uy/4.0; - - n += 1; - Nxi(0,n,g) = -(lx - mx)*my*uy*lz*uz/4.0; - Nxi(1,n,g) = -(my + uy)*mx*lx*lz*uz/4.0; - Nxi(2,n,g) = -(lz - uz)*mx*lx*my*uy/4.0; - - n += 1; - Nxi(0,n,g) = -(lx - mx)*ly*uy*lz*uz/2.0; - Nxi(1,n,g) = -(ly - uy)*mx*lx*lz*uz/2.0; - Nxi(2,n,g) = -(lz - uz)*mx*lx*ly*uy/2.0; - - n += 1; - Nxi(0,n,g) = (mx + ux)*ly*uy*lz*uz/2.0; - Nxi(1,n,g) = (ly - uy)*mx*ux*lz*uz/2.0; - Nxi(2,n,g) = (lz - uz)*mx*ux*ly*uy/2.0; - - n += 1; - Nxi(0,n,g) = -(lx - ux)*my*ly*lz*uz/2.0; - Nxi(1,n,g) = -(ly - my)*lx*ux*lz*uz/2.0; - Nxi(2,n,g) = -(lz - uz)*lx*ux*my*ly/2.0; - - n += 1; - Nxi(0,n,g) = (lx - ux)*my*uy*lz*uz/2.0; - Nxi(1,n,g) = (my + uy)*lx*ux*lz*uz/2.0; - Nxi(2,n,g) = (lz - uz)*lx*ux*my*uy/2.0; - - n += 1; - Nxi(0,n,g) = -(lx - ux)*ly*uy*mz*lz/2.0; - Nxi(1,n,g) = -(ly - uy)*lx*ux*mz*lz/2.0; - Nxi(2,n,g) = -(lz - mz)*lx*ux*ly*uy/2.0; - - n += 1; - Nxi(0,n,g) = (lx - ux)*ly*uy*mz*uz/2.0; - Nxi(1,n,g) = (ly - uy)*lx*ux*mz*uz/2.0; - Nxi(2,n,g) = (mz + uz)*lx*ux*ly*uy/2.0; - - n += 1; - Nxi(0,n,g) = (lx - ux)*ly*uy*lz*uz; - Nxi(1,n,g) = (ly - uy)*lx*ux*lz*uz; - Nxi(2,n,g) = (lz - uz)*lx*ux*ly*uy; - } - }, - - {ElementType::LIN1, [](int g, mshType& mesh) -> void { - //std::cout << "[set_element_shape_data] **************************" << std::endl; - //std::cout << "[set_element_shape_data] ERROR: LIN1 not supported." << std::endl; - //std::cout << "[set_element_shape_data] **************************" << std::endl; - auto& xi = mesh.xi; - auto& N = mesh.N; - N(0,g) = (1.0 - xi(0,g))*0.5; - N(1,g) = (1.0 + xi(0,g))*0.5; - - auto& Nx = mesh.Nx; - Nx(0,0,g) = -0.5; - Nx(0,1,g) = 0.5; - } - }, - - {ElementType::LIN2, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - auto& N = mesh.N; - N(0,g) = -xi(0,g)*(1.0 - xi(0,g))*0.50; - N(1,g) = xi(0,g)*(1.0 + xi(0,g))*0.50; - N(2,g) = (1.0 - xi(0,g))*(1.0 + xi(0,g)); - - auto& Nx = mesh.Nx; - Nx(0,0,g) = -0.50 + xi(0,g); - Nx(0,1,g) = 0.50 + xi(0,g); - Nx(0,2,g) = -2.0*xi(0,g); - } - }, - - {ElementType::QUD4, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - - auto& N = mesh.N; - N(0,g) = lx*ly / 4.0; - N(1,g) = ux*ly / 4.0; - N(2,g) = ux*uy / 4.0; - N(3,g) = lx*uy / 4.0; - - auto& Nx = mesh.Nx; - Nx(0,0,g) = -ly / 4.0; - Nx(1,0,g) = -lx / 4.0; - Nx(0,1,g) = ly / 4.0; - Nx(1,1,g) = -ux / 4.0; - Nx(0,2,g) = uy / 4.0; - Nx(1,2,g) = ux / 4.0; - Nx(0,3,g) = -uy / 4.0; - Nx(1,3,g) = lx / 4.0; - } - }, - - {ElementType::QUD9, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double mx = xi(0,g); - double my = xi(1,g); - - auto& N = mesh.N; - N(0,g) = mx*lx*my*ly/4.0; - N(1,g) = -mx*ux*my*ly/4.0; - N(2,g) = mx*ux*my*uy/4.0; - N(3,g) = -mx*lx*my*uy/4.0; - N(4,g) = -lx*ux*my*ly*0.50; - N(5,g) = mx*ux*ly*uy*0.50; - N(6,g) = lx*ux*my*uy*0.50; - N(7,g) = -mx*lx*ly*uy*0.50; - N(8,g) = lx*ux*ly*uy; - - auto& Nx = mesh.Nx; - Nx(0,0,g) = (lx - mx)*my*ly/4.0; - Nx(1,0,g) = (ly - my)*mx*lx/4.0; - - Nx(0,1,g) = -(ux + mx)*my*ly/4.0; - Nx(1,1,g) = -(ly - my)*mx*ux/4.0; - - Nx(0,2,g) = (ux + mx)*my*uy/4.0; - Nx(1,2,g) = (uy + my)*mx*ux/4.0; - - Nx(0,3,g) = -(lx - mx)*my*uy/4.0; - Nx(1,3,g) = -(uy + my)*mx*lx/4.0; - - Nx(0,4,g) = -(lx - ux)*my*ly*0.50; - Nx(1,4,g) = -(ly - my)*lx*ux*0.50; - - Nx(0,5,g) = (ux + mx)*ly*uy*0.50; - Nx(1,5,g) = (ly - uy)*mx*ux*0.50; - - Nx(0,6,g) = (lx - ux)*my*uy*0.50; - Nx(1,6,g) = (uy + my)*lx*ux*0.50; - - Nx(0,7,g) = -(lx - mx)*ly*uy*0.50; - Nx(1,7,g) = -(ly - uy)*mx*lx*0.50; - - Nx(0,8,g) = (lx - ux)*ly*uy; - Nx(1,8,g) = (ly - uy)*lx*ux; - } - }, - - {ElementType::TET4, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - auto& N = mesh.N; - N(0,g) = xi(0,g); - N(1,g) = xi(1,g); - N(2,g) = xi(2,g); - N(3,g) = 1.0 - xi(0,g) - xi(1,g) - xi(2,g); - - auto& Nx = mesh.Nx; - Nx(0,0,g) = 1.0; - Nx(1,0,g) = 0.0; - Nx(2,0,g) = 0.0; - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 1.0; - Nx(2,1,g) = 0.0; - Nx(0,2,g) = 0.0; - Nx(1,2,g) = 0.0; - Nx(2,2,g) = 1.0; - Nx(0,3,g) = -1.0; - Nx(1,3,g) = -1.0; - Nx(2,3,g) = -1.0; - } - }, - - {ElementType::TET10, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - auto& N = mesh.N; - double s = 1.0 - xi(0,g) - xi(1,g) - xi(2,g); - N(0,g) = xi(0,g)*(2.0*xi(0,g) - 1.0); - N(1,g) = xi(1,g)*(2.0*xi(1,g) - 1.0); - N(2,g) = xi(2,g)*(2.0*xi(2,g) - 1.0); - N(3,g) = s *(2.0*s - 1.0); - N(4,g) = 4.0*xi(0,g)*xi(1,g); - N(5,g) = 4.0*xi(1,g)*xi(2,g); - N(6,g) = 4.0*xi(0,g)*xi(2,g); - N(7,g) = 4.0*xi(0,g)*s; - N(8,g) = 4.0*xi(1,g)*s; - N(9,g) = 4.0*xi(2,g)*s; - - auto& Nx = mesh.Nx; - Nx(0,0,g) = 4.0*xi(0,g) - 1.0; - Nx(1,0,g) = 0.0; - Nx(2,0,g) = 0.0; - - Nx(0,1,g) = 0.0; - Nx(1,1,g) = 4.0*xi(1,g) - 1.0; - Nx(2,1,g) = 0.0; - - Nx(0,2,g) = 0.0; - Nx(1,2,g) = 0.0; - Nx(2,2,g) = 4.0*xi(2,g) - 1.0; - - Nx(0,3,g) = 1.0 - 4.0*s; - Nx(1,3,g) = 1.0 - 4.0*s; - Nx(2,3,g) = 1.0 - 4.0*s; - - Nx(0,4,g) = 4.0*xi(1,g); - Nx(1,4,g) = 4.0*xi(0,g); - Nx(2,4,g) = 0.0; - - Nx(0,5,g) = 0.0; - Nx(1,5,g) = 4.0*xi(2,g); - Nx(2,5,g) = 4.0*xi(1,g); - - Nx(0,6,g) = 4.0*xi(2,g); - Nx(1,6,g) = 0.0; - Nx(2,6,g) = 4.0*xi(0,g); - - Nx(0,7,g) = 4.0*( s - xi(0,g)); - Nx(1,7,g) = -4.0*xi(0,g); - Nx(2,7,g) = -4.0*xi(0,g); - - Nx(0,8,g) = -4.0*xi(1,g); - Nx(1,8,g) = 4.0*( s - xi(1,g)); - Nx(2,8,g) = -4.0*xi(1,g); - - Nx(0,9,g) = -4.0*xi(2,g); - Nx(1,9,g) = -4.0*xi(2,g); - Nx(2,9,g) = 4.0*( s - xi(2,g)); - } - }, - - {ElementType::TRI3, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - auto& N = mesh.N; - N(0,g) = xi(0,g); - N(1,g) = xi(1,g); - N(2,g) = 1.0 - xi(0,g) - xi(1,g); - - auto& Nxi = mesh.Nx; - Nxi(0,0,g) = 1.0; - Nxi(1,0,g) = 0.0; - Nxi(0,1,g) = 0.0; - Nxi(1,1,g) = 1.0; - Nxi(0,2,g) = -1.0; - Nxi(1,2,g) = -1.0; - } - }, - - {ElementType::TRI6, [](int g, mshType& mesh) -> void { - auto& xi = mesh.xi; - auto& N = mesh.N; - - double s = 1.0 - xi(0,g) - xi(1,g); - N(0,g) = xi(0,g)*( 2.0*xi(0,g) - 1.0 ); - N(1,g) = xi(1,g)*( 2.0*xi(1,g) - 1.0 ); - N(2,g) = s *( 2.0*s - 1.0 ); - N(3,g) = 4.0*xi(0,g)*xi(1,g); - N(4,g) = 4.0*xi(1,g)*s; - N(5,g) = 4.0*xi(0,g)*s; - - auto& Nxi = mesh.Nx; - Nxi(0,0,g) = 4.0*xi(0,g) - 1.0; - Nxi(1,0,g) = 0.0; - Nxi(0,1,g) = 0.0; - Nxi(1,1,g) = 4.0*xi(1,g) - 1.0; - Nxi(0,2,g) = 1.0 - 4.0*s; - Nxi(1,2,g) = 1.0 - 4.0*s; - Nxi(0,3,g) = 4.0*xi(1,g); - Nxi(1,3,g) = 4.0*xi(0,g); - Nxi(0,4,g) = -4.0*xi(1,g); - Nxi(1,4,g) = 4.0*( s - xi(1,g) ); - Nxi(0,5,g) = 4.0*( s - xi(0,g) ); - Nxi(1,5,g) = -4.0*xi(0,g); - } - }, - - {ElementType::WDG, [](int g, mshType& mesh) -> void - { - auto& xi = mesh.xi; - auto& N = mesh.N; - double ux = xi(0,g); - double uy = xi(1,g); - double uz = 1.0 - ux - uy; - double s = (1.0 + xi(2,g))*0.5; - double t = (1.0 - xi(2,g))*0.5; - N(0,g) = ux*t; - N(1,g) = uy*t; - N(2,g) = uz*t; - N(3,g) = ux*s; - N(4,g) = uy*s; - N(5,g) = uz*s; - - auto& Nxi = mesh.Nx; - Nxi(0,0,g) = t; - Nxi(1,0,g) = 0.0; - Nxi(2,0,g) = -ux*0.50; - - Nxi(0,1,g) = 0.0; - Nxi(1,1,g) = t; - Nxi(2,1,g) = -uy*0.50; - - Nxi(0,2,g) = -t; - Nxi(1,2,g) = -t; - Nxi(2,2,g) = -uz*0.50; - - Nxi(0,3,g) = s; - Nxi(1,3,g) = 0.0; - Nxi(2,3,g) = ux*0.50; - - Nxi(0,4,g) = 0.0; - Nxi(1,4,g) = s; - Nxi(2,4,g) = uy*0.50; - - Nxi(0,5,g) = -s; - Nxi(1,5,g) = -s; - Nxi(2,5,g) = uz*0.50; - } - }, - -}; - -//--------------------- -// set_face_shape_data -//--------------------- -// Define a map type used to face element shape function data. -// -// This reproduces 'SUBROUTINE GETGNN(insd, eType, eNoN, xi, N, Nxi)' in NN.f. -// -using SetFaceShapeMapType = std::map>; - -SetFaceShapeMapType set_face_shape_data = { - - {ElementType::PNT, [](int g, faceType& face) -> void - { - face.N(0,g) = 1.0; - } - }, - - {ElementType::QUD8, [](int g, faceType& face) -> void - { - auto& xi = face.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double mx = lx*ux; - double my = ly*uy; - - auto& N = face.N; - N(0,g) = lx*ly*(lx+ly-3.0)/4.0; - N(1,g) = ux*ly*(ux+ly-3.0)/4.0; - N(2,g) = ux*uy*(ux+uy-3.0)/4.0; - N(3,g) = lx*uy*(lx+uy-3.0)/4.0; - N(4,g) = mx*ly*0.50; - N(5,g) = ux*my*0.50; - N(6,g) = mx*uy*0.50; - N(7,g) = lx*my*0.50; - - auto& Nxi = face.Nx; - Nxi(0,0,g) = -ly*(lx+ly-3.0+lx)/4.0; - Nxi(1,0,g) = -lx*(lx+ly-3.0+ly)/4.0; - - Nxi(0,1,g) = ly*(ux+ly-3.0+ux)/4.0; - Nxi(1,1,g) = -ux*(ux+ly-3.0+ly)/4.0; - - Nxi(0,2,g) = uy*(ux+uy-3.0+ux)/4.0; - Nxi(1,2,g) = ux*(ux+uy-3.0+uy)/4.0; - - Nxi(0,3,g) = -uy*(lx+uy-3.0+lx)/4.0; - Nxi(1,3,g) = lx*(lx+uy-3.0+uy)/4.0; - - Nxi(0,4,g) = (lx - ux)*ly*0.50; - Nxi(1,4,g) = -mx*0.50; - - Nxi(0,5,g) = my*0.50; - Nxi(1,5,g) = (ly - uy)*ux*0.50; - - Nxi(0,6,g) = (lx - ux)*uy*0.50; - Nxi(1,6,g) = mx*0.50; - - Nxi(0,7,g) = -my*0.50; - Nxi(1,7,g) = (ly - uy)*lx*0.50; - } - }, - - {ElementType::QUD9, [](int g, faceType& face) -> void - { - auto& xi = face.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double mx = xi(0,g); - double my = xi(1,g); - - auto& N = face.N; - N(0,g) = mx*lx*my*ly/4.0; - N(1,g) = -mx*ux*my*ly/4.0; - N(2,g) = mx*ux*my*uy/4.0; - N(3,g) = -mx*lx*my*uy/4.0; - N(4,g) = -lx*ux*my*ly*0.50; - N(5,g) = mx*ux*ly*uy*0.50; - N(6,g) = lx*ux*my*uy*0.50; - N(7,g) = -mx*lx*ly*uy*0.50; - N(8,g) = lx*ux*ly*uy; - - auto& Nx = face.Nx; - Nx(0,0,g) = (lx - mx)*my*ly/4.0; - Nx(1,0,g) = (ly - my)*mx*lx/4.0; - Nx(0,1,g) = -(ux + mx)*my*ly/4.0; - Nx(1,1,g) = -(ly - my)*mx*ux/4.0; - Nx(0,2,g) = (ux + mx)*my*uy/4.0; - Nx(1,2,g) = (uy + my)*mx*ux/4.0; - Nx(0,3,g) = -(lx - mx)*my*uy/4.0; - Nx(1,3,g) = -(uy + my)*mx*lx/4.0; - Nx(0,4,g) = -(lx - ux)*my*ly*0.50; - Nx(1,4,g) = -(ly - my)*lx*ux*0.50; - Nx(0,5,g) = (ux + mx)*ly*uy*0.50; - Nx(1,5,g) = (ly - uy)*mx*ux*0.50; - Nx(0,6,g) = (lx - ux)*my*uy*0.50; - Nx(1,6,g) = (uy + my)*lx*ux*0.50; - Nx(0,7,g) = -(lx - mx)*ly*uy*0.50; - Nx(1,7,g) = -(ly - uy)*mx*lx*0.50; - Nx(0,8,g) = (lx - ux)*ly*uy; - Nx(1,8,g) = (ly - uy)*lx*ux; - } - }, - - {ElementType::LIN1, [](int g, faceType& face) -> void - { - face.N(0,g) = 0.5 * (1.0 - face.xi(0,g)); - face.N(1,g) = 0.5 * (1.0 + face.xi(0,g)); - - face.Nx(0,0,g) = -0.5; - face.Nx(0,1,g) = 0.5; - } - }, - - {ElementType::LIN2, [](int g, faceType& face) -> void - { - auto& xi = face.xi; - auto& N = face.N; - N(0,g) = -xi(0,g)*(1.0 - xi(0,g))*0.50; - N(1,g) = xi(0,g)*(1.0 + xi(0,g))*0.50; - N(2,g) = (1.0 - xi(0,g))*(1.0 + xi(0,g)); - - auto& Nx = face.Nx; - Nx(0,0,g) = -0.50 + xi(0,g); - Nx(0,1,g) = 0.50 + xi(0,g); - Nx(0,2,g) = -2.0*xi(0,g); - } - }, - - {ElementType::QUD4, [](int g, faceType& face) -> void { - auto& xi = face.xi; - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - - auto& N =face.N; - N(0,g) = lx*ly / 4.0; - N(1,g) = ux*ly / 4.0; - N(2,g) = ux*uy / 4.0; - N(3,g) = lx*uy / 4.0; - - auto& Nx = face.Nx; - Nx(0,0,g) = -ly / 4.0; - Nx(1,0,g) = -lx / 4.0; - Nx(0,1,g) = ly / 4.0; - Nx(1,1,g) = -ux / 4.0; - Nx(0,2,g) = uy / 4.0; - Nx(1,2,g) = ux / 4.0; - Nx(0,3,g) = -uy / 4.0; - Nx(1,3,g) = lx / 4.0; - } - }, - - {ElementType::TRI3, [](int g, faceType& face) -> void - { - face.N(0,g) = face.xi(0,g); - face.N(1,g) = face.xi(1,g); - face.N(2,g) = 1.0 - face.xi(0,g) - face.xi(1,g); - - face.Nx(0,0,g) = 1.0; - face.Nx(1,0,g) = 0.0; - - face.Nx(0,1,g) = 0.0; - face.Nx(1,1,g) = 1.0; - - face.Nx(0,2,g) = -1.0; - face.Nx(1,2,g) = -1.0; - } - }, - - {ElementType::TRI6, [](int g, faceType& face) -> void - { - auto& xi = face.xi; - auto& N = face.N; - - double s = 1.0 - xi(0,g) - xi(1,g); - N(0,g) = xi(0,g)*( 2.0*xi(0,g) - 1.0 ); - N(1,g) = xi(1,g)*( 2.0*xi(1,g) - 1.0 ); - N(2,g) = s *( 2.0*s - 1.0 ); - N(3,g) = 4.0*xi(0,g)*xi(1,g); - N(4,g) = 4.0*xi(1,g)*s; - N(5,g) = 4.0*xi(0,g)*s; - - auto& Nxi = face.Nx; - Nxi(0,0,g) = 4.0*xi(0,g) - 1.0; - Nxi(1,0,g) = 0.0; - - Nxi(0,1,g) = 0.0; - Nxi(1,1,g) = 4.0*xi(1,g) - 1.0; - - Nxi(0,2,g) = 1.0 - 4.0*s; - Nxi(1,2,g) = 1.0 - 4.0*s; - - Nxi(0,3,g) = 4.0*xi(1,g); - Nxi(1,3,g) = 4.0*xi(0,g); - - Nxi(0,4,g) = -4.0*xi(1,g); - Nxi(1,4,g) = 4.0*( s - xi(1,g) ); - - Nxi(0,5,g) = 4.0*( s - xi(0,g) ); - Nxi(1,5,g) = -4.0*xi(0,g); - } - }, - - -}; diff --git a/Code/Source/solver/nn_elem_gnnxx.h b/Code/Source/solver/nn_elem_gnnxx.h deleted file mode 100644 index 7b40a783b..000000000 --- a/Code/Source/solver/nn_elem_gnnxx.h +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. -// SPDX-License-Identifier: BSD-3-Clause - -/// @brief Define a map type used to compute 2nd direivatives of element shape function data. -/// -/// Replicates 'SUBROUTINE GETGNNxx(insd, ind2, eType, eNoN, xi, Nxx)' -// -static double fp = 4.0; -static double fn = -4.0; -static double en = -8.0; -static double ze = 0.0; - -using GetElement2ndDerivMapType = std::map&, Array3&)>>; - -GetElement2ndDerivMapType get_element_2nd_derivs = { - - {ElementType::QUD8, [](const int insd, const int ind2, const int eNoN, const int g, const Array& xi, - Array3& Nxx) -> void { - - double lx = 1.0 - xi(0); - double ly = 1.0 - xi(1); - double ux = 1.0 + xi(0); - double uy = 1.0 + xi(1); - double mx = xi(0); - double my = xi(1); - - Nxx(0,0,g) = ly*0.50; - Nxx(1,0,g) = lx*0.50; - Nxx(2,0,g) = (lx+lx+ly+ly-3.0)/4.0; - - Nxx(0,1,g) = ly*0.50; - Nxx(1,1,g) = ux*0.50; - Nxx(2,1,g) = -(ux+ux+ly+ly-3.0)/4.0; - - Nxx(0,2,g) = uy*0.50; - Nxx(1,2,g) = ux*0.50; - Nxx(2,3,g) = (ux+ux+uy+uy-3.0)/4.0; - - Nxx(0,3,g) = uy*0.50; - Nxx(1,3,g) = lx*0.50; - Nxx(2,3,g) = -(lx+lx+uy+uy-3.0)/4.0; - - Nxx(0,4,g) = -ly; - Nxx(1,4,g) = 0.0; - Nxx(2,4,g) = mx; - - Nxx(0,5,g) = 0.0; - Nxx(1,5,g) = -ux; - Nxx(2,5,g) = -my; - - Nxx(0,6,g) = -uy; - Nxx(1,6,g) = 0.0; - Nxx(2,6,g) = -mx; - - Nxx(0,7,g) = 0.0; - Nxx(1,7,g) = -lx; - Nxx(2,7,g) = my; - } - }, - - {ElementType::QUD9, [](const int insd, const int ind2, const int eNoN, const int g, const Array& xi, - Array3& Nxx) -> void { - - double lx = 1.0 - xi(0,g); - double ly = 1.0 - xi(1,g); - double ux = 1.0 + xi(0,g); - double uy = 1.0 + xi(1,g); - double mx = xi(0,g); - double my = xi(1,g); - - Nxx(0,0,g) = -ly*my*0.5; - Nxx(1,0,g) = -lx*mx*0.5; - Nxx(2,0,g) = (lx-mx)*(ly-my)/4.0; - - Nxx(0,1,g) = -ly*my*0.5; - Nxx(1,1,g) = ux*mx*0.5; - Nxx(2,1,g) = -(ux+mx)*(ly-my)/4.0; - - Nxx(0,2,g) = uy*my*0.5; - Nxx(1,2,g) = ux*mx*0.5; - Nxx(2,2,g) = (ux+mx)*(uy+my)/4.0; - - Nxx(0,3,g) = uy*my*0.5; - Nxx(1,3,g) = -lx*mx*0.5; - Nxx(2,3,g) = -(lx-mx)*(uy+my)/4.0; - - Nxx(0,4,g) = ly*my; - Nxx(1,4,g) = lx*ux; - Nxx(2,4,g) = mx*(ly-my); - - Nxx(0,5,g) = ly*uy; - Nxx(1,5,g) = -ux*mx; - Nxx(2,5,g) = -(ux+mx)*my; - - Nxx(0,6,g) = -uy*my; - Nxx(1,6,g) = lx*ux; - Nxx(2,6,g) = -mx*(uy+my); - - Nxx(0,7,g) = ly*uy; - Nxx(1,7,g) = lx*mx; - Nxx(2,7,g) = (lx-mx)*my; - - Nxx(0,8,g) = -ly*uy*2.0; - Nxx(1,8,g) = -lx*ux*2.0; - Nxx(2,8,g) = mx*my*4.0; - } - }, - - {ElementType::TET10, [](const int insd, const int ind2, const int eNoN, const int g, const Array& xi, - Array3& Nxx) -> void { - Nxx.set_row(0, g, {fp, ze, ze, ze, ze, ze}); - Nxx.set_row(1, g, {ze, fp, ze, ze, ze, ze}); - Nxx.set_row(2, g, {ze, ze, fp, ze, ze, ze}); - Nxx.set_row(3, g, {fp, fp, fp, fp, fp, fp}); - Nxx.set_row(4, g, {ze, ze, ze, fp, ze, ze}); - Nxx.set_row(5, g, {ze, ze, ze, ze, fp, ze}); - Nxx.set_row(6, g, {ze, ze, ze, ze, ze, fp}); - Nxx.set_row(7, g, {en, ze, ze, fn, ze, fn}); - Nxx.set_row(8, g, {ze, en, ze, fn, fn, ze}); - Nxx.set_row(9, g, {ze, ze, en, ze, fn, fn}); - } - }, - - {ElementType::TRI6, [](const int insd, const int ind2, const int eNoN, const int g, const Array& xi, - Array3& Nxx) -> void { - - Nxx.set_row(0, g, {fp, ze, ze}); - Nxx.set_row(1, g, {ze, fp, ze}); - Nxx.set_row(2, g, {fp, fp, fp}); - Nxx.set_row(3, g, {ze, ze, fp}); - Nxx.set_row(4, g, {ze, en, fn}); - Nxx.set_row(5, g, {en, ze, fn}); - } - }, - -}; - - diff --git a/Code/Source/solver/post.cpp b/Code/Source/solver/post.cpp index 84b2c23c8..b53e85745 100644 --- a/Code/Source/solver/post.cpp +++ b/Code/Source/solver/post.cpp @@ -805,13 +805,11 @@ void fib_stretch_rate(const ComMod& com_mod, const int iEq, const mshType& lM, c if (dt <= 0.0) { svmp::raise( - SVMP_HERE, "[fib_stretch_rate] Expected com_mod.dt > 0, but got " + std::to_string(dt) + "."); } if (res.size() != nNo) { svmp::raise( - SVMP_HERE, "[fib_stretch_rate] Expected res size " + std::to_string(nNo) + ", but got " + std::to_string(res.size()) + "."); } diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 6c6723bce..e29b0a649 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -203,7 +203,7 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, if (effective_direction.size() != com_mod.nsd) { auto effective_size = (std::stringstream() << "(" << effective_direction.size() << ")").str(); auto space_dim = (std::stringstream() << "(" << com_mod.nsd << ")").str(); - svmp::raise(SVMP_HERE, "The size of the effective direction " + effective_size + + svmp::raise("The size of the effective direction " + effective_size + " does not equal the number of space dimensions " + space_dim); } @@ -2380,7 +2380,7 @@ void read_outputs(Simulation* simulation, EquationParameters* eq_params, eqType& continue; svmp::check_not_null( - dmn.cep.ionic_model, SVMP_HERE, "ionic model was not constructed."); + dmn.cep.ionic_model, "ionic model was not constructed."); const auto registered_outputs = dmn.cep.ionic_model->get_registered_outputs(); @@ -3322,4 +3322,3 @@ void set_equation_properties(Simulation* simulation, EquationParameters* eq_para } }; - diff --git a/Code/Source/solver/utils.cpp b/Code/Source/solver/utils.cpp index e899d6324..fb7874f95 100644 --- a/Code/Source/solver/utils.cpp +++ b/Code/Source/solver/utils.cpp @@ -37,12 +37,8 @@ int CountBits(int n) double cput() { - auto now = std::chrono::system_clock::now(); - auto now_ms = std::chrono::time_point_cast(now); - - auto value = now_ms.time_since_epoch(); - auto duration = value.count() / 1000.0; - return static_cast(duration); + const auto now = std::chrono::system_clock::now(); + return std::chrono::duration(now.time_since_epoch()).count(); } Vector @@ -361,4 +357,4 @@ void find_loc(const Array& array, int value, std::array& ind) } } -}; \ No newline at end of file +}; diff --git a/Documentation/Doxyfile b/Documentation/Doxyfile index acd5ba21c..fba8c016a 100644 --- a/Documentation/Doxyfile +++ b/Documentation/Doxyfile @@ -267,12 +267,12 @@ PERLMOD_MAKEVAR_PREFIX = # Configuration options related to the preprocessor #--------------------------------------------------------------------------- ENABLE_PREPROCESSING = YES -MACRO_EXPANSION = NO -EXPAND_ONLY_PREDEF = NO +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = YES INCLUDE_PATH = INCLUDE_FILE_PATTERNS = -PREDEFINED = +PREDEFINED = "SVMP_DEFINE_EXCEPTION(Name,Base,Status)=class Name : public Base { }" EXPAND_AS_DEFINED = SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- diff --git a/Documentation/DoxygenLayout.xml b/Documentation/DoxygenLayout.xml index df0146828..f056df891 100644 --- a/Documentation/DoxygenLayout.xml +++ b/Documentation/DoxygenLayout.xml @@ -3,7 +3,11 @@ + + diff --git a/tests/cases/fsi/pipe_3d/result_005.vtu b/tests/cases/fsi/pipe_3d/result_005.vtu index b78ea6500..a7ca69daf 100644 --- a/tests/cases/fsi/pipe_3d/result_005.vtu +++ b/tests/cases/fsi/pipe_3d/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ac116931be9b2a7d5024de8359f9ea09cae964e9bd34ba949f4bfb9312c8af -size 210065 +oid sha256:b13d09a343a3fd8d033b0e3ecaf2cd94ce68e2ee8665144f7a53cca201db4266 +size 227356 diff --git a/tests/cases/fsi/pipe_3d_petsc/result_005.vtu b/tests/cases/fsi/pipe_3d_petsc/result_005.vtu index b78ea6500..a7ca69daf 100644 --- a/tests/cases/fsi/pipe_3d_petsc/result_005.vtu +++ b/tests/cases/fsi/pipe_3d_petsc/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ac116931be9b2a7d5024de8359f9ea09cae964e9bd34ba949f4bfb9312c8af -size 210065 +oid sha256:b13d09a343a3fd8d033b0e3ecaf2cd94ce68e2ee8665144f7a53cca201db4266 +size 227356 diff --git a/tests/cases/fsi/pipe_3d_trilinos_bj/result_005.vtu b/tests/cases/fsi/pipe_3d_trilinos_bj/result_005.vtu index b78ea6500..a7ca69daf 100644 --- a/tests/cases/fsi/pipe_3d_trilinos_bj/result_005.vtu +++ b/tests/cases/fsi/pipe_3d_trilinos_bj/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ac116931be9b2a7d5024de8359f9ea09cae964e9bd34ba949f4bfb9312c8af -size 210065 +oid sha256:b13d09a343a3fd8d033b0e3ecaf2cd94ce68e2ee8665144f7a53cca201db4266 +size 227356 diff --git a/tests/cases/fsi/pipe_3d_trilinos_ml/result_005.vtu b/tests/cases/fsi/pipe_3d_trilinos_ml/result_005.vtu index b78ea6500..a7ca69daf 100644 --- a/tests/cases/fsi/pipe_3d_trilinos_ml/result_005.vtu +++ b/tests/cases/fsi/pipe_3d_trilinos_ml/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54ac116931be9b2a7d5024de8359f9ea09cae964e9bd34ba949f4bfb9312c8af -size 210065 +oid sha256:b13d09a343a3fd8d033b0e3ecaf2cd94ce68e2ee8665144f7a53cca201db4266 +size 227356 diff --git a/tests/cases/fsi/pipe_RCR_3d/result_005.vtu b/tests/cases/fsi/pipe_RCR_3d/result_005.vtu index 79eaced8c..6945fd005 100644 --- a/tests/cases/fsi/pipe_RCR_3d/result_005.vtu +++ b/tests/cases/fsi/pipe_RCR_3d/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f194a3c364de0bf1a6cc79ba542306469e151de36275a06564022730c3f2c84c -size 209865 +oid sha256:25a08e99ae0163800e73ea54720557d742548fe75a0eb6b68461d8bdb366972f +size 227320 diff --git a/tests/cases/fsi_ustruct/pipe_3d/result_005.vtu b/tests/cases/fsi_ustruct/pipe_3d/result_005.vtu index c838c9c3f..8b5f73c2a 100644 --- a/tests/cases/fsi_ustruct/pipe_3d/result_005.vtu +++ b/tests/cases/fsi_ustruct/pipe_3d/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:262ffb4d7b644280f15bb2e32c8e5fc5ddade7fa5cabd845c31fe3803e9ef0a0 -size 207864 +oid sha256:16f0f2b2ea6a133f54db03954e76ea7586b0fb56d36e2e350ccd21ebadaf4bfb +size 228764 diff --git a/tests/cases/fsi_ustruct/pipe_RCR_3d/result_005.vtu b/tests/cases/fsi_ustruct/pipe_RCR_3d/result_005.vtu index e9e051d73..7d6c64d9b 100644 --- a/tests/cases/fsi_ustruct/pipe_RCR_3d/result_005.vtu +++ b/tests/cases/fsi_ustruct/pipe_RCR_3d/result_005.vtu @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7dec176a56b610ed6b754f66e532a15ac1563b72c25198f49a0bc53adc6e4552 -size 207628 +oid sha256:5c00d715542a495f37a6ea1cd514cc654d3215360170a06c3af1440b71f7d093 +size 228708 diff --git a/tests/unitTests/FE/Basis/test_BasisErrorPaths.cpp b/tests/unitTests/FE/Basis/test_BasisErrorPaths.cpp new file mode 100644 index 000000000..e81a9c377 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_BasisErrorPaths.cpp @@ -0,0 +1,439 @@ +/** + * @file test_BasisErrorPaths.cpp + * @brief Error-path coverage for the Lagrange-focused Basis subset. + */ + +#include + +#include "FE/Basis/BasisExceptions.h" +#include "FE/Basis/BasisFactory.h" +#include "FE/Basis/BasisFunction.h" +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/NodeOrderingConventions.h" +#include "FE/Basis/SerendipityBasis.h" + +#include +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +// Build a symmetric 3x3 Hessian from its six independent components. Local to +// this test; the production basis evaluators fill Hessians directly. +[[nodiscard]] Hessian make_symmetric_hessian(double xx, + double yy, + double zz, + double xy, + double xz, + double yz) { + Hessian hessian = Hessian::Zero(); + hessian(0, 0) = xx; + hessian(1, 1) = yy; + hessian(2, 2) = zz; + hessian(0, 1) = hessian(1, 0) = xy; + hessian(0, 2) = hessian(2, 0) = xz; + hessian(1, 2) = hessian(2, 1) = yz; + return hessian; +} + +class MinimalScalarBasis : public BasisFunction { +public: + BasisType basis_type() const noexcept override { return BasisType::Lagrange; } + BasisTopology topology() const noexcept override { return BasisTopology::Line; } + int dimension() const noexcept override { return 1; } + int order() const noexcept override { return 1; } + std::size_t size() const noexcept override { return 2u; } + + void evaluate_values_to(const math::Vector&, + std::span values_out) const override + { + std::fill(values_out.begin(), values_out.end(), double(0)); + } +}; + +// Quadratic scalar basis with exact analytic derivatives, used to verify the +// protected numerical_gradient/numerical_hessian development helpers. Centered +// differences are exact (up to roundoff) on quadratics, so any mismatch is a +// bug in the helpers themselves. +class ExactQuadraticBasis : public BasisFunction { +public: + using BasisFunction::numerical_gradient; + using BasisFunction::numerical_hessian; + + BasisType basis_type() const noexcept override { return BasisType::Custom; } + BasisTopology topology() const noexcept override { return BasisTopology::Hexahedron; } + int dimension() const noexcept override { return 3; } + int order() const noexcept override { return 2; } + std::size_t size() const noexcept override { return 2u; } + + void evaluate_values_to(const math::Vector& xi, + std::span values_out) const override + { + const double x = xi[0]; + const double y = xi[1]; + const double z = xi[2]; + values_out[0] = double(1) + double(2) * x - y + double(0.5) * z + + x * x + double(0.75) * y * y - double(0.25) * z * z + + double(0.2) * x * y - double(0.3) * x * z + double(0.4) * y * z; + values_out[1] = double(3) - x + double(2) * y + z + + double(0.5) * x * x - y * y + z * z + + x * y + x * z - y * z; + } + + void evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const override + { + const double x = xi[0]; + const double y = xi[1]; + const double z = xi[2]; + gradients_out[0] = Gradient::Zero(); + gradients_out[1] = Gradient::Zero(); + gradients_out[0][0] = double(2) + double(2) * x + double(0.2) * y - double(0.3) * z; + gradients_out[0][1] = double(-1) + double(1.5) * y + double(0.2) * x + double(0.4) * z; + gradients_out[0][2] = double(0.5) - double(0.5) * z - double(0.3) * x + double(0.4) * y; + gradients_out[1][0] = double(-1) + x + y + z; + gradients_out[1][1] = double(2) - double(2) * y + x - z; + gradients_out[1][2] = double(1) + double(2) * z + x - y; + } + + void exact_hessians(std::vector& hessians) const + { + hessians.assign(size(), Hessian::Zero()); + hessians[0] = make_symmetric_hessian(double(2), double(1.5), double(-0.5), + double(0.2), double(-0.3), double(0.4)); + hessians[1] = make_symmetric_hessian(double(1), double(-2), double(2), + double(1), double(1), double(-1)); + } +}; + +// Basis that implements only the span primitives and deliberately does not +// override the combined evaluate_all_to. It therefore exercises the base class's +// vector overloads and the default combined evaluator, both of which must forward +// to these primitives. +class SpanPrimitiveBasis : public BasisFunction { +public: + BasisType basis_type() const noexcept override { return BasisType::Lagrange; } + BasisTopology topology() const noexcept override { return BasisTopology::Triangle; } + int dimension() const noexcept override { return 2; } + int order() const noexcept override { return 1; } + std::size_t size() const noexcept override { return 2u; } + + void evaluate_values_to(const math::Vector& xi, + std::span values_out) const override + { + values_out[0] = double(1) + xi[0]; + values_out[1] = double(2) + xi[1]; + } + + void evaluate_gradients_to(const math::Vector&, + std::span gradients_out) const override + { + gradients_out[0] = Gradient::Zero(); + gradients_out[1] = Gradient::Zero(); + gradients_out[0][0] = double(1); + gradients_out[1][1] = double(1); + } + + void evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const override + { + for (std::size_t d = 0; d < size(); ++d) { + hessians_out[d] = Hessian::Zero(); + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + hessians_out[d](r, c) = double(100) * static_cast(d + 1u) + + double(10) * static_cast(r) + + static_cast(c) + xi[2]; + } + } + } + } +}; + +void expect_source_location(const svmp::ExceptionBase& e) +{ + EXPECT_NE(e.context().file().find("test_BasisErrorPaths.cpp"), std::string::npos); + EXPECT_GT(e.context().line(), 0); + EXPECT_FALSE(e.context().function().empty()); +} + +// The core helpers raise both FE-subsystem exceptions and Core exceptions (for +// example not_implemented() defaults to svmp::NotImplementedException), so this +// catches their common base, svmp::ExceptionBase. +template +void expect_core_helper_preserves_source_location(Thrower&& thrower) +{ + try { + thrower(); + FAIL() << "Expected an svmp::ExceptionBase"; + } catch (const svmp::ExceptionBase& e) { + expect_source_location(e); + } +} + +} // namespace + +TEST(BasisErrorPaths, LagrangeInvalidRequestsThrowBasisExceptions) { + EXPECT_THROW(LagrangeBasis(ElementType::Unknown, 1), + BasisElementCompatibilityException); + EXPECT_THROW(LagrangeBasis(ElementType::Line2, -1), + BasisConfigurationException); + EXPECT_THROW(LagrangeBasis(ElementType::Quad8, 2), + BasisElementCompatibilityException); + EXPECT_NO_THROW((void)LagrangeBasis(BasisTopology::Point, 0)); + EXPECT_THROW((void)LagrangeBasis(BasisTopology::Point, 1), + BasisConfigurationException); +} + +// A named Lagrange element layout fixes its polynomial order: the matching order +// is accepted and any other order is rejected. Arbitrary orders must be +// requested through the BasisTopology overload, never by over-/under-specifying +// a node-count-named element. +TEST(BasisErrorPaths, NamedLagrangeElementsRejectNonBakedOrders) { + const std::vector> named = { + {ElementType::Point1, 0}, + {ElementType::Line2, 1}, {ElementType::Line3, 2}, + {ElementType::Triangle3, 1}, {ElementType::Triangle6, 2}, + {ElementType::Quad4, 1}, {ElementType::Quad9, 2}, + {ElementType::Tetra4, 1}, {ElementType::Tetra10, 2}, + {ElementType::Hex8, 1}, {ElementType::Hex27, 2}, + {ElementType::Wedge6, 1}, {ElementType::Wedge18, 2}, + }; + + for (const auto& [type, baked] : named) { + EXPECT_NO_THROW((void)LagrangeBasis(type, baked)) + << "element=" << static_cast(type); + EXPECT_THROW((void)LagrangeBasis(type, baked + 1), BasisConfigurationException) + << "element=" << static_cast(type); + if (baked > 0) { + EXPECT_THROW((void)LagrangeBasis(type, baked - 1), BasisConfigurationException) + << "element=" << static_cast(type); + } + } +} + +TEST(BasisErrorPaths, SerendipityInvalidRequestsThrowBasisExceptions) { + EXPECT_THROW(SerendipityBasis(ElementType::Unknown, 2), + BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Quad8, 3), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid13, 2), + BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid14, 2), + BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex8, 2), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex20, 1), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex20, 3), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Wedge15, 1), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Wedge15, 3), + BasisConfigurationException); + + // Order 0 and negative orders are rejected for every serendipity layout; a + // named element is pinned to its inferred order and is never floored up to it. + EXPECT_THROW(SerendipityBasis(ElementType::Quad8, 0), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex8, 0), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex20, 0), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Wedge15, 0), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Hex8, -1), + BasisConfigurationException); +} + +TEST(BasisErrorPaths, BasisFactoryRejectsNonC0Continuity) { + BasisRequest c1_request{ElementType::Line2, BasisType::Lagrange, 1}; + c1_request.continuity = Continuity::C1; + EXPECT_THROW((void)basis_factory::create(c1_request), BasisConfigurationException); + + BasisRequest l2_request{ElementType::Quad8, BasisType::Serendipity, 2}; + l2_request.continuity = Continuity::L2; + EXPECT_THROW((void)basis_factory::create(l2_request), BasisConfigurationException); +} + +TEST(BasisErrorPaths, BasisFactoryInvalidRequestsThrowBasisExceptions) { + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Line2, BasisType::Lagrange}), + BasisConfigurationException); + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Line2, BasisType::Lagrange, -1}), + BasisConfigurationException); + // NURBS is a declared but unimplemented family, so the scalar factory rejects + // it as outside the Lagrange/Serendipity scope. + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Line2, BasisType::NURBS, 1}), + BasisConfigurationException); + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Pyramid5, BasisType::Lagrange, 1}), + BasisElementCompatibilityException); + + BasisRequest vector_req{ElementType::Line2, BasisType::Lagrange, 1}; + vector_req.field_type = FieldType::Vector; + EXPECT_THROW((void)basis_factory::create(vector_req), BasisConfigurationException); + + auto serendipity = basis_factory::create( + BasisRequest{ElementType::Quad8, BasisType::Serendipity, 2}); + ASSERT_NE(serendipity, nullptr); + EXPECT_EQ(serendipity->basis_type(), BasisType::Serendipity); +} + +TEST(BasisErrorPaths, BasisExceptionsUseCommonStatusCodes) { + try { + svmp::raise("invalid config"); + } catch (const FEException& e) { + EXPECT_EQ(e.status(), svmp::StatusCode::InvalidArgument); + } + + try { + svmp::raise("construction failure"); + } catch (const FEException& e) { + EXPECT_EQ(e.status(), svmp::StatusCode::InternalError); + } +} + +TEST(BasisErrorPaths, CoreHelpersPreserveSourceLocation) { + expect_core_helper_preserves_source_location([] { + svmp::raise("raise location"); + }); + + expect_core_helper_preserves_source_location([] { + svmp::throw_if( + true, "throw_if location"); + }); + + expect_core_helper_preserves_source_location([] { + svmp::check( + false, "check location"); + }); + + expect_core_helper_preserves_source_location([] { + const int* ptr = nullptr; + svmp::check_not_null( + ptr, "check_not_null location"); + }); + + expect_core_helper_preserves_source_location([] { + svmp::check_index(1, 1); + }); + + expect_core_helper_preserves_source_location([] { + svmp::not_implemented( + "test feature"); + }); +} + +TEST(BasisErrorPaths, NodeOrderingInvalidNodeThrows) { + EXPECT_THROW((void)line_coord_pm_one(-1, 1), + BasisNodeOrderingException); + EXPECT_THROW((void)line_coord_pm_one(2, 1), + BasisNodeOrderingException); + EXPECT_THROW((void)line_coord_pm_one(-1, 0), + BasisNodeOrderingException); + EXPECT_THROW((void)line_coord_pm_one(1, 0), + BasisNodeOrderingException); + EXPECT_THROW((void)ReferenceNodeLayout::node_coord_at(ElementType::Quad8, 99u), + BasisNodeOrderingException); + EXPECT_THROW((void)ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Quad8, 2), + BasisNodeOrderingException); + EXPECT_THROW((void)ReferenceNodeLayout::num_nodes(ElementType::Pyramid5), + BasisNodeOrderingException); +} + +TEST(BasisErrorPaths, BasisFunctionDefaultsThrowForMissingDerivatives) { + MinimalScalarBasis basis; + const math::Vector xi{double(0), double(0), double(0)}; + std::vector gradients; + std::vector hessians; + + EXPECT_THROW(basis.evaluate_gradients(xi, gradients), BasisEvaluationException); + EXPECT_THROW(basis.evaluate_hessians(xi, hessians), BasisEvaluationException); +} + +TEST(BasisErrorPaths, NumericalDerivativeHelpersMatchAnalyticDerivatives) { + ExactQuadraticBasis basis; + const math::Vector xi{double(0.2), double(-0.35), double(0.4)}; + + // On a quadratic, centered differences are exact except for the round-off + // floor ~ eps_machine/step. The tolerances below are a few times + // those floors -- tight enough that a wrong difference or analytic formula + // (which would give an O(step) or O(1) error) cannot slip through. + std::vector exact_gradients; + basis.evaluate_gradients(xi, exact_gradients); + + std::vector approx_gradients; + basis.numerical_gradient(xi, approx_gradients); + ASSERT_EQ(approx_gradients.size(), basis.size()); + for (std::size_t n = 0; n < basis.size(); ++n) { + for (int d = 0; d < basis.dimension(); ++d) { + const std::size_t sd = static_cast(d); + EXPECT_NEAR(approx_gradients[n][sd], exact_gradients[n][sd], double(3e-9)) + << "basis=" << n << " component=" << d; + } + } + + std::vector exact_hessians; + basis.exact_hessians(exact_hessians); + + std::vector approx_hessians; + basis.numerical_hessian(xi, approx_hessians); + ASSERT_EQ(approx_hessians.size(), basis.size()); + for (std::size_t n = 0; n < basis.size(); ++n) { + for (int r = 0; r < basis.dimension(); ++r) { + for (int c = 0; c < basis.dimension(); ++c) { + const std::size_t sr = static_cast(r); + const std::size_t sc = static_cast(c); + EXPECT_NEAR(approx_hessians[n](sr, sc), exact_hessians[n](sr, sc), + double(2e-10)) + << "basis=" << n << " component=(" << r << "," << c << ")"; + } + } + } +} + +TEST(BasisErrorPaths, BasisFunctionVectorOverloadsForwardToSpanPrimitives) { + SpanPrimitiveBasis basis; + const math::Vector point{double(0.25), double(0.5), double(-0.25)}; + + // Reference results taken directly from the span primitives the basis defines. + std::vector span_values(basis.size()); + std::vector span_gradients(basis.size()); + std::vector span_hessians(basis.size()); + basis.evaluate_values_to(point, span_values); + basis.evaluate_gradients_to(point, span_gradients); + basis.evaluate_hessians_to(point, span_hessians); + + // The base-class vector overloads must size their outputs and forward to the + // span primitives; evaluate_all() goes through the default combined evaluator. + std::vector values; + basis.evaluate_values(point, values); + std::vector all_values; + std::vector all_gradients; + std::vector all_hessians; + basis.evaluate_all(point, all_values, all_gradients, all_hessians); + + ASSERT_EQ(values.size(), basis.size()); + ASSERT_EQ(all_values.size(), basis.size()); + ASSERT_EQ(all_gradients.size(), basis.size()); + ASSERT_EQ(all_hessians.size(), basis.size()); + for (std::size_t d = 0; d < basis.size(); ++d) { + EXPECT_EQ(values[d], span_values[d]); + EXPECT_EQ(all_values[d], span_values[d]); + for (std::size_t c = 0; c < 3u; ++c) { + EXPECT_EQ(all_gradients[d][c], span_gradients[d][c]); + } + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + EXPECT_EQ(all_hessians[d](r, c), span_hessians[d](r, c)); + } + } + } +} diff --git a/tests/unitTests/FE/Basis/test_BasisHessians.cpp b/tests/unitTests/FE/Basis/test_BasisHessians.cpp new file mode 100644 index 000000000..fd66a9e74 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_BasisHessians.cpp @@ -0,0 +1,451 @@ +/** + * @file test_BasisHessians.cpp + * @brief Analytical Hessian coverage for the migrated Lagrange basis. + */ + +#include + +#include "FE/Basis/BasisFactory.h" +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/SerendipityBasis.h" + +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +// The exact Hessian identities below -- the partition sum (sum_i Hess N_i = 0) and +// symmetry (Hess N_i = Hess N_i^T) -- have a floating-point round-off residual at +// every order and family, so they share one tolerance. The finite-difference-vs- +// analytic comparisons keep their own, larger, order-dependent tolerances because +// finite-difference error grows with order. +constexpr double kHessianInvariantTol = double(1e-12); + +void numerical_gradient_helper(const BasisFunction& basis, + const math::Vector& xi, + std::vector& gradients, + double eps = double(1e-6)) +{ + std::vector base; + basis.evaluate_values(xi, base); + gradients.assign(base.size(), Gradient::Zero()); + + for (int d = 0; d < basis.dimension(); ++d) { + const std::size_t sd = static_cast(d); + math::Vector xi_p = xi; + math::Vector xi_m = xi; + xi_p[sd] += eps; + xi_m[sd] -= eps; + + std::vector v_p; + std::vector v_m; + basis.evaluate_values(xi_p, v_p); + basis.evaluate_values(xi_m, v_m); + + for (std::size_t n = 0; n < base.size(); ++n) { + gradients[n][sd] = (v_p[n] - v_m[n]) / (double(2) * eps); + } + } +} + +void numerical_hessian_helper(const BasisFunction& basis, + const math::Vector& xi, + std::vector& hessians, + double eps = double(1e-5)) +{ + hessians.assign(basis.size(), Hessian::Zero()); + const int dim = basis.dimension(); + + for (int i = 0; i < dim; ++i) { + for (int j = 0; j < dim; ++j) { + math::Vector xi_p = xi; + math::Vector xi_m = xi; + const std::size_t sj = static_cast(j); + xi_p[sj] += eps; + xi_m[sj] -= eps; + + std::vector g_p; + std::vector g_m; + basis.evaluate_gradients(xi_p, g_p); + basis.evaluate_gradients(xi_m, g_m); + + for (std::size_t n = 0; n < basis.size(); ++n) { + const std::size_t si = static_cast(i); + hessians[n](si, sj) = (g_p[n][si] - g_m[n][si]) / (double(2) * eps); + } + } + } +} + +std::vector> sample_points_for(BasisTopology topology) { + switch (topology) { + case BasisTopology::Line: + return {{double(-0.35), double(0), double(0)}, {double(0.2), double(0), double(0)}}; + case BasisTopology::Triangle: + return {{double(0.15), double(0.2), double(0)}, {double(0.25), double(0.1), double(0)}}; + case BasisTopology::Quadrilateral: + return {{double(0.2), double(-0.3), double(0)}, {double(-0.45), double(0.25), double(0)}}; + case BasisTopology::Tetrahedron: + return {{double(0.12), double(0.18), double(0.16)}, {double(0.2), double(0.1), double(0.18)}}; + case BasisTopology::Hexahedron: + return {{double(0.1), double(-0.2), double(0.3)}, {double(-0.35), double(0.25), double(-0.15)}}; + case BasisTopology::Wedge: + return {{double(0.18), double(0.22), double(-0.2)}, {double(0.12), double(0.16), double(0.1)}}; + default: + return {{double(0), double(0), double(0)}}; + } +} + +void expect_gradients_match_numerical(const BasisFunction& basis, + const std::vector>& points, + double tol, + double eps = double(1e-6)) +{ + for (const auto& xi : points) { + std::vector analytical; + std::vector numerical; + basis.evaluate_gradients(xi, analytical); + numerical_gradient_helper(basis, xi, numerical, eps); + + ASSERT_EQ(analytical.size(), numerical.size()); + for (std::size_t n = 0; n < analytical.size(); ++n) { + for (int d = 0; d < basis.dimension(); ++d) { + const std::size_t sd = static_cast(d); + EXPECT_NEAR(analytical[n][sd], numerical[n][sd], tol) + << "basis " << n << ", component " << d + << ", element " << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + } + } + } +} + +void expect_hessians_match_numerical(const BasisFunction& basis, + const std::vector>& points, + double tol, + double eps = double(1e-5)) +{ + for (const auto& xi : points) { + std::vector analytical; + std::vector numerical; + basis.evaluate_hessians(xi, analytical); + numerical_hessian_helper(basis, xi, numerical, eps); + + ASSERT_EQ(analytical.size(), numerical.size()); + for (std::size_t n = 0; n < analytical.size(); ++n) { + for (int i = 0; i < basis.dimension(); ++i) { + for (int j = 0; j < basis.dimension(); ++j) { + const std::size_t si = static_cast(i); + const std::size_t sj = static_cast(j); + EXPECT_NEAR(analytical[n](si, sj), numerical[n](si, sj), tol) + << "basis " << n << ", component (" << i << "," << j + << "), element " << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + } + } + } + } +} + +void expect_partition_hessian_sum_zero(const BasisFunction& basis, + const math::Vector& xi, + double tol) +{ + std::vector hessians; + basis.evaluate_hessians(xi, hessians); + + Hessian sum = Hessian::Zero(); + for (const auto& hessian : hessians) { + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + sum(r, c) += hessian(r, c); + } + } + } + + for (int r = 0; r < basis.dimension(); ++r) { + for (int c = 0; c < basis.dimension(); ++c) { + EXPECT_NEAR(sum(static_cast(r), static_cast(c)), + double(0), + tol) + << "element " << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + } + } +} + +void expect_hessians_symmetric(const BasisFunction& basis, + const math::Vector& xi, + double tol) +{ + std::vector hessians; + basis.evaluate_hessians(xi, hessians); + + for (const auto& hessian : hessians) { + for (int r = 0; r < basis.dimension(); ++r) { + for (int c = r + 1; c < basis.dimension(); ++c) { + const std::size_t sr = static_cast(r); + const std::size_t sc = static_cast(c); + EXPECT_NEAR(hessian(sr, sc), hessian(sc, sr), tol); + } + } + } +} + +void expect_inactive_z_derivatives_zero(const BasisFunction& basis, + const std::vector>& points, + double tol) +{ + ASSERT_EQ(basis.dimension(), 2); + for (const auto& xi : points) { + std::vector gradients; + std::vector hessians; + basis.evaluate_gradients(xi, gradients); + basis.evaluate_hessians(xi, hessians); + + ASSERT_EQ(gradients.size(), basis.size()); + ASSERT_EQ(hessians.size(), basis.size()); + for (std::size_t n = 0; n < basis.size(); ++n) { + EXPECT_NEAR(gradients[n][2], double(0), tol) + << "basis " << n << ", element " + << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(hessians[n](2, d), double(0), tol) + << "basis " << n << ", component (2," << d + << "), element " << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + EXPECT_NEAR(hessians[n](d, 2), double(0), tol) + << "basis " << n << ", component (" << d + << ",2), element " << static_cast(named_element_for(basis.topology(), basis.order(), basis.basis_type())) + << ", order " << basis.order(); + } + } + } +} + +std::vector> serendipity_sample_points(BasisTopology topology) { + if (topology == BasisTopology::Quadrilateral) { + return {{double(0.17), double(-0.31), double(0)}, {double(-0.45), double(0.25), double(0)}}; + } + if (topology == BasisTopology::Hexahedron) { + return {{double(0.2), double(-0.1), double(0.3)}, {double(-0.35), double(0.25), double(-0.15)}}; + } + return {{double(0.2), double(0.3), double(0.1)}, {double(0.12), double(0.16), double(-0.2)}}; // wedge +} + +} // namespace + +TEST(BasisHessians, LagrangeCanonicalTopologiesMatchNumericalHessians) { + const struct Case { + BasisTopology topology; + int order; + double tol; + double eps; + } cases[] = { + {BasisTopology::Line, 3, double(1e-7), double(1e-5)}, + {BasisTopology::Triangle, 3, double(2e-6), double(1e-5)}, + {BasisTopology::Quadrilateral, 3, double(1e-6), double(1e-5)}, + {BasisTopology::Tetrahedron, 2, double(1e-6), double(1e-5)}, + {BasisTopology::Hexahedron, 2, double(1e-6), double(1e-5)}, + {BasisTopology::Wedge, 2, double(1e-5), double(1e-5)}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.topology, c.order); + expect_hessians_match_numerical(basis, sample_points_for(c.topology), c.tol, c.eps); + } +} + +TEST(BasisHessians, LagrangeHessiansSumToZeroAndAreSymmetric) { + const struct Case { + BasisTopology topology; + int order; + math::Vector xi; + } cases[] = { + {BasisTopology::Line, 3, {double(0.15), double(0), double(0)}}, + {BasisTopology::Triangle, 3, {double(0.2), double(0.25), double(0)}}, + {BasisTopology::Quadrilateral, 3, {double(0.3), double(-0.2), double(0)}}, + {BasisTopology::Tetrahedron, 2, {double(0.15), double(0.2), double(0.1)}}, + {BasisTopology::Hexahedron, 2, {double(0.1), double(-0.2), double(0.3)}}, + {BasisTopology::Wedge, 2, {double(0.2), double(0.15), double(-0.3)}}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.topology, c.order); + expect_partition_hessian_sum_zero(basis, c.xi, kHessianInvariantTol); + expect_hessians_symmetric(basis, c.xi, kHessianInvariantTol); + } +} + +TEST(BasisHessians, SerendipityHessiansSumToZeroAndAreSymmetric) { + const struct Case { + ElementType type; + int order; + math::Vector xi; + } cases[] = { + {ElementType::Quad8, 2, {double(0.17), double(-0.31), double(0)}}, + {ElementType::Hex20, 2, {double(0.2), double(-0.1), double(0.3)}}, + {ElementType::Wedge15, 2, {double(0.2), double(0.3), double(0.1)}}, + }; + + for (const auto& c : cases) { + SerendipityBasis basis(c.type, c.order); + expect_partition_hessian_sum_zero(basis, c.xi, kHessianInvariantTol); + expect_hessians_symmetric(basis, c.xi, kHessianInvariantTol); + } +} + +TEST(BasisHessians, SolverMappedVolumeSelectionsSatisfyInvariants) { + // Mirrors the full default element set pinned in + // BasisFactoryDefaults.SelectionsArePinnedForAllSupportedElements (including + // both wedge defaults: Wedge15 serendipity and Wedge18 Lagrange), so every + // family the solver adapter can map is exercised for the Hessian invariants. + const struct Case { + ElementType type; + BasisType basis_type; + int order; + math::Vector xi; + } cases[] = { + {ElementType::Line2, BasisType::Lagrange, 1, {double(0.15), double(0), double(0)}}, + {ElementType::Line3, BasisType::Lagrange, 2, {double(-0.25), double(0), double(0)}}, + {ElementType::Triangle3, BasisType::Lagrange, 1, {double(0.2), double(0.25), double(0)}}, + {ElementType::Triangle6, BasisType::Lagrange, 2, {double(0.2), double(0.25), double(0)}}, + {ElementType::Quad4, BasisType::Lagrange, 1, {double(0.3), double(-0.2), double(0)}}, + {ElementType::Quad8, BasisType::Serendipity, 2, {double(0.17), double(-0.31), double(0)}}, + {ElementType::Quad9, BasisType::Lagrange, 2, {double(0.3), double(-0.2), double(0)}}, + {ElementType::Tetra4, BasisType::Lagrange, 1, {double(0.15), double(0.2), double(0.1)}}, + {ElementType::Tetra10, BasisType::Lagrange, 2, {double(0.15), double(0.2), double(0.1)}}, + {ElementType::Hex8, BasisType::Lagrange, 1, {double(0.1), double(-0.2), double(0.3)}}, + {ElementType::Hex20, BasisType::Serendipity, 2, {double(0.2), double(-0.1), double(0.3)}}, + {ElementType::Hex27, BasisType::Lagrange, 2, {double(0.1), double(-0.2), double(0.3)}}, + {ElementType::Wedge6, BasisType::Lagrange, 1, {double(0.2), double(0.15), double(-0.3)}}, + {ElementType::Wedge15, BasisType::Serendipity, 2, {double(0.2), double(0.3), double(0.1)}}, + {ElementType::Wedge18, BasisType::Lagrange, 2, {double(0.2), double(0.15), double(-0.3)}}, + }; + + for (const auto& c : cases) { + auto basis = basis_factory::create(BasisRequest{c.type, c.basis_type, c.order}); + ASSERT_NE(basis, nullptr) << "element=" << static_cast(c.type); + expect_partition_hessian_sum_zero(*basis, c.xi, kHessianInvariantTol); + expect_hessians_symmetric(*basis, c.xi, kHessianInvariantTol); + } +} + +// Gradients must match centered finite differences of values. This is the only +// check that ties the gradient code path back to the value code path; partition +// sums and Hessian-vs-FD(gradient) comparisons cannot catch a systematic error +// shared by the first- and second-derivative recurrences. +TEST(BasisGradients, LagrangeCanonicalTopologiesMatchNumericalGradients) { + const struct Case { + BasisTopology topology; + int order; + double tol; + } cases[] = { + {BasisTopology::Line, 3, double(1e-8)}, + {BasisTopology::Triangle, 3, double(1e-7)}, + {BasisTopology::Quadrilateral, 3, double(1e-7)}, + {BasisTopology::Tetrahedron, 2, double(1e-7)}, + {BasisTopology::Hexahedron, 2, double(1e-7)}, + {BasisTopology::Wedge, 2, double(1e-7)}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.topology, c.order); + expect_gradients_match_numerical(basis, sample_points_for(c.topology), c.tol); + } +} + +// The serendipity coefficient tables (Hex20 20x20, Wedge15 15x15) and the quad +// inverse-Vandermonde path each differentiate values through hand-written code +// that is independent of the value evaluation. Partition sums only verify that +// the constant function differentiates to zero, and symmetry is assigned +// structurally, so neither can detect a wrong derivative formula. Finite +// differences of values are the authoritative check. +TEST(BasisGradients, SerendipityFamiliesMatchNumericalGradients) { + // Arbitrary-order quadrilateral serendipity (topology path). + const struct QuadCase { int order; double tol; } quad_cases[] = { + {1, double(1e-8)}, {3, double(1e-7)}, {4, double(5e-7)}, {6, double(2e-6)}, + }; + for (const auto& c : quad_cases) { + SerendipityBasis basis(BasisTopology::Quadrilateral, c.order); + expect_gradients_match_numerical( + basis, serendipity_sample_points(BasisTopology::Quadrilateral), c.tol); + } + + // Arbitrary-order hexahedral serendipity (topology path). + const struct HexCase { int order; double tol; } hex_cases[] = { + {1, double(1e-8)}, {2, double(1e-7)}, {3, double(5e-7)}, + {4, double(1e-6)}, {5, double(5e-6)}, + }; + for (const auto& c : hex_cases) { + SerendipityBasis basis(BasisTopology::Hexahedron, c.order); + expect_gradients_match_numerical( + basis, serendipity_sample_points(BasisTopology::Hexahedron), c.tol); + } + + // Named fixed serendipity layouts. + const struct NamedCase { ElementType type; int order; double tol; } named_cases[] = { + {ElementType::Quad8, 2, double(1e-7)}, + {ElementType::Hex8, 1, double(1e-8)}, + {ElementType::Hex20, 2, double(1e-7)}, + {ElementType::Wedge15, 2, double(1e-7)}, + }; + for (const auto& c : named_cases) { + SerendipityBasis basis(c.type, c.order); + expect_gradients_match_numerical( + basis, serendipity_sample_points(basis.topology()), c.tol); + } +} + +TEST(BasisGradients, QuadrilateralSerendipityInactiveZDerivativesRemainZero) { + // All quadrilateral serendipity, including the production order 2, exercised + // through the arbitrary-order topology path. + for (const int order : {1, 2, 4, 6, 10}) { + SerendipityBasis basis(BasisTopology::Quadrilateral, order); + expect_inactive_z_derivatives_zero( + basis, + serendipity_sample_points(BasisTopology::Quadrilateral), + double(1e-12)); + } +} + +TEST(BasisHessians, SerendipityFamiliesMatchNumericalHessians) { + // Arbitrary-order quadrilateral serendipity (topology path). + const struct QuadCase { int order; double tol; } quad_cases[] = { + {1, double(1e-6)}, {3, double(1e-6)}, {4, double(2e-6)}, {6, double(5e-6)}, + }; + for (const auto& c : quad_cases) { + SerendipityBasis basis(BasisTopology::Quadrilateral, c.order); + expect_hessians_match_numerical( + basis, serendipity_sample_points(BasisTopology::Quadrilateral), c.tol); + } + + // Arbitrary-order hexahedral serendipity (topology path). + const struct HexCase { int order; double tol; } hex_cases[] = { + {1, double(1e-6)}, {2, double(1e-6)}, {3, double(2e-6)}, + {4, double(5e-6)}, {5, double(1e-5)}, + }; + for (const auto& c : hex_cases) { + SerendipityBasis basis(BasisTopology::Hexahedron, c.order); + expect_hessians_match_numerical( + basis, serendipity_sample_points(BasisTopology::Hexahedron), c.tol); + } + + // Named fixed serendipity layouts. + const struct NamedCase { ElementType type; int order; double tol; } named_cases[] = { + {ElementType::Quad8, 2, double(1e-6)}, + {ElementType::Hex8, 1, double(1e-6)}, + {ElementType::Hex20, 2, double(1e-6)}, + {ElementType::Wedge15, 2, double(1e-6)}, + }; + for (const auto& c : named_cases) { + SerendipityBasis basis(c.type, c.order); + expect_hessians_match_numerical( + basis, serendipity_sample_points(basis.topology()), c.tol); + } +} diff --git a/tests/unitTests/FE/Basis/test_ConstexprBasis.cpp b/tests/unitTests/FE/Basis/test_ConstexprBasis.cpp new file mode 100644 index 000000000..f1d50d83f --- /dev/null +++ b/tests/unitTests/FE/Basis/test_ConstexprBasis.cpp @@ -0,0 +1,106 @@ +/** + * @file test_ConstexprBasis.cpp + * @brief Compile-time and lightweight runtime checks for reduced Basis helpers. + */ + +#include "FE/Basis/BasisExceptions.h" +#include "FE/Basis/BasisTraits.h" +#include "FE/Basis/NodeOrderingConventions.h" + +#include + +#include +#include + +namespace svmp { +namespace FE { +namespace basis { +namespace { + +static_assert(topology(ElementType::Pyramid5) == BasisTopology::Unknown); +static_assert(canonical_lagrange_type(ElementType::Hex27) == ElementType::Hex8); +static_assert(canonical_lagrange_type(ElementType::Pyramid13) == ElementType::Pyramid13); +static_assert(complete_lagrange_alias_order(ElementType::Wedge18) == 2); +static_assert(complete_lagrange_alias_order(ElementType::Pyramid14) == -1); + +// Topology/order helpers backing the BasisTopology construction path. +static_assert(topology_dimension(BasisTopology::Line) == 1); +static_assert(topology_dimension(BasisTopology::Hexahedron) == 3); +static_assert(lagrange_topology_representative(BasisTopology::Hexahedron) == ElementType::Hex8); +static_assert(lagrange_topology_representative(BasisTopology::Point) == ElementType::Point1); +static_assert(named_lagrange_order(ElementType::Hex8) == 1); +static_assert(named_lagrange_order(ElementType::Hex27) == 2); +static_assert(named_lagrange_order(ElementType::Point1) == 0); +static_assert(named_element_for(BasisTopology::Hexahedron, 1, BasisType::Lagrange) == ElementType::Hex8); +static_assert(named_element_for(BasisTopology::Hexahedron, 2, BasisType::Lagrange) == ElementType::Hex27); +static_assert(named_element_for(BasisTopology::Hexahedron, 5, BasisType::Lagrange) == ElementType::Unknown); +static_assert(named_element_for(BasisTopology::Point, 0, BasisType::Lagrange) == ElementType::Point1); +static_assert(named_element_for(BasisTopology::Quadrilateral, 2, BasisType::Serendipity) == ElementType::Quad8); +static_assert(named_element_for(BasisTopology::Hexahedron, 2, BasisType::Serendipity) == ElementType::Hex20); +static_assert(named_element_for(BasisTopology::Hexahedron, 1, BasisType::Serendipity) == ElementType::Hex8); + +TEST(ConstexprBasis, FixedNodeTableSizesForSupportedLayouts) { + const std::vector> expected = { + {ElementType::Line2, 2u}, + {ElementType::Line3, 3u}, + {ElementType::Triangle3, 3u}, + {ElementType::Triangle6, 6u}, + {ElementType::Quad4, 4u}, + {ElementType::Quad8, 8u}, + {ElementType::Quad9, 9u}, + {ElementType::Tetra4, 4u}, + {ElementType::Tetra10, 10u}, + {ElementType::Hex8, 8u}, + {ElementType::Hex20, 20u}, + {ElementType::Hex27, 27u}, + {ElementType::Wedge6, 6u}, + {ElementType::Wedge15, 15u}, + {ElementType::Wedge18, 18u}, + }; + + for (const auto& [type, size] : expected) { + EXPECT_EQ(ReferenceNodeLayout::num_nodes(type), size); + } +} + +TEST(ConstexprBasis, CompleteAliasTablesMatchGeneratedLagrangeNodes) { + const std::vector> aliases = { + {ElementType::Line2, ElementType::Line2, 1}, + {ElementType::Line3, ElementType::Line2, 2}, + {ElementType::Triangle3, ElementType::Triangle3, 1}, + {ElementType::Triangle6, ElementType::Triangle3, 2}, + {ElementType::Quad4, ElementType::Quad4, 1}, + {ElementType::Quad9, ElementType::Quad4, 2}, + {ElementType::Tetra4, ElementType::Tetra4, 1}, + {ElementType::Tetra10, ElementType::Tetra4, 2}, + {ElementType::Hex8, ElementType::Hex8, 1}, + {ElementType::Hex27, ElementType::Hex8, 2}, + {ElementType::Wedge6, ElementType::Wedge6, 1}, + {ElementType::Wedge18, ElementType::Wedge6, 2}, + }; + + for (const auto& [alias, canonical_type, order] : aliases) { + const auto nodes = ReferenceNodeLayout::get_lagrange_node_coords(canonical_type, order); + ASSERT_EQ(nodes.size(), ReferenceNodeLayout::num_nodes(alias)); + for (std::size_t i = 0; i < nodes.size(); ++i) { + const auto direct = ReferenceNodeLayout::node_coord_at(alias, i); + EXPECT_EQ(nodes[i][0], direct[0]); + EXPECT_EQ(nodes[i][1], direct[1]); + EXPECT_EQ(nodes[i][2], direct[2]); + } + } +} + +TEST(ConstexprBasis, PyramidNodeOrderingIsOutsideCurrentScope) { + EXPECT_THROW((void)ReferenceNodeLayout::num_nodes(ElementType::Pyramid5), + BasisNodeOrderingException); + EXPECT_THROW((void)ReferenceNodeLayout::num_nodes(ElementType::Pyramid13), + BasisNodeOrderingException); + EXPECT_THROW((void)ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Pyramid5, 1), + BasisNodeOrderingException); +} + +} // namespace +} // namespace basis +} // namespace FE +} // namespace svmp diff --git a/tests/unitTests/FE/Basis/test_HigherOrderWedge.cpp b/tests/unitTests/FE/Basis/test_HigherOrderWedge.cpp new file mode 100644 index 000000000..dd4511b18 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_HigherOrderWedge.cpp @@ -0,0 +1,160 @@ +/** + * @file test_HigherOrderWedge.cpp + * @brief Focused higher-order wedge checks for LagrangeBasis. + */ + +#include + +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/NodeOrderingConventions.h" + +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +void expect_nodes_close(const std::vector>& lhs, + const std::vector>& rhs, + double tol) +{ + ASSERT_EQ(lhs.size(), rhs.size()); + for (std::size_t i = 0; i < lhs.size(); ++i) { + EXPECT_NEAR(lhs[i][0], rhs[i][0], tol) << "node " << i; + EXPECT_NEAR(lhs[i][1], rhs[i][1], tol) << "node " << i; + EXPECT_NEAR(lhs[i][2], rhs[i][2], tol) << "node " << i; + } +} + +void expect_kronecker_at_nodes(const LagrangeBasis& basis, double tol) +{ + const auto& nodes = basis.nodes(); + ASSERT_EQ(nodes.size(), basis.size()); + + std::vector values; + for (std::size_t node = 0; node < nodes.size(); ++node) { + basis.evaluate_values(nodes[node], values); + ASSERT_EQ(values.size(), basis.size()); + for (std::size_t i = 0; i < values.size(); ++i) { + const double expected = (i == node) ? double(1) : double(0); + EXPECT_NEAR(values[i], expected, tol) + << "node " << node << ", basis " << i; + } + } +} + +void expect_partition_gradient_hessian_sums(const LagrangeBasis& basis, + const std::vector>& points, + double value_tol, + double derivative_tol) +{ + for (const auto& xi : points) { + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + double value_sum = double(0); + Gradient gradient_sum = Gradient::Zero(); + Hessian hessian_sum = Hessian::Zero(); + for (std::size_t i = 0; i < values.size(); ++i) { + value_sum += values[i]; + for (std::size_t d = 0; d < 3u; ++d) { + gradient_sum[d] += gradients[i][d]; + for (std::size_t e = 0; e < 3u; ++e) { + hessian_sum(d, e) += hessians[i](d, e); + } + } + } + + EXPECT_NEAR(value_sum, double(1), value_tol); + for (int d = 0; d < basis.dimension(); ++d) { + EXPECT_NEAR(gradient_sum[static_cast(d)], double(0), derivative_tol); + for (int e = 0; e < basis.dimension(); ++e) { + EXPECT_NEAR(hessian_sum(static_cast(d), + static_cast(e)), + double(0), + derivative_tol); + } + } + } +} + +void expect_all_entries_finite(const LagrangeBasis& basis, + const math::Vector& xi) +{ + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + for (std::size_t i = 0; i < values.size(); ++i) { + EXPECT_TRUE(std::isfinite(static_cast(values[i]))) << "value " << i; + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_TRUE(std::isfinite(static_cast(gradients[i][d]))) + << "gradient " << i << ", " << d; + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_TRUE(std::isfinite(static_cast(hessians[i](d, e)))) + << "hessian " << i << ", " << d << ", " << e; + } + } + } +} + +} // namespace + +TEST(HigherOrderWedge, CompleteAliasMatchesGeneratedNodeLayout) { + LagrangeBasis alias_basis(ElementType::Wedge18, 2); + const auto generated = + ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Wedge6, 2); + + ASSERT_EQ(generated.size(), ReferenceNodeLayout::num_nodes(ElementType::Wedge18)); + EXPECT_EQ(alias_basis.topology(), BasisTopology::Wedge); + EXPECT_EQ(named_element_for(alias_basis.topology(), alias_basis.order(), alias_basis.basis_type()), + ElementType::Wedge18); // faithful round-trip + EXPECT_EQ(alias_basis.order(), 2); + expect_nodes_close(alias_basis.nodes(), generated, double(1e-14)); +} + +TEST(HigherOrderWedge, OrderThreeIsNodalAndPartitionsUnity) { + LagrangeBasis wedge(BasisTopology::Wedge, 3); + + expect_kronecker_at_nodes(wedge, double(2e-10)); + expect_partition_gradient_hessian_sums( + wedge, + { + {double(0.18), double(0.22), double(-0.2)}, + {double(0.12), double(0.16), double(0.1)}, + {double(0.25), double(0.15), double(0.45)}, + }, + double(1e-12), + double(1e-9)); +} + +TEST(HigherOrderWedge, OrderFourEvaluationsRemainFinite) { + LagrangeBasis wedge(BasisTopology::Wedge, 4); + + expect_all_entries_finite(wedge, {double(0.2), double(0.1), double(-0.6)}); + expect_all_entries_finite(wedge, {double(0.05), double(0.8), double(0.3)}); +} + +// Finiteness alone cannot detect a wrong triangle-index or axis-index lookup; +// the Kronecker property validates the order-four node lattice and its inverse +// index mapping end to end. +TEST(HigherOrderWedge, OrderFourIsNodalAndPartitionsUnity) { + LagrangeBasis wedge(BasisTopology::Wedge, 4); + + // Order-4 wedge = triangle(order 4) x line(order 4) = 15 x 5 nodes. + EXPECT_EQ(wedge.size(), 15u * 5u); + expect_kronecker_at_nodes(wedge, double(1e-9)); + expect_partition_gradient_hessian_sums( + wedge, + { + {double(0.18), double(0.22), double(-0.2)}, + {double(0.25), double(0.15), double(0.45)}, + }, + double(1e-12), + double(1e-7)); +} diff --git a/tests/unitTests/FE/Basis/test_LagrangeBasis.cpp b/tests/unitTests/FE/Basis/test_LagrangeBasis.cpp new file mode 100644 index 000000000..48c5897a1 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_LagrangeBasis.cpp @@ -0,0 +1,892 @@ +/** + * @file test_LagrangeBasis.cpp + * @brief Unit tests for the reduced scalar Lagrange basis implementation. + */ + +#include + +#include "FE/Basis/BasisExceptions.h" +#include "FE/Basis/BasisFactory.h" +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/NodeOrderingConventions.h" + +#include +#include +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +using Point = math::Vector; + +// Tolerance convention (set from measured residuals, not guessed). Identities that +// share a generator -- node coordinates, the lattice forward-image, span vs vector +// evaluation, P0 constants -- are exact up to round-off and use 1e-14. The +// nodal/partition identities here -- the Kronecker delta at the nodes and the +// value/gradient/Hessian partition sums (sum_i N_i = 1, sum_i grad N_i = 0, +// sum_i Hess N_i = 0) -- are exact at every order (at a node an off-diagonal shape +// function has an exactly-zero factor, and the partition sums differentiate the +// constant 1), so one tolerance covers them all instead of an order-dependent +// ladder. Measured residuals stay near round-off (~7e-15 through the orders here). +constexpr double kPartitionTol = double(1e-12); + +struct CanonicalCase { + BasisTopology topology; + ElementType representative; // linear element for sample-point lookup and labeling + int order; + std::size_t size; + int dimension; + std::vector points; +}; + +const std::vector& canonical_cases() { + static const std::vector cases = { + {BasisTopology::Line, ElementType::Line2, 3, 4u, 1, + {{double(-0.35), double(0), double(0)}, {double(0.2), double(0), double(0)}}}, + {BasisTopology::Triangle, ElementType::Triangle3, 3, 10u, 2, + {{double(0.15), double(0.2), double(0)}, {double(0.25), double(0.1), double(0)}}}, + {BasisTopology::Quadrilateral, ElementType::Quad4, 3, 16u, 2, + {{double(0.2), double(-0.3), double(0)}, {double(-0.45), double(0.25), double(0)}}}, + {BasisTopology::Tetrahedron, ElementType::Tetra4, 2, 10u, 3, + {{double(0.12), double(0.18), double(0.16)}, {double(0.2), double(0.1), double(0.18)}}}, + {BasisTopology::Hexahedron, ElementType::Hex8, 2, 27u, 3, + {{double(0.1), double(-0.2), double(0.3)}, {double(-0.35), double(0.25), double(-0.15)}}}, + {BasisTopology::Wedge, ElementType::Wedge6, 2, 18u, 3, + {{double(0.18), double(0.22), double(-0.2)}, {double(0.12), double(0.16), double(0.1)}}}, + }; + return cases; +} + +std::vector sample_points_for(ElementType representative) { + for (const auto& c : canonical_cases()) { + if (c.representative == representative) { + return c.points; + } + } + return {}; +} + +void expect_kronecker_at_nodes(const LagrangeBasis& basis, double tol) +{ + const auto& nodes = basis.nodes(); + ASSERT_EQ(nodes.size(), basis.size()); + + std::vector values; + for (std::size_t node = 0; node < nodes.size(); ++node) { + basis.evaluate_values(nodes[node], values); + ASSERT_EQ(values.size(), basis.size()); + for (std::size_t i = 0; i < values.size(); ++i) { + EXPECT_NEAR(values[i], i == node ? double(1) : double(0), tol) + << "node=" << node << " basis=" << i; + } + } +} + +void expect_partition_gradient_hessian_sums(const LagrangeBasis& basis, + const std::vector& points) +{ + for (const auto& xi : points) { + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + double value_sum = double(0); + Gradient gradient_sum = Gradient::Zero(); + Hessian hessian_sum = Hessian::Zero(); + for (std::size_t i = 0; i < values.size(); ++i) { + value_sum += values[i]; + for (std::size_t d = 0; d < 3u; ++d) { + gradient_sum[d] += gradients[i][d]; + for (std::size_t e = 0; e < 3u; ++e) { + hessian_sum(d, e) += hessians[i](d, e); + } + } + } + + EXPECT_NEAR(value_sum, double(1), kPartitionTol); + for (int d = 0; d < basis.dimension(); ++d) { + EXPECT_NEAR(gradient_sum[static_cast(d)], double(0), kPartitionTol); + for (int e = 0; e < basis.dimension(); ++e) { + EXPECT_NEAR(hessian_sum(static_cast(d), + static_cast(e)), + double(0), + kPartitionTol); + } + } + } +} + +void expect_span_sinks_match_vector_evaluation(const LagrangeBasis& basis, + const Point& xi) +{ + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + std::vector span_values(basis.size()); + std::vector span_gradients(basis.size()); + std::vector span_hessians(basis.size()); + basis.evaluate_values_to(xi, span_values); + basis.evaluate_gradients_to(xi, span_gradients); + basis.evaluate_hessians_to(xi, span_hessians); + + for (std::size_t i = 0; i < basis.size(); ++i) { + EXPECT_NEAR(span_values[i], values[i], double(1e-14)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(span_gradients[i][d], gradients[i][d], double(1e-14)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(span_hessians[i](d, e), + hessians[i](d, e), + double(1e-14)); + } + } + } +} + +void expect_nodes_close(const std::vector& lhs, + const std::vector& rhs, + double tol) +{ + ASSERT_EQ(lhs.size(), rhs.size()); + for (std::size_t i = 0; i < lhs.size(); ++i) { + EXPECT_NEAR(lhs[i][0], rhs[i][0], tol) << "node=" << i; + EXPECT_NEAR(lhs[i][1], rhs[i][1], tol) << "node=" << i; + EXPECT_NEAR(lhs[i][2], rhs[i][2], tol) << "node=" << i; + } +} + +void expect_evaluations_match(const LagrangeBasis& lhs, + const LagrangeBasis& rhs, + const std::vector& points, + double tol) +{ + ASSERT_EQ(lhs.size(), rhs.size()); + + for (const auto& xi : points) { + std::vector lhs_values; + std::vector rhs_values; + std::vector lhs_gradients; + std::vector rhs_gradients; + std::vector lhs_hessians; + std::vector rhs_hessians; + + lhs.evaluate_all(xi, lhs_values, lhs_gradients, lhs_hessians); + rhs.evaluate_all(xi, rhs_values, rhs_gradients, rhs_hessians); + + for (std::size_t i = 0; i < lhs.size(); ++i) { + EXPECT_NEAR(lhs_values[i], rhs_values[i], tol); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(lhs_gradients[i][d], rhs_gradients[i][d], tol); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(lhs_hessians[i](d, e), rhs_hessians[i](d, e), tol); + } + } + } + } +} + +double linear_function(const Point& p) { + return double(2) + double(3) * p[0] - double(4) * p[1] + double(5) * p[2]; +} + +Gradient linear_gradient() { + Gradient g = Gradient::Zero(); + g[0] = double(3); + g[1] = double(-4); + g[2] = double(5); + return g; +} + +double quadratic_function(const Point& p) { + return double(1) + double(2) * p[0] - p[1] + double(0.5) * p[2] + + p[0] * p[0] + double(0.75) * p[1] * p[1] - double(0.25) * p[2] * p[2] + + double(0.2) * p[0] * p[1] - double(0.3) * p[0] * p[2] + + double(0.4) * p[1] * p[2]; +} + +// Total degree three, so it lies in both the P3 simplex space and the Q3 +// tensor-product space. +double cubic_function(const Point& p) { + return quadratic_function(p) + + double(0.1) * p[0] * p[0] * p[0] - + double(0.2) * p[1] * p[1] * p[1] + + double(0.3) * p[2] * p[2] * p[2] + + double(0.15) * p[0] * p[0] * p[1] - + double(0.12) * p[0] * p[2] * p[2] + + double(0.08) * p[0] * p[1] * p[2]; +} + +template +double interpolate_value(const LagrangeBasis& basis, + const std::vector& values, + Function&& nodal_function) +{ + double result = double(0); + const auto& nodes = basis.nodes(); + for (std::size_t i = 0; i < values.size(); ++i) { + result += values[i] * nodal_function(nodes[i]); + } + return result; +} + +} // namespace + +TEST(LagrangeBasis, CanonicalTopologiesHaveExpectedSizesAndDimensions) { + for (const auto& c : canonical_cases()) { + LagrangeBasis basis(c.topology, c.order); + EXPECT_EQ(basis.basis_type(), BasisType::Lagrange); + EXPECT_EQ(basis.topology(), c.topology); + EXPECT_EQ(basis.order(), c.order); + EXPECT_EQ(basis.size(), c.size); + EXPECT_EQ(basis.dimension(), c.dimension); + } +} + +TEST(LagrangeBasis, CanonicalTopologiesAreNodalAndPartitionUnity) { + for (const auto& c : canonical_cases()) { + LagrangeBasis basis(c.topology, c.order); + expect_kronecker_at_nodes(basis, kPartitionTol); + expect_partition_gradient_hessian_sums(basis, c.points); + } +} + +TEST(LagrangeBasis, SpanOutputSinksMatchVectorEvaluationAcrossTopologies) { + for (const auto& c : canonical_cases()) { + LagrangeBasis basis(c.topology, c.order); + expect_span_sinks_match_vector_evaluation(basis, c.points.front()); + } +} + +// A named quadratic alias is a fixed-order shorthand for the same topology at +// order 2: it builds the identical basis as the BasisTopology overload, reports +// that topology, and round-trips faithfully through named_element_for() (Hex27 +// stays Hex27 rather than collapsing to a canonical linear type). +TEST(LagrangeBasis, CompleteAliasesMatchTopologyConstruction) { + const std::vector> aliases = { + {ElementType::Line3, BasisTopology::Line, ElementType::Line2}, + {ElementType::Triangle6, BasisTopology::Triangle, ElementType::Triangle3}, + {ElementType::Quad9, BasisTopology::Quadrilateral, ElementType::Quad4}, + {ElementType::Tetra10, BasisTopology::Tetrahedron, ElementType::Tetra4}, + {ElementType::Hex27, BasisTopology::Hexahedron, ElementType::Hex8}, + {ElementType::Wedge18, BasisTopology::Wedge, ElementType::Wedge6}, + }; + + for (const auto& [alias, topo, representative] : aliases) { + LagrangeBasis alias_basis(alias, 2); + LagrangeBasis topo_basis(topo, 2); + const auto generated = ReferenceNodeLayout::get_lagrange_node_coords(representative, 2); + + EXPECT_EQ(alias_basis.topology(), topo); + EXPECT_EQ(named_element_for(alias_basis.topology(), alias_basis.order(), alias_basis.basis_type()), + alias); + EXPECT_EQ(alias_basis.order(), 2); + expect_nodes_close(alias_basis.nodes(), generated, double(1e-14)); + expect_nodes_close(alias_basis.nodes(), topo_basis.nodes(), double(1e-14)); + expect_evaluations_match(alias_basis, + topo_basis, + sample_points_for(representative), + double(1e-12)); + } +} + +// The arbitrary order a named alias rejects is exactly what the BasisTopology +// overload is for: a node-count-named element cannot carry a conflicting order, +// and an order with no named layout maps to named_element_for() == Unknown. +TEST(LagrangeBasis, ArbitraryOrderRequiresTopologyNotNamedAlias) { + EXPECT_THROW((void)LagrangeBasis(ElementType::Hex27, 3), BasisConfigurationException); + + const LagrangeBasis basis(BasisTopology::Hexahedron, 5); + EXPECT_EQ(basis.topology(), BasisTopology::Hexahedron); + EXPECT_EQ(basis.order(), 5); + EXPECT_EQ(basis.size(), 216u); // (5 + 1)^3 + EXPECT_EQ(named_element_for(basis.topology(), basis.order(), basis.basis_type()), + ElementType::Unknown); // no named order-5 hex +} + +// named_element_for() is the inverse of (topology, order): a named layout at +// orders 0-2, Unknown where no named element exists (order 0 on a volume +// topology, or any order >= 3). topology() + order() remain the authoritative +// identity; callers recover a named ElementType through this free helper. +TEST(LagrangeBasis, NamedElementForReflectsTopologyAndOrder) { + EXPECT_EQ(named_element_for(BasisTopology::Point, 0, BasisType::Lagrange), ElementType::Point1); + EXPECT_EQ(named_element_for(BasisTopology::Hexahedron, 1, BasisType::Lagrange), ElementType::Hex8); + EXPECT_EQ(named_element_for(BasisTopology::Hexahedron, 2, BasisType::Lagrange), ElementType::Hex27); + EXPECT_EQ(named_element_for(BasisTopology::Hexahedron, 3, BasisType::Lagrange), ElementType::Unknown); + EXPECT_EQ(named_element_for(BasisTopology::Tetrahedron, 1, BasisType::Lagrange), ElementType::Tetra4); + EXPECT_EQ(named_element_for(BasisTopology::Tetrahedron, 2, BasisType::Lagrange), ElementType::Tetra10); + EXPECT_EQ(named_element_for(BasisTopology::Quadrilateral, 2, BasisType::Lagrange), ElementType::Quad9); + // An order-0 P0 basis on a volume topology has no named element. + EXPECT_EQ(named_element_for(BasisTopology::Hexahedron, 0, BasisType::Lagrange), ElementType::Unknown); +} + +// The single-argument named overload infers the layout's baked-in order, so it +// builds the same basis as passing that order explicitly, and still rejects the +// non-Lagrange (serendipity/pyramid) layouts the two-argument overload rejects. +TEST(LagrangeBasis, SingleArgumentNamedOverloadInfersBakedOrder) { + const std::vector> named = { + {ElementType::Point1, 0}, + {ElementType::Line2, 1}, {ElementType::Line3, 2}, + {ElementType::Triangle3, 1}, {ElementType::Triangle6, 2}, + {ElementType::Quad4, 1}, {ElementType::Quad9, 2}, + {ElementType::Tetra4, 1}, {ElementType::Tetra10, 2}, + {ElementType::Hex8, 1}, {ElementType::Hex27, 2}, + {ElementType::Wedge6, 1}, {ElementType::Wedge18, 2}, + }; + for (const auto& [type, baked_order] : named) { + const LagrangeBasis inferred(type); + const LagrangeBasis explicit_order(type, baked_order); + EXPECT_EQ(inferred.order(), baked_order) << "type=" << static_cast(type); + EXPECT_EQ(inferred.topology(), explicit_order.topology()) << "type=" << static_cast(type); + EXPECT_EQ(inferred.size(), explicit_order.size()) << "type=" << static_cast(type); + } + + EXPECT_THROW((void)LagrangeBasis(ElementType::Quad8), BasisElementCompatibilityException); + EXPECT_THROW((void)LagrangeBasis(ElementType::Pyramid5), BasisElementCompatibilityException); +} + +TEST(LagrangeBasis, NodeOrderingMatchesPublicAliasLayouts) { + const std::vector> aliases = { + {ElementType::Line2, ElementType::Line2, 1}, + {ElementType::Line3, ElementType::Line2, 2}, + {ElementType::Triangle3, ElementType::Triangle3, 1}, + {ElementType::Triangle6, ElementType::Triangle3, 2}, + {ElementType::Quad4, ElementType::Quad4, 1}, + {ElementType::Quad9, ElementType::Quad4, 2}, + {ElementType::Tetra4, ElementType::Tetra4, 1}, + {ElementType::Tetra10, ElementType::Tetra4, 2}, + {ElementType::Hex8, ElementType::Hex8, 1}, + {ElementType::Hex27, ElementType::Hex8, 2}, + {ElementType::Wedge6, ElementType::Wedge6, 1}, + {ElementType::Wedge18, ElementType::Wedge6, 2}, + }; + + for (const auto& [alias, canonical, order] : aliases) { + const auto generated = ReferenceNodeLayout::get_lagrange_node_coords(canonical, order); + ASSERT_EQ(generated.size(), ReferenceNodeLayout::num_nodes(alias)); + + for (std::size_t i = 0; i < generated.size(); ++i) { + const auto public_node = ReferenceNodeLayout::node_coord_at(alias, i); + EXPECT_NEAR(public_node[0], generated[i][0], double(1e-14)) << "node=" << i; + EXPECT_NEAR(public_node[1], generated[i][1], double(1e-14)) << "node=" << i; + EXPECT_NEAR(public_node[2], generated[i][2], double(1e-14)) << "node=" << i; + } + } +} + +// The lattice emitted with each node must be the exact forward image of the +// coordinate: tensor axes invert through line_coord_pm_one, simplex axes through +// the [0, 1] equispaced map, and the wedge combines the two. This pins the +// integer-lattice contract that replaced the floating-point round-trip, so a +// generator that emitted a coordinate and a mismatched index would fail here. +TEST(LagrangeBasis, LatticeIsExactForwardImageOfCoordinates) { + constexpr double kTol = double(1e-14); + + const std::vector> tensor_cases = { + {ElementType::Line2, 1, 1}, {ElementType::Line2, 4, 1}, + {ElementType::Quad4, 1, 2}, {ElementType::Quad4, 4, 2}, + {ElementType::Hex8, 1, 3}, {ElementType::Hex8, 3, 3}, + }; + for (const auto& [type, order, dim] : tensor_cases) { + const auto layout = ReferenceNodeLayout::get_lagrange_lattice(type, order); + ASSERT_EQ(layout.coords.size(), layout.lattice.size()) + << "type=" << static_cast(type); + for (std::size_t n = 0; n < layout.coords.size(); ++n) { + for (int d = 0; d < dim; ++d) { + const auto sd = static_cast(d); + EXPECT_NEAR(layout.coords[n][sd], + line_coord_pm_one(layout.lattice[n][sd], order), kTol) + << "type=" << static_cast(type) << " node=" << n << " axis=" << d; + } + } + } + + const std::vector> simplex_cases = { + {ElementType::Triangle3, 1, 2}, {ElementType::Triangle3, 4, 2}, + {ElementType::Tetra4, 1, 3}, {ElementType::Tetra4, 4, 3}, + }; + for (const auto& [type, order, dim] : simplex_cases) { + const auto layout = ReferenceNodeLayout::get_lagrange_lattice(type, order); + ASSERT_EQ(layout.coords.size(), layout.lattice.size()) + << "type=" << static_cast(type); + for (std::size_t n = 0; n < layout.coords.size(); ++n) { + for (int d = 0; d < dim; ++d) { + const auto sd = static_cast(d); + EXPECT_NEAR(layout.coords[n][sd], + static_cast(layout.lattice[n][sd]) / + static_cast(order), + kTol) + << "type=" << static_cast(type) << " node=" << n << " axis=" << d; + } + } + } + + for (const int order : {1, 2, 3, 4}) { + const auto layout = + ReferenceNodeLayout::get_lagrange_lattice(ElementType::Wedge6, order); + ASSERT_EQ(layout.coords.size(), layout.lattice.size()) << "wedge order=" << order; + for (std::size_t n = 0; n < layout.coords.size(); ++n) { + // (x, y) are triangle [0, 1] indices; z inverts through line_coord_pm_one. + EXPECT_NEAR(layout.coords[n][0], + static_cast(layout.lattice[n][0]) / static_cast(order), + kTol) << "wedge order=" << order << " node=" << n; + EXPECT_NEAR(layout.coords[n][1], + static_cast(layout.lattice[n][1]) / static_cast(order), + kTol) << "wedge order=" << order << " node=" << n; + EXPECT_NEAR(layout.coords[n][2], + line_coord_pm_one(layout.lattice[n][2], order), kTol) + << "wedge order=" << order << " node=" << n; + } + } +} + +TEST(LagrangeBasis, RemovedOrSerendipityFamiliesAreRejected) { + const std::array unsupported = { + ElementType::Quad8, + ElementType::Hex20, + ElementType::Wedge15, + ElementType::Pyramid5, + ElementType::Pyramid13, + ElementType::Pyramid14, + }; + + for (const auto type : unsupported) { + EXPECT_THROW((void)LagrangeBasis(type, 2), BasisElementCompatibilityException) + << "element=" << static_cast(type); + } +} + +// The polynomial-reproduction and higher-order-lattice tests here validate +// VALUES and derivative invariants (gradient/Hessian sums). The authoritative +// finite-difference checks of gradient and Hessian *values* live in +// test_BasisHessians.cpp (BasisGradients/BasisHessians suites), covering the +// canonical Lagrange topologies and the serendipity families. +TEST(LagrangeBasis, LinearPolynomialReproductionAcrossLinearTopologies) { + const std::vector> cases = { + {ElementType::Line2, {double(-0.2), double(0), double(0)}}, + {ElementType::Triangle3, {double(0.2), double(0.3), double(0)}}, + {ElementType::Quad4, {double(0.25), double(-0.4), double(0)}}, + {ElementType::Tetra4, {double(0.1), double(0.2), double(0.3)}}, + {ElementType::Hex8, {double(0.15), double(-0.2), double(0.25)}}, + {ElementType::Wedge6, {double(0.2), double(0.15), double(-0.3)}}, + }; + const Gradient expected_gradient = linear_gradient(); + + for (const auto& [type, point] : cases) { + LagrangeBasis basis(type, 1); + std::vector values; + std::vector gradients; + basis.evaluate_values(point, values); + basis.evaluate_gradients(point, gradients); + + const double interpolated = + interpolate_value(basis, values, linear_function); + EXPECT_NEAR(interpolated, linear_function(point), double(1e-12)); + + Gradient interpolated_gradient = Gradient::Zero(); + for (std::size_t i = 0; i < gradients.size(); ++i) { + const double nodal_value = linear_function(basis.nodes()[i]); + for (int d = 0; d < basis.dimension(); ++d) { + interpolated_gradient[static_cast(d)] += + nodal_value * gradients[i][static_cast(d)]; + } + } + for (int d = 0; d < basis.dimension(); ++d) { + EXPECT_NEAR(interpolated_gradient[static_cast(d)], + expected_gradient[static_cast(d)], + double(1e-12)); + } + } +} + +TEST(LagrangeBasis, QuadraticPolynomialReproductionAcrossQuadraticAliases) { + const std::vector> cases = { + {ElementType::Line3, {double(-0.2), double(0), double(0)}}, + {ElementType::Triangle6, {double(0.2), double(0.3), double(0)}}, + {ElementType::Quad9, {double(0.25), double(-0.4), double(0)}}, + {ElementType::Tetra10, {double(0.1), double(0.2), double(0.3)}}, + {ElementType::Hex27, {double(0.15), double(-0.2), double(0.25)}}, + {ElementType::Wedge18, {double(0.2), double(0.15), double(-0.3)}}, + }; + + for (const auto& [type, point] : cases) { + LagrangeBasis basis(type, 2); + std::vector values; + basis.evaluate_values(point, values); + + const double interpolated = + interpolate_value(basis, values, quadratic_function); + EXPECT_NEAR(interpolated, quadratic_function(point), double(5e-12)) + << "element=" << static_cast(type); + } +} + +// Tetra order >= 3 activates the face-interior node loops, tetra order >= 4 +// activates the volume-interior lattice, and hex order >= 3 activates the six +// orientation-specific face traversals in NodeOrderingConventions. None of +// those generation paths run at the orders covered elsewhere; the Kronecker +// test is what validates the node lattice together with the integer +// lattice-index mapping the basis builds from it (a duplicated or missing +// node makes the basis non-nodal here). +TEST(LagrangeBasis, HigherOrderLatticesAreNodalAndPartitionUnity) { + const struct Case { + BasisTopology topology; + int order; + std::size_t size; + std::vector points; + } cases[] = { + {BasisTopology::Tetrahedron, 3, 20u, + {{double(0.12), double(0.18), double(0.16)}, {double(0.3), double(0.2), double(0.25)}}}, + {BasisTopology::Tetrahedron, 4, 35u, + {{double(0.12), double(0.18), double(0.16)}, {double(0.2), double(0.1), double(0.18)}}}, + {BasisTopology::Hexahedron, 3, 64u, + {{double(0.1), double(-0.2), double(0.3)}, {double(-0.35), double(0.25), double(-0.15)}}}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.topology, c.order); + EXPECT_EQ(basis.size(), c.size); + expect_kronecker_at_nodes(basis, kPartitionTol); + expect_partition_gradient_hessian_sums(basis, c.points); + } +} + +// The Kronecker test above proves the order-3 hex lattice is nodal, but a +// permuted-yet-consistent face ordering would also pass it. This pins the +// load-bearing external contract of the order>=3 face-interior emission: the +// six face-interior blocks appear in VTK face order (-X, +X, -Y, +Y, -Z, +Z) +// and lie on the correct face. (The within-face traversal is an internal +// convention and is not separately pinned here.) +TEST(LagrangeBasis, HigherOrderHexFaceInteriorFollowsVtkFaceOrder) { + // Order-3 hex (64 nodes): 8 vertices + 24 edge nodes + 24 face-interior + // (6 faces x (order-1)^2 = 4 each) + 8 volume. Face-interior block at [32, 56). + const auto nodes = ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Hex8, 3); + ASSERT_EQ(nodes.size(), 64u); + + struct FaceBlock { + std::size_t axis; // constant axis: 0=x, 1=y, 2=z + double value; // constant coordinate on the face + }; + const FaceBlock blocks[] = { + {0u, double(-1)}, // -X + {0u, double(1)}, // +X + {1u, double(-1)}, // -Y + {1u, double(1)}, // +Y + {2u, double(-1)}, // -Z + {2u, double(1)}, // +Z + }; + + constexpr std::size_t kFaceStart = 32u; + constexpr std::size_t kPerFace = 4u; // (order-1)^2 at order 3 + for (std::size_t f = 0; f < 6u; ++f) { + for (std::size_t m = 0; m < kPerFace; ++m) { + const auto& node = nodes[kFaceStart + f * kPerFace + m]; + EXPECT_NEAR(node[blocks[f].axis], blocks[f].value, double(1e-14)) + << "face block " << f << ", node " << m; + } + } +} + +TEST(LagrangeBasis, CubicPolynomialReproductionAtOrderThree) { + const std::vector> cases = { + {BasisTopology::Tetrahedron, {double(0.15), double(0.2), double(0.25)}}, + {BasisTopology::Hexahedron, {double(0.15), double(-0.2), double(0.25)}}, + }; + + for (const auto& [topo, point] : cases) { + LagrangeBasis basis(topo, 3); + std::vector values; + basis.evaluate_values(point, values); + + const double interpolated = interpolate_value(basis, values, cubic_function); + EXPECT_NEAR(interpolated, cubic_function(point), double(1e-10)) + << "topology=" << static_cast(topo); + } +} + +TEST(LagrangeBasis, PointTopologyEvaluatesConstantUnity) { + LagrangeBasis basis(ElementType::Point1, 0); + + EXPECT_EQ(named_element_for(basis.topology(), basis.order(), basis.basis_type()), + ElementType::Point1); + EXPECT_EQ(basis.size(), 1u); + EXPECT_EQ(basis.dimension(), 0); + ASSERT_EQ(basis.nodes().size(), 1u); + + const Point xi{double(0.3), double(-0.4), double(0.1)}; + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + ASSERT_EQ(values.size(), 1u); + EXPECT_EQ(values[0], double(1)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(gradients[0][d], double(0)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_EQ(hessians[0](d, e), double(0)); + } + } + + double span_value = double(-1); + Gradient span_gradient; + span_gradient[0] = span_gradient[1] = span_gradient[2] = double(-1); + Hessian span_hessian; + for (std::size_t d = 0; d < 3u; ++d) { + for (std::size_t e = 0; e < 3u; ++e) { + span_hessian(d, e) = double(-1); + } + } + basis.evaluate_values_to(xi, std::span(&span_value, 1u)); + basis.evaluate_gradients_to(xi, std::span(&span_gradient, 1u)); + basis.evaluate_hessians_to(xi, std::span(&span_hessian, 1u)); + EXPECT_EQ(span_value, double(1)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(span_gradient[d], double(0)); + } + for (std::size_t d = 0; d < 3u; ++d) { + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_EQ(span_hessian(d, e), double(0)); + } + } +} + +// P0 bases back piecewise-constant fields (e.g. pressure in mixed elements); +// the order-zero branches in node generation and the simplex/tensor/wedge +// evaluators have no other coverage. +TEST(LagrangeBasis, OrderZeroBasesAreConstantUnity) { + const std::array, 6> cases = {{ + {BasisTopology::Line, ElementType::Line2}, + {BasisTopology::Triangle, ElementType::Triangle3}, + {BasisTopology::Quadrilateral, ElementType::Quad4}, + {BasisTopology::Tetrahedron, ElementType::Tetra4}, + {BasisTopology::Hexahedron, ElementType::Hex8}, + {BasisTopology::Wedge, ElementType::Wedge6}, + }}; + + for (const auto& [topo, representative] : cases) { + LagrangeBasis basis(topo, 0); + EXPECT_EQ(basis.order(), 0) << "topology=" << static_cast(topo); + EXPECT_EQ(basis.size(), 1u) << "topology=" << static_cast(topo); + + for (const auto& xi : sample_points_for(representative)) { + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + ASSERT_EQ(values.size(), 1u); + EXPECT_NEAR(values[0], double(1), double(1e-14)) + << "topology=" << static_cast(topo); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(gradients[0][d], double(0), double(1e-14)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(hessians[0](d, e), double(0), double(1e-14)); + } + } + } + } +} + +// Pins the default basis selection for every supported element type. The +// solver adapter (nn.cpp) translates solver element names to ElementType and +// delegates the family/order choice to default_basis_request; a silent change +// here would change the discretization of every simulation using that element. +TEST(BasisFactoryDefaults, SelectionsArePinnedForAllSupportedElements) { + struct Expected { + ElementType type; + BasisType family; + int order; + std::size_t size; + }; + const std::vector cases = { + {ElementType::Point1, BasisType::Lagrange, 0, 1u}, + {ElementType::Line2, BasisType::Lagrange, 1, 2u}, + {ElementType::Line3, BasisType::Lagrange, 2, 3u}, + {ElementType::Triangle3, BasisType::Lagrange, 1, 3u}, + {ElementType::Triangle6, BasisType::Lagrange, 2, 6u}, + {ElementType::Quad4, BasisType::Lagrange, 1, 4u}, + {ElementType::Quad8, BasisType::Serendipity, 2, 8u}, + {ElementType::Quad9, BasisType::Lagrange, 2, 9u}, + {ElementType::Tetra4, BasisType::Lagrange, 1, 4u}, + {ElementType::Tetra10, BasisType::Lagrange, 2, 10u}, + {ElementType::Hex8, BasisType::Lagrange, 1, 8u}, + {ElementType::Hex20, BasisType::Serendipity, 2, 20u}, + {ElementType::Hex27, BasisType::Lagrange, 2, 27u}, + {ElementType::Wedge6, BasisType::Lagrange, 1, 6u}, + {ElementType::Wedge15, BasisType::Serendipity, 2, 15u}, + {ElementType::Wedge18, BasisType::Lagrange, 2, 18u}, + }; + + for (const auto& expected : cases) { + const auto request = basis_factory::default_basis_request(expected.type); + EXPECT_EQ(request.element_type, expected.type) + << "element=" << static_cast(expected.type); + EXPECT_EQ(request.basis_type, expected.family) + << "element=" << static_cast(expected.type); + ASSERT_TRUE(request.order.has_value()) + << "element=" << static_cast(expected.type); + EXPECT_EQ(*request.order, expected.order) + << "element=" << static_cast(expected.type); + + auto basis = basis_factory::create_default_for(expected.type); + ASSERT_NE(basis, nullptr); + EXPECT_EQ(basis->basis_type(), expected.family) + << "element=" << static_cast(expected.type); + EXPECT_EQ(basis->order(), expected.order) + << "element=" << static_cast(expected.type); + EXPECT_EQ(basis->size(), expected.size) + << "element=" << static_cast(expected.type); + } +} + +TEST(BasisFactoryDefaults, RejectsElementsWithoutDefaultBasis) { + EXPECT_THROW((void)basis_factory::default_basis_request(ElementType::Pyramid5), + BasisElementCompatibilityException); + EXPECT_THROW((void)basis_factory::default_basis_request(ElementType::Pyramid13), + BasisElementCompatibilityException); + EXPECT_THROW((void)basis_factory::create_default_for(ElementType::Unknown), + BasisElementCompatibilityException); +} + +// The factory-creation paths below were one 90-line test; they are split by the +// path under test so an early ASSERT_NE in one cannot mask the others, and each +// name says what it covers. +TEST(LagrangeBasis, FactoryCreatesNamedLagrangeBasis) { + auto lagrange = + basis_factory::create(BasisRequest{ElementType::Hex27, BasisType::Lagrange, 2}); + ASSERT_NE(lagrange, nullptr); + EXPECT_EQ(lagrange->basis_type(), BasisType::Lagrange); + EXPECT_EQ(lagrange->topology(), BasisTopology::Hexahedron); + EXPECT_EQ(named_element_for(lagrange->topology(), lagrange->order(), lagrange->basis_type()), + ElementType::Hex27); + EXPECT_EQ(lagrange->order(), 2); + + // The factory inherits the named-element order validation: Hex27 is order 2. + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Hex27, BasisType::Lagrange, 1}), + BasisConfigurationException); +} + +TEST(LagrangeBasis, FactoryCreatesArbitraryOrderLagrangeBasis) { + BasisRequest arbitrary_lagrange; + arbitrary_lagrange.basis_type = BasisType::Lagrange; + arbitrary_lagrange.order = 5; + arbitrary_lagrange.topology = BasisTopology::Hexahedron; + auto high_order_lagrange = basis_factory::create(arbitrary_lagrange); + ASSERT_NE(high_order_lagrange, nullptr); + EXPECT_EQ(high_order_lagrange->basis_type(), BasisType::Lagrange); + EXPECT_EQ(high_order_lagrange->topology(), BasisTopology::Hexahedron); + EXPECT_EQ(named_element_for(high_order_lagrange->topology(), high_order_lagrange->order(), + high_order_lagrange->basis_type()), + ElementType::Unknown); + EXPECT_EQ(high_order_lagrange->order(), 5); + EXPECT_EQ(high_order_lagrange->size(), 216u); +} + +TEST(LagrangeBasis, FactoryCreatesNamedSerendipityBasis) { + auto serendipity = + basis_factory::create(BasisRequest{ElementType::Quad8, BasisType::Serendipity, 2}); + ASSERT_NE(serendipity, nullptr); + EXPECT_EQ(serendipity->basis_type(), BasisType::Serendipity); +} + +TEST(LagrangeBasis, FactoryCreatesArbitraryOrderQuadSerendipityBasis) { + BasisRequest arbitrary_quad_serendipity; + arbitrary_quad_serendipity.basis_type = BasisType::Serendipity; + arbitrary_quad_serendipity.order = 4; + arbitrary_quad_serendipity.topology = BasisTopology::Quadrilateral; + auto high_order_quad_serendipity = basis_factory::create(arbitrary_quad_serendipity); + ASSERT_NE(high_order_quad_serendipity, nullptr); + EXPECT_EQ(high_order_quad_serendipity->basis_type(), BasisType::Serendipity); + EXPECT_EQ(high_order_quad_serendipity->topology(), BasisTopology::Quadrilateral); + EXPECT_EQ(named_element_for(high_order_quad_serendipity->topology(), + high_order_quad_serendipity->order(), + high_order_quad_serendipity->basis_type()), + ElementType::Unknown); + EXPECT_EQ(high_order_quad_serendipity->order(), 4); + EXPECT_EQ(high_order_quad_serendipity->size(), 17u); +} + +TEST(LagrangeBasis, FactoryCreatesArbitraryOrderHexSerendipityBasis) { + BasisRequest arbitrary_hex_serendipity; + arbitrary_hex_serendipity.basis_type = BasisType::Serendipity; + arbitrary_hex_serendipity.order = 3; + arbitrary_hex_serendipity.topology = BasisTopology::Hexahedron; + auto high_order_hex_serendipity = basis_factory::create(arbitrary_hex_serendipity); + ASSERT_NE(high_order_hex_serendipity, nullptr); + EXPECT_EQ(high_order_hex_serendipity->basis_type(), BasisType::Serendipity); + EXPECT_EQ(high_order_hex_serendipity->topology(), BasisTopology::Hexahedron); + EXPECT_EQ(named_element_for(high_order_hex_serendipity->topology(), + high_order_hex_serendipity->order(), + high_order_hex_serendipity->basis_type()), + ElementType::Unknown); + EXPECT_EQ(high_order_hex_serendipity->order(), 3); + EXPECT_EQ(high_order_hex_serendipity->size(), 32u); +} + +TEST(LagrangeBasis, FactoryRejectsInvalidScalarRequests) { + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Pyramid5, BasisType::Lagrange, 1}), + BasisElementCompatibilityException); + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Pyramid13, BasisType::Serendipity, 2}), + BasisElementCompatibilityException); + + BasisRequest ambiguous; + ambiguous.element_type = ElementType::Hex27; + ambiguous.basis_type = BasisType::Lagrange; + ambiguous.order = 2; + ambiguous.topology = BasisTopology::Hexahedron; + EXPECT_THROW((void)basis_factory::create(ambiguous), BasisConfigurationException); + + BasisRequest missing_target; + missing_target.basis_type = BasisType::Lagrange; + missing_target.order = 2; + EXPECT_THROW((void)basis_factory::create(missing_target), BasisConfigurationException); + + BasisRequest unsupported_serendipity_topology; + unsupported_serendipity_topology.basis_type = BasisType::Serendipity; + unsupported_serendipity_topology.order = 2; + unsupported_serendipity_topology.topology = BasisTopology::Wedge; + EXPECT_THROW((void)basis_factory::create(unsupported_serendipity_topology), + BasisElementCompatibilityException); +} + +// The shared 1D tensor-axis distribution (line_coord_pm_one) is Gauss-Lobatto- +// Legendre: endpoints exactly +/-1, symmetric about 0, strictly increasing, and +// coincident with the equispaced layout at the production orders 1 and 2. +TEST(LagrangeBasis, TensorAxisNodesAreGaussLobattoLegendre) { + // Orders 1 and 2 coincide with equispaced (the production layouts). + EXPECT_EQ(line_coord_pm_one(0, 1), double(-1)); + EXPECT_EQ(line_coord_pm_one(1, 1), double(1)); + EXPECT_EQ(line_coord_pm_one(0, 2), double(-1)); + EXPECT_EQ(line_coord_pm_one(1, 2), double(0)); + EXPECT_EQ(line_coord_pm_one(2, 2), double(1)); + + for (int order = 1; order <= 12; ++order) { + EXPECT_EQ(line_coord_pm_one(0, order), double(-1)) << "order=" << order; + EXPECT_EQ(line_coord_pm_one(order, order), double(1)) << "order=" << order; + + double previous = line_coord_pm_one(0, order); + for (int i = 1; i <= order; ++i) { + const double x = line_coord_pm_one(i, order); + EXPECT_GT(x, previous) << "order=" << order << " i=" << i; // strictly increasing + EXPECT_LE(x, double(1)) << "order=" << order << " i=" << i; + EXPECT_GE(x, double(-1)) << "order=" << order << " i=" << i; + EXPECT_NEAR(x, -line_coord_pm_one(order - i, order), double(1e-14)) + << "order=" << order << " i=" << i; // symmetric about 0 + previous = x; + } + } + + // For order >= 3 GLL differs from equispaced: the interior nodes cluster toward + // the endpoints. At order 4 the first interior node is -sqrt(3/7) ~ -0.6547, + // past the equispaced position -0.5. + EXPECT_LT(line_coord_pm_one(1, 4), double(-0.5) - double(1e-6)); +} diff --git a/tests/unitTests/FE/Basis/test_SerendipityBasis.cpp b/tests/unitTests/FE/Basis/test_SerendipityBasis.cpp new file mode 100644 index 000000000..b764c8a8a --- /dev/null +++ b/tests/unitTests/FE/Basis/test_SerendipityBasis.cpp @@ -0,0 +1,1179 @@ +/** + * @file test_SerendipityBasis.cpp + * @brief Nodal-delta, partition-of-unity, and polynomial-reproduction tests for SerendipityBasis. + */ + +#include + +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/NodeOrderingConventions.h" +#include "FE/Basis/SerendipityBasis.h" +#include "FE/Math/DenseLinearAlgebra.h" + +#include +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +void expect_partition_of_unity(const SerendipityBasis& basis, + const math::Vector& xi, + double tolerance = double(1e-10)) +{ + std::vector values; + std::vector gradients; + basis.evaluate_values(xi, values); + basis.evaluate_gradients(xi, gradients); + + double value_sum = double(0); + Gradient gradient_sum = Gradient::Zero(); + for (std::size_t i = 0; i < values.size(); ++i) { + value_sum += values[i]; + for (std::size_t component = 0; component < 3u; ++component) { + gradient_sum[component] += gradients[i][component]; + } + } + + EXPECT_NEAR(value_sum, double(1), tolerance); + for (int component = 0; component < basis.dimension(); ++component) { + EXPECT_NEAR(gradient_sum[static_cast(component)], + double(0), + tolerance); + } +} + +void expect_nodal_delta(const SerendipityBasis& basis, + const std::vector>& nodes, + double tolerance) +{ + ASSERT_EQ(nodes.size(), basis.size()); + for (std::size_t node = 0; node < nodes.size(); ++node) { + std::vector values; + basis.evaluate_values(nodes[node], values); + ASSERT_EQ(values.size(), basis.size()); + for (std::size_t dof = 0; dof < values.size(); ++dof) { + EXPECT_NEAR(values[dof], dof == node ? double(1) : double(0), tolerance) + << "node=" << node << " dof=" << dof; + } + } +} + +std::vector> reference_nodes(ElementType type, + std::size_t count) +{ + std::vector> nodes; + nodes.reserve(count); + for (std::size_t i = 0; i < count; ++i) { + nodes.push_back(ReferenceNodeLayout::node_coord_at(type, i)); + } + return nodes; +} + +template +double interpolate_nodal_function(const SerendipityBasis& basis, + const math::Vector& xi, + Function&& nodal_function) +{ + std::vector values; + basis.evaluate_values(xi, values); + + double result = double(0); + const auto& nodes = basis.nodes(); + for (std::size_t i = 0; i < values.size(); ++i) { + result += values[i] * nodal_function(nodes[i]); + } + return result; +} + +// The _for_test helpers below intentionally re-derive the production monomial +// selection (superlinear-degree rule, exponent enumeration, and size formula) +// independently of SerendipityBasis, so the basis is checked against an external +// oracle rather than against its own code. If the production formula in +// SerendipityBasis.cpp is changed deliberately, update these copies to match; an +// accidental drift between the two is meant to surface here as a test failure. +int quad_serendipity_superlinear_degree_for_test(int ax, int ay) { + return (ax > 1 ? ax : 0) + (ay > 1 ? ay : 0); +} + +std::vector> quad_serendipity_exponents_for_test(int order) { + std::vector> exponents; + for (int ay = 0; ay <= order; ++ay) { + for (int ax = 0; ax <= order; ++ax) { + if (quad_serendipity_superlinear_degree_for_test(ax, ay) <= order) { + exponents.push_back({ax, ay}); + } + } + } + return exponents; +} + +std::size_t expected_quad_serendipity_size(int order) { + const auto p = static_cast(order); + const std::size_t boundary = 4u * p; + if (order < 4) { + return boundary; + } + const auto m = static_cast(order - 4); + return boundary + (m + 1u) * (m + 2u) / 2u; +} + +double integer_power_for_test(double base, int exponent) { + double result = double(1); + for (int k = 0; k < exponent; ++k) { + result *= base; + } + return result; +} + +double monomial_value_for_test(const math::Vector& p, + const std::array& exponent) { + return integer_power_for_test(p[0], exponent[0]) * + integer_power_for_test(p[1], exponent[1]); +} + +std::vector quadrilateral_vandermonde_for_test( + const std::vector>& nodes, + const std::vector>& exponents) +{ + const std::size_t n = nodes.size(); + std::vector vandermonde(n * n, double(0)); + for (std::size_t row = 0; row < n; ++row) { + for (std::size_t col = 0; col < n; ++col) { + vandermonde[row * n + col] = + monomial_value_for_test(nodes[row], exponents[col]); + } + } + return vandermonde; +} + +void expect_no_duplicate_nodes(const std::vector>& nodes, + double tolerance) +{ + for (std::size_t a = 0; a < nodes.size(); ++a) { + for (std::size_t b = a + 1u; b < nodes.size(); ++b) { + const double dx = std::abs(nodes[a][0] - nodes[b][0]); + const double dy = std::abs(nodes[a][1] - nodes[b][1]); + EXPECT_GT(std::max(dx, dy), tolerance) + << "duplicate nodes " << a << " and " << b; + } + } +} + +void expect_nodes_near(const std::vector>& actual, + const std::vector>& expected, + double tolerance) +{ + ASSERT_EQ(actual.size(), expected.size()); + for (std::size_t i = 0; i < actual.size(); ++i) { + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(actual[i][d], expected[i][d], tolerance) + << "node=" << i << " component=" << d; + } + } +} + +// Every monomial here has superlinear degree at most three, so it lies in the +// order-three quadrilateral serendipity space. +double cubic_serendipity_function(const math::Vector& p) { + const double x = p[0]; + const double y = p[1]; + return double(1) + double(2) * x - y + double(3) * x * y + + x * x * x - double(2) * y * y * y + + double(0.5) * x * x * x * y - double(0.25) * x * y * y * y; +} + +double bilinear_function(const math::Vector& p) { + return double(2) - double(3) * p[0] + double(4) * p[1] + double(0.5) * p[0] * p[1]; +} + +// --- 3D serendipity guard-test helpers (Hex20, Wedge15) -------------------- +// +// Like the quadrilateral _for_test helpers above, these re-derive the monomial +// selection and reference-node placement from the mathematical definition of +// each serendipity space, independently of the production tables in +// SerendipityBasis.cpp + +double monomial_value_3d_for_test(const math::Vector& p, + const std::array& exponent) { + return integer_power_for_test(p[0], exponent[0]) * + integer_power_for_test(p[1], exponent[1]) * + integer_power_for_test(p[2], exponent[2]); +} + +std::vector vandermonde_3d_for_test( + const std::vector>& nodes, + const std::vector>& exponents) { + const std::size_t n = nodes.size(); + std::vector vandermonde(n * n, double(0)); + for (std::size_t row = 0; row < n; ++row) { + for (std::size_t col = 0; col < n; ++col) { + vandermonde[row * n + col] = + monomial_value_3d_for_test(nodes[row], exponents[col]); + } + } + return vandermonde; +} + +// Superlinear degree generalized to three axes (the quadrilateral rule extended +// to t). An exponent contributes only when it exceeds one. +int superlinear_degree_3d_for_test(int ax, int ay, int az) { + return (ax > 1 ? ax : 0) + (ay > 1 ? ay : 0) + (az > 1 ? az : 0); +} + +// Hex20 serendipity span: every (ax, ay, az) in {0,1,2}^3 with superlinear +// degree at most two. +std::vector> hex20_serendipity_exponents_for_test() { + std::vector> exponents; + for (int ax = 0; ax <= 2; ++ax) { + for (int ay = 0; ay <= 2; ++ay) { + for (int az = 0; az <= 2; ++az) { + if (superlinear_degree_3d_for_test(ax, ay, az) <= 2) { + exponents.push_back({ax, ay, az}); + } + } + } + } + return exponents; +} + +// Arbitrary-order hexahedral serendipity verification +std::vector> hex_serendipity_exponents_for_test(int order) { + std::vector> exponents; + for (int az = 0; az <= order; ++az) { + for (int ay = 0; ay <= order; ++ay) { + for (int ax = 0; ax <= order; ++ax) { + if (superlinear_degree_3d_for_test(ax, ay, az) <= order) { + exponents.push_back({ax, ay, az}); + } + } + } + } + return exponents; +} + +std::size_t quad_serendipity_interior_count_for_test(int order) { + if (order < 4) { + return 0u; + } + const auto m = static_cast(order - 4); + return (m + 1u) * (m + 2u) / 2u; +} + +std::size_t hex_serendipity_volume_interior_count_for_test(int order) { + if (order < 6) { + return 0u; + } + const auto m = static_cast(order - 6); + return (m + 1u) * (m + 2u) * (m + 3u) / 6u; +} + +// dim S_p from the node strata: 8 corners, 12 (p - 1) edge nodes, 6 q(p) face +// interiors, and the P_{p-6} volume residual. +std::size_t expected_hex_serendipity_size(int order) { + const auto p = static_cast(order); + return 8u + 12u * (p - 1u) + + 6u * quad_serendipity_interior_count_for_test(order) + + hex_serendipity_volume_interior_count_for_test(order); +} + +// Wedge15 serendipity span: triangle monomials (ax, ay) with ax + ay <= 2, +// tensored with the through-axis. Linear triangle monomials (ax + ay <= 1) carry +// t-degree up to two; quadratic triangle monomials (ax + ay == 2) carry t-degree +// up to one. +std::vector> wedge15_serendipity_exponents_for_test() { + std::vector> exponents; + for (int ax = 0; ax <= 2; ++ax) { + for (int ay = 0; ax + ay <= 2; ++ay) { + const int triangle_degree = ax + ay; + const int max_t = (triangle_degree <= 1) ? 2 : 1; + for (int az = 0; az <= max_t; ++az) { + exponents.push_back({ax, ay, az}); + } + } + } + return exponents; +} + +// Independent Hex20 reference layout: the eight cube corners followed by the +// twelve edge midpoints, in the corner/edge order the reference layout uses. +std::vector> hex20_reference_nodes_for_test() { + std::vector> corners; + corners.push_back({double(-1), double(-1), double(-1)}); + corners.push_back({double(1), double(-1), double(-1)}); + corners.push_back({double(1), double(1), double(-1)}); + corners.push_back({double(-1), double(1), double(-1)}); + corners.push_back({double(-1), double(-1), double(1)}); + corners.push_back({double(1), double(-1), double(1)}); + corners.push_back({double(1), double(1), double(1)}); + corners.push_back({double(-1), double(1), double(1)}); + + const int edges[12][2] = { + {0, 1}, {1, 2}, {2, 3}, {3, 0}, + {4, 5}, {5, 6}, {6, 7}, {7, 4}, + {0, 4}, {1, 5}, {2, 6}, {3, 7}, + }; + + std::vector> nodes = corners; + for (const auto& edge : edges) { + const math::Vector midpoint = + (corners[static_cast(edge[0])] + + corners[static_cast(edge[1])]) * double(0.5); + nodes.push_back(midpoint); + } + return nodes; +} + +// Independent Wedge15 reference layout: the six prism corners followed by the +// nine edge midpoints, in reference-layout order. +std::vector> wedge15_reference_nodes_for_test() { + std::vector> corners; + corners.push_back({double(0), double(0), double(-1)}); + corners.push_back({double(1), double(0), double(-1)}); + corners.push_back({double(0), double(1), double(-1)}); + corners.push_back({double(0), double(0), double(1)}); + corners.push_back({double(1), double(0), double(1)}); + corners.push_back({double(0), double(1), double(1)}); + + const int edges[9][2] = { + {0, 1}, {1, 2}, {2, 0}, + {3, 4}, {4, 5}, {5, 3}, + {0, 3}, {1, 4}, {2, 5}, + }; + + std::vector> nodes = corners; + for (const auto& edge : edges) { + const math::Vector midpoint = + (corners[static_cast(edge[0])] + + corners[static_cast(edge[1])]) * double(0.5); + nodes.push_back(midpoint); + } + return nodes; +} + +// Independent Quad8 reference layout: the four quad corners followed by the four +// edge midpoints, in the corner/edge order the reference layout uses (the VTK +// quad boundary traversal). Mirrors the Hex20/Wedge15 anchors above. +std::vector> quad8_reference_nodes_for_test() { + std::vector> corners; + corners.push_back({double(-1), double(-1), double(0)}); + corners.push_back({double(1), double(-1), double(0)}); + corners.push_back({double(1), double(1), double(0)}); + corners.push_back({double(-1), double(1), double(0)}); + + const int edges[4][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 0}}; + + std::vector> nodes = corners; + for (const auto& edge : edges) { + const math::Vector midpoint = + (corners[static_cast(edge[0])] + + corners[static_cast(edge[1])]) * double(0.5); + nodes.push_back(midpoint); + } + return nodes; +} + +// --- Conditioning oracles (Legendre Vandermonde + Lebesgue constant) ---------- + +double legendre_value_for_test(double x, int degree) { + if (degree <= 0) { + return double(1); + } + double p_km1 = double(1); + double p_k = x; + for (int k = 1; k < degree; ++k) { + const double p_kp1 = + ((double(2) * double(k) + double(1)) * x * p_k - double(k) * p_km1) / + double(k + 1); + p_km1 = p_k; + p_k = p_kp1; + } + return p_k; +} + +double legendre_mode_for_test(const math::Vector& p, + const std::array& mode) { + return legendre_value_for_test(p[0], mode[0]) * + legendre_value_for_test(p[1], mode[1]) * + legendre_value_for_test(p[2], mode[2]); +} + +double matrix_norm_inf_for_test(const std::vector& matrix, std::size_t n) { + double max_row = double(0); + for (std::size_t row = 0; row < n; ++row) { + double sum = double(0); + for (std::size_t col = 0; col < n; ++col) { + sum += std::abs(matrix[row * n + col]); + } + max_row = std::max(max_row, sum); + } + return max_row; +} + +// Infinity-norm condition number of the Legendre generalized Vandermonde the +// production basis inverts, rebuilt from the basis nodes and the re-derived +// serendipity modes (an independent check that Source B is fixed). +double legendre_vandermonde_condition(const std::vector>& nodes, + const std::vector>& modes) { + const std::size_t n = nodes.size(); + std::vector v(n * n, double(0)); + for (std::size_t row = 0; row < n; ++row) { + for (std::size_t col = 0; col < n; ++col) { + v[row * n + col] = legendre_mode_for_test(nodes[row], modes[col]); + } + } + const double norm_v = matrix_norm_inf_for_test(v, n); + const auto inverse = math::invert_dense_matrix(v, n, "test Legendre Vandermonde"); + return norm_v * matrix_norm_inf_for_test(inverse, n); +} + +std::vector> quad_serendipity_modes_3d_for_test(int order) { + std::vector> modes; + for (const auto& e : quad_serendipity_exponents_for_test(order)) { + modes.push_back({e[0], e[1], 0}); + } + return modes; +} + +// Lebesgue constant of the nodal basis: the maximum over a dense reference-cell +// sample of sum_i |N_i(xi)|. Bounded and slowly growing for GLL nodes; it is the +// "are the shape functions good" metric (equispaced nodes make it blow up). +double serendipity_lebesgue_constant(const SerendipityBasis& basis, int samples) { + const int dim = basis.dimension(); + const auto axis = [samples](int idx) { + return double(-1) + double(2) * double(idx) / double(samples); + }; + double max_sum = double(0); + std::vector values; + for (int i = 0; i <= samples; ++i) { + for (int j = 0; j <= samples; ++j) { + const int kmax = (dim >= 3) ? samples : 0; + for (int k = 0; k <= kmax; ++k) { + const math::Vector xi{ + axis(i), axis(j), dim >= 3 ? axis(k) : double(0)}; + basis.evaluate_values(xi, values); + double sum = double(0); + for (const double v : values) { + sum += std::abs(v); + } + max_sum = std::max(max_sum, sum); + } + } + } + return max_sum; +} + +} // namespace + +TEST(SerendipityBasis, Quad8IsNodalAndPartitionsUnity) { + SerendipityBasis basis(ElementType::Quad8, 2); + SerendipityBasis topology_quad_basis(BasisTopology::Quadrilateral, 2); + + EXPECT_EQ(basis.size(), 8u); + // The named Quad8 and the arbitrary-order Quadrilateral path at order 2 now + // share the single ReferenceNodeLayout serendipity generator, so this pins + // that the named and topology overloads build the same object. The independent + // node-coordinate oracle is Quad8ReferenceNodesMatchIndependentConstruction. + expect_nodes_near(basis.nodes(), topology_quad_basis.nodes(), double(1e-14)); + expect_nodal_delta(basis, basis.nodes(), double(1e-10)); + expect_partition_of_unity(basis, {double(0.17), double(-0.31), double(0)}); +} + +// Quad8 takes its reference nodes from ReferenceNodeLayout -- the single public +// node-ordering source the solver adapter permutes against, the same source +// Hex20 and Wedge15 use. +TEST(SerendipityBasis, Quad8ReferenceNodesComeFromReferenceNodeLayout) { + SerendipityBasis basis(ElementType::Quad8, 2); + expect_nodes_near(basis.nodes(), + ReferenceNodeLayout::node_coords(ElementType::Quad8), + double(1e-14)); +} + +// Independent node-coordinate anchor for the Quad8 layout: the four corners +// followed by the four edge midpoints, breaking the loop where the basis and the +// reference layout are otherwise only checked against each other. Mirrors the +// Hex20/Wedge15 independent-construction anchors. +TEST(SerendipityBasis, Quad8ReferenceNodesMatchIndependentConstruction) { + SerendipityBasis basis(ElementType::Quad8, 2); + expect_nodes_near(basis.nodes(), quad8_reference_nodes_for_test(), double(1e-14)); +} + +TEST(SerendipityBasis, Hex20IsNodalAndPartitionsUnity) { + SerendipityBasis basis(ElementType::Hex20, 2); + + EXPECT_EQ(basis.size(), 20u); + expect_nodal_delta(basis, + reference_nodes(ElementType::Hex20, basis.size()), + double(1e-10)); + expect_partition_of_unity(basis, {double(0.2), double(-0.1), double(0.3)}); +} + +TEST(SerendipityBasis, Wedge15IsNodalAndPartitionsUnity) { + SerendipityBasis basis(ElementType::Wedge15, 2); + + EXPECT_EQ(basis.size(), 15u); + expect_nodal_delta(basis, + reference_nodes(ElementType::Wedge15, basis.size()), + double(1e-9)); + expect_partition_of_unity(basis, {double(0.2), double(0.3), double(0.1)}); +} + +TEST(SerendipityBasis, RejectsUnsupportedSerendipityAliases) { + EXPECT_THROW(SerendipityBasis(ElementType::Quad9, 2), BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid13, 2), BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid14, 2), BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(ElementType::Quad8, 3), BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(ElementType::Quad8, 1), BasisConfigurationException); + // Quad4 is the linear Lagrange quad, not a named serendipity layout; arbitrary + // quadrilateral serendipity is requested through BasisTopology::Quadrilateral. + EXPECT_THROW(SerendipityBasis(ElementType::Quad4, 2), BasisElementCompatibilityException); +} + +// Topology construction is the arbitrary-order entry point and exists only for +// the quadrilateral, the single serendipity family with a free order. Hex and +// wedge serendipity are fixed layouts requested through their named ElementType. +TEST(SerendipityBasis, TopologyConstructionSupportsQuadrilateralAndHexahedron) { + EXPECT_NO_THROW((void)SerendipityBasis(BasisTopology::Quadrilateral, 3)); + EXPECT_NO_THROW((void)SerendipityBasis(BasisTopology::Hexahedron, 3)); + EXPECT_THROW(SerendipityBasis(BasisTopology::Wedge, 2), + BasisElementCompatibilityException); + EXPECT_THROW(SerendipityBasis(BasisTopology::Triangle, 2), + BasisElementCompatibilityException); + + // Topology and named construction agree at the production order for both the + // quadrilateral and the hexahedron. + SerendipityBasis quad(BasisTopology::Quadrilateral, 2); + EXPECT_EQ(quad.topology(), BasisTopology::Quadrilateral); + EXPECT_EQ(quad.order(), 2); + EXPECT_EQ(named_element_for(quad.topology(), quad.order(), quad.basis_type()), + ElementType::Quad8); + + SerendipityBasis hex(BasisTopology::Hexahedron, 2); + EXPECT_EQ(hex.topology(), BasisTopology::Hexahedron); + EXPECT_EQ(hex.order(), 2); + EXPECT_EQ(named_element_for(hex.topology(), hex.order(), hex.basis_type()), + ElementType::Hex20); +} + +TEST(SerendipityBasis, SingleArgumentNamedOverloadInfersFixedOrder) { + const std::vector> named = { + {ElementType::Quad8, 2}, + {ElementType::Hex8, 1}, + {ElementType::Hex20, 2}, + {ElementType::Wedge15, 2}, + }; + for (const auto& [type, fixed_order] : named) { + const SerendipityBasis inferred(type); + const SerendipityBasis explicit_order(type, fixed_order); + EXPECT_EQ(inferred.order(), fixed_order) << "type=" << static_cast(type); + EXPECT_EQ(inferred.topology(), explicit_order.topology()) << "type=" << static_cast(type); + EXPECT_EQ(inferred.size(), explicit_order.size()) << "type=" << static_cast(type); + } + + EXPECT_THROW((void)SerendipityBasis(ElementType::Quad4), BasisElementCompatibilityException); + EXPECT_THROW((void)SerendipityBasis(ElementType::Tetra4), BasisElementCompatibilityException); +} + +TEST(SerendipityBasis, QuadrilateralRejectsOrdersBelowOne) { + // Serendipity bases require a positive polynomial order; orders <= 0 are + // rejected rather than normalized up to the linear space. + EXPECT_THROW(SerendipityBasis(BasisTopology::Quadrilateral, 0), + BasisConfigurationException); + EXPECT_THROW(SerendipityBasis(BasisTopology::Quadrilateral, -1), + BasisConfigurationException); + + // Order 1 is the smallest valid quadrilateral serendipity (the bilinear Q1 + // space): four corner nodes and the nodal-interpolation property. + SerendipityBasis basis(BasisTopology::Quadrilateral, 1); + EXPECT_EQ(basis.order(), 1); + EXPECT_EQ(basis.size(), 4u); + expect_nodal_delta(basis, basis.nodes(), double(1e-12)); +} + +// Explicit quadrilateral-topology serendipity orders run the documented monomial +// selection, boundary plus triangular interior node placement, and runtime +// Vandermonde inversion. Order four is the first order with an interior residual +// polynomial, so it is the first order that appends an interior node. +TEST(SerendipityBasis, QuadrilateralOrdersOneThreeFourAreNodalAndPartitionUnity) { + const struct Case { + int order; + std::size_t size; + } cases[] = { + {1, 4u}, + {3, 12u}, + {4, 17u}, + }; + + for (const auto& c : cases) { + SerendipityBasis basis(BasisTopology::Quadrilateral, c.order); + EXPECT_EQ(basis.size(), c.size) << "order=" << c.order; + EXPECT_EQ(basis.order(), c.order); + EXPECT_EQ(basis.dimension(), 2); + ASSERT_EQ(basis.nodes().size(), c.size); + + for (const auto& node : basis.nodes()) { + EXPECT_LE(std::abs(node[0]), double(1)); + EXPECT_LE(std::abs(node[1]), double(1)); + } + + expect_nodal_delta(basis, basis.nodes(), double(1e-9)); + expect_partition_of_unity(basis, {double(0.17), double(-0.31), double(0)}, double(1e-9)); + expect_partition_of_unity(basis, {double(-0.45), double(0.25), double(0)}, double(1e-9)); + } +} + +TEST(SerendipityBasis, QuadrilateralNodesFollowDocumentedConstructionThroughOrderTen) { + constexpr double kTol = double(1e-14); + + for (int order = 1; order <= 10; ++order) { + SerendipityBasis basis(BasisTopology::Quadrilateral, order); + const auto& nodes = basis.nodes(); + const std::size_t expected_size = expected_quad_serendipity_size(order); + const std::size_t boundary_count = static_cast(4 * order); + + ASSERT_EQ(basis.size(), expected_size) << "order=" << order; + ASSERT_EQ(nodes.size(), expected_size) << "order=" << order; + EXPECT_EQ(quad_serendipity_exponents_for_test(order).size(), + expected_size) << "order=" << order; + expect_no_duplicate_nodes(nodes, kTol); + + for (std::size_t i = 0; i < nodes.size(); ++i) { + EXPECT_NEAR(nodes[i][2], double(0), kTol) << "order=" << order + << " node=" << i; + EXPECT_LE(std::abs(nodes[i][0]), double(1)) << "order=" << order + << " node=" << i; + EXPECT_LE(std::abs(nodes[i][1]), double(1)) << "order=" << order + << " node=" << i; + + const bool on_boundary = + std::abs(std::abs(nodes[i][0]) - double(1)) <= kTol || + std::abs(std::abs(nodes[i][1]) - double(1)) <= kTol; + if (i < boundary_count) { + EXPECT_TRUE(on_boundary) << "order=" << order << " node=" << i; + } else { + EXPECT_FALSE(on_boundary) << "order=" << order << " node=" << i; + EXPECT_LT(std::abs(nodes[i][0]), double(1)) << "order=" << order + << " node=" << i; + EXPECT_LT(std::abs(nodes[i][1]), double(1)) << "order=" << order + << " node=" << i; + } + } + + std::size_t index = boundary_count; + if (order >= 4) { + // The interior staircase sits on Gauss-Lobatto-Legendre interior nodes: + // row r at the (r+1)-th GLL node of order m+2, each row's columns at the + // GLL interior of order row_count+1 (same line_coord_pm_one the basis + // uses), re-derived here as an independent placement oracle. + const int m = order - 4; + for (int row = 0; row <= m; ++row) { + const int row_count = m + 1 - row; + const double expected_y = line_coord_pm_one(row + 1, m + 2); + for (int col = 0; col < row_count; ++col) { + ASSERT_LT(index, nodes.size()); + const double expected_x = line_coord_pm_one(col + 1, row_count + 1); + EXPECT_NEAR(nodes[index][0], expected_x, kTol) + << "order=" << order << " row=" << row << " col=" << col; + EXPECT_NEAR(nodes[index][1], expected_y, kTol) + << "order=" << order << " row=" << row << " col=" << col; + ++index; + } + } + } + EXPECT_EQ(index, nodes.size()) << "order=" << order; + } +} + +TEST(SerendipityBasis, QuadrilateralOrderOneReproducesBilinearFunctions) { + SerendipityBasis basis(BasisTopology::Quadrilateral, 1); + + const std::vector> points = { + {double(0.25), double(-0.4), double(0)}, + {double(-0.7), double(0.6), double(0)}, + }; + for (const auto& xi : points) { + EXPECT_NEAR(interpolate_nodal_function(basis, xi, bilinear_function), + bilinear_function(xi), + double(1e-12)); + } +} + +TEST(SerendipityBasis, QuadrilateralOrderThreeReproducesSerendipityCubics) { + SerendipityBasis basis(BasisTopology::Quadrilateral, 3); + + const std::vector> points = { + {double(0.25), double(-0.4), double(0)}, + {double(-0.7), double(0.6), double(0)}, + }; + for (const auto& xi : points) { + EXPECT_NEAR(interpolate_nodal_function(basis, xi, cubic_serendipity_function), + cubic_serendipity_function(xi), + double(1e-11)); + } +} + +TEST(SerendipityBasis, QuadrilateralOrdersReproduceEverySerendipityMonomial) { + const std::vector> points = { + {double(0.25), double(-0.4), double(0)}, + {double(-0.7), double(0.6), double(0)}, + {double(0.11), double(0.23), double(0)}, + }; + + for (int order = 1; order <= 10; ++order) { + SerendipityBasis basis(BasisTopology::Quadrilateral, order); + const auto exponents = quad_serendipity_exponents_for_test(order); + ASSERT_EQ(exponents.size(), basis.size()) << "order=" << order; + + // Uniformly tight across the whole range: GLL nodes and the Legendre modal + // basis keep the reproduction accurate even at order 10 (the equispaced/ + // monomial construction needed 2e-8 here). + const double tolerance = double(1e-10); + for (const auto& exponent : exponents) { + for (const auto& xi : points) { + const double interpolated = + interpolate_nodal_function( + basis, + xi, + [&exponent](const math::Vector& node) { + return monomial_value_for_test(node, exponent); + }); + const double expected = monomial_value_for_test(xi, exponent); + EXPECT_NEAR(interpolated, expected, tolerance) + << "order=" << order << " ax=" << exponent[0] + << " ay=" << exponent[1] << " xi=(" << xi[0] << "," + << xi[1] << ")"; + } + } + } +} + +TEST(SerendipityBasis, QuadrilateralVandermondeHasFullRankThroughOrderTen) { + for (int order = 1; order <= 10; ++order) { + SerendipityBasis basis(BasisTopology::Quadrilateral, order); + const auto exponents = quad_serendipity_exponents_for_test(order); + const auto vandermonde = + quadrilateral_vandermonde_for_test(basis.nodes(), exponents); + const std::size_t n = basis.size(); + + ASSERT_EQ(exponents.size(), n) << "order=" << order; + ASSERT_EQ(vandermonde.size(), n * n) << "order=" << order; + EXPECT_EQ(math::dense_matrix_rank(vandermonde, n, n), n) + << "order=" << order; + } +} + +// Hex8 serendipity is the order-1 instance of the generated hexahedral +// serendipity space (the eight multilinear monomials). It must still reproduce +// the trilinear Lagrange basis -- values, gradients, and Hessians -- which guards +// the generated order-1 coefficient table against the closed-form trilinear basis. +TEST(SerendipityBasis, TrilinearHexMatchesLagrangeHex8) { + SerendipityBasis serendipity(ElementType::Hex8, 1); + LagrangeBasis lagrange(ElementType::Hex8, 1); + + EXPECT_EQ(serendipity.size(), 8u); + EXPECT_EQ(serendipity.dimension(), 3); + expect_nodal_delta(serendipity, + reference_nodes(ElementType::Hex8, serendipity.size()), + double(1e-12)); + + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + }; + for (const auto& xi : points) { + std::vector s_values; + std::vector l_values; + std::vector s_gradients; + std::vector l_gradients; + std::vector s_hessians; + std::vector l_hessians; + serendipity.evaluate_all(xi, s_values, s_gradients, s_hessians); + lagrange.evaluate_all(xi, l_values, l_gradients, l_hessians); + + ASSERT_EQ(s_values.size(), l_values.size()); + for (std::size_t i = 0; i < s_values.size(); ++i) { + EXPECT_NEAR(s_values[i], l_values[i], double(1e-13)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(s_gradients[i][d], l_gradients[i][d], double(1e-13)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(s_hessians[i](d, e), l_hessians[i](d, e), double(1e-13)); + } + } + } + } +} + +// The Hex20 and Wedge15 guard tests below pin the nodal coefficients away from +// the reference nodes. Each builds an independent Vandermonde from the +// re-derived monomial span and reference nodes, so a coefficient error that +// still vanishes at the nodes is caught. + +TEST(SerendipityBasis, Hex20VandermondeHasFullRank) { + SerendipityBasis basis(ElementType::Hex20, 2); + const auto exponents = hex20_serendipity_exponents_for_test(); + const std::size_t n = basis.size(); + ASSERT_EQ(exponents.size(), n); + const auto vandermonde = vandermonde_3d_for_test(basis.nodes(), exponents); + ASSERT_EQ(vandermonde.size(), n * n); + EXPECT_EQ(math::dense_matrix_rank(vandermonde, n, n), n); +} + +TEST(SerendipityBasis, Wedge15VandermondeHasFullRank) { + SerendipityBasis basis(ElementType::Wedge15, 2); + const auto exponents = wedge15_serendipity_exponents_for_test(); + const std::size_t n = basis.size(); + ASSERT_EQ(exponents.size(), n); + const auto vandermonde = vandermonde_3d_for_test(basis.nodes(), exponents); + ASSERT_EQ(vandermonde.size(), n * n); + EXPECT_EQ(math::dense_matrix_rank(vandermonde, n, n), n); +} + +// V * C == I guard: independently invert the Vandermonde and confirm the basis +// evaluates to the same inverse-Vandermonde nodal functions, without reading the +// basis's internal coefficient table. +TEST(SerendipityBasis, Hex20MatchesIndependentlyInvertedVandermonde) { + SerendipityBasis basis(ElementType::Hex20, 2); + const auto exponents = hex20_serendipity_exponents_for_test(); + const std::size_t n = basis.size(); + ASSERT_EQ(exponents.size(), n); + auto vandermonde = vandermonde_3d_for_test(basis.nodes(), exponents); + const auto coefficients = + math::invert_dense_matrix(std::move(vandermonde), n, "Hex20 test Vandermonde"); + + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + {double(0.11), double(0.23), double(-0.42)}, + }; + for (const auto& xi : points) { + std::vector values; + basis.evaluate_values(xi, values); + ASSERT_EQ(values.size(), n); + for (std::size_t i = 0; i < n; ++i) { + double expected = double(0); + for (std::size_t j = 0; j < n; ++j) { + expected += coefficients[j * n + i] * + monomial_value_3d_for_test(xi, exponents[j]); + } + EXPECT_NEAR(values[i], expected, double(1e-10)) << "basis=" << i; + } + } +} + +TEST(SerendipityBasis, Wedge15MatchesIndependentlyInvertedVandermonde) { + SerendipityBasis basis(ElementType::Wedge15, 2); + const auto exponents = wedge15_serendipity_exponents_for_test(); + const std::size_t n = basis.size(); + ASSERT_EQ(exponents.size(), n); + auto vandermonde = vandermonde_3d_for_test(basis.nodes(), exponents); + const auto coefficients = + math::invert_dense_matrix(std::move(vandermonde), n, "Wedge15 test Vandermonde"); + + const std::vector> points = { + {double(0.2), double(0.3), double(0.1)}, + {double(0.25), double(0.25), double(-0.4)}, + {double(0.1), double(0.6), double(0.5)}, + }; + for (const auto& xi : points) { + std::vector values; + basis.evaluate_values(xi, values); + ASSERT_EQ(values.size(), n); + for (std::size_t i = 0; i < n; ++i) { + double expected = double(0); + for (std::size_t j = 0; j < n; ++j) { + expected += coefficients[j * n + i] * + monomial_value_3d_for_test(xi, exponents[j]); + } + EXPECT_NEAR(values[i], expected, double(1e-10)) << "basis=" << i; + } + } +} + +// Non-nodal polynomial reproduction: the basis must reproduce every monomial in +// its span at interior points, not just interpolate at the nodes. +TEST(SerendipityBasis, Hex20ReproducesEverySerendipityMonomial) { + SerendipityBasis basis(ElementType::Hex20, 2); + const auto exponents = hex20_serendipity_exponents_for_test(); + ASSERT_EQ(exponents.size(), basis.size()); + + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + {double(0.11), double(0.23), double(-0.42)}, + }; + for (const auto& exponent : exponents) { + for (const auto& xi : points) { + const double interpolated = interpolate_nodal_function( + basis, xi, + [&exponent](const math::Vector& node) { + return monomial_value_3d_for_test(node, exponent); + }); + EXPECT_NEAR(interpolated, monomial_value_3d_for_test(xi, exponent), + double(1e-10)) + << "ax=" << exponent[0] << " ay=" << exponent[1] + << " az=" << exponent[2]; + } + } +} + +TEST(SerendipityBasis, Wedge15ReproducesEverySerendipityMonomial) { + SerendipityBasis basis(ElementType::Wedge15, 2); + const auto exponents = wedge15_serendipity_exponents_for_test(); + ASSERT_EQ(exponents.size(), basis.size()); + + const std::vector> points = { + {double(0.2), double(0.3), double(0.1)}, + {double(0.25), double(0.25), double(-0.4)}, + {double(0.1), double(0.6), double(0.5)}, + }; + for (const auto& exponent : exponents) { + for (const auto& xi : points) { + const double interpolated = interpolate_nodal_function( + basis, xi, + [&exponent](const math::Vector& node) { + return monomial_value_3d_for_test(node, exponent); + }); + EXPECT_NEAR(interpolated, monomial_value_3d_for_test(xi, exponent), + double(1e-10)) + << "ax=" << exponent[0] << " ay=" << exponent[1] + << " az=" << exponent[2]; + } + } +} + +// Independent node-coordinate anchor: the reference nodes must be the cube/prism +// corners and edge midpoints, breaking the loop where the basis and its node +// table are otherwise only checked against each other. +TEST(SerendipityBasis, Hex20ReferenceNodesMatchIndependentConstruction) { + SerendipityBasis basis(ElementType::Hex20, 2); + expect_nodes_near(basis.nodes(), hex20_reference_nodes_for_test(), double(1e-14)); +} + +TEST(SerendipityBasis, Wedge15ReferenceNodesMatchIndependentConstruction) { + SerendipityBasis basis(ElementType::Wedge15, 2); + expect_nodes_near(basis.nodes(), wedge15_reference_nodes_for_test(), double(1e-14)); +} + +// --- Arbitrary-order hexahedral serendipity (BasisTopology::Hexahedron) ------- + +// dim S_p of the cube serendipity space for p = 1..6 (Hex8 = 8, Hex20 = 20), +// checked against both the re-derived monomial enumeration and the node-strata +// decomposition. +TEST(SerendipityBasis, HexahedralSerendipitySpaceHasExpectedDimensions) { + const std::array expected = {0u, 8u, 20u, 32u, 50u, 74u, 105u}; + for (int order = 1; order <= 6; ++order) { + const auto exponents = hex_serendipity_exponents_for_test(order); + const auto p = static_cast(order); + EXPECT_EQ(exponents.size(), expected[p]) << "order=" << order; + EXPECT_EQ(expected_hex_serendipity_size(order), expected[p]) << "order=" << order; + for (const auto& e : exponents) { + EXPECT_LE(superlinear_degree_3d_for_test(e[0], e[1], e[2]), order); + for (int d = 0; d < 3; ++d) { + EXPECT_GE(e[d], 0); + EXPECT_LE(e[d], order); + } + } + } + + // The order-2 hex serendipity span is exactly the Hex20 span (as a set). + auto order_two = hex_serendipity_exponents_for_test(2); + auto hex20 = hex20_serendipity_exponents_for_test(); + std::sort(order_two.begin(), order_two.end()); + std::sort(hex20.begin(), hex20.end()); + EXPECT_EQ(order_two, hex20); +} + +// VTK conformance: the generated arbitrary-order layout reproduces the public +// Hex8 (order 1) and Hex20 (order 2) node ordering coordinate-for-coordinate. +TEST(SerendipityBasis, HexahedralTopologyNodesMatchPublicHex8AndHex20Layouts) { + SerendipityBasis hex8(BasisTopology::Hexahedron, 1); + EXPECT_EQ(hex8.size(), 8u); + expect_nodes_near(hex8.nodes(), + ReferenceNodeLayout::node_coords(ElementType::Hex8), + double(1e-14)); + + SerendipityBasis hex20(BasisTopology::Hexahedron, 2); + EXPECT_EQ(hex20.size(), 20u); + expect_nodes_near(hex20.nodes(), + ReferenceNodeLayout::node_coords(ElementType::Hex20), + double(1e-14)); +} + +TEST(SerendipityBasis, SkeletonMatchesCompleteLagrangePrefix) { + for (int order = 1; order <= 8; ++order) { + SerendipityBasis quad(BasisTopology::Quadrilateral, order); + const auto quad_complete = + ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Quad4, order); + const std::size_t quad_skeleton = static_cast(4 * order); + ASSERT_LE(quad_skeleton, quad.nodes().size()) << "quad order=" << order; + ASSERT_LE(quad_skeleton, quad_complete.size()) << "quad order=" << order; + for (std::size_t i = 0; i < quad_skeleton; ++i) { + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(quad.nodes()[i][d], quad_complete[i][d]) + << "quad order=" << order << " node=" << i << " d=" << d; + } + } + + SerendipityBasis hex(BasisTopology::Hexahedron, order); + const auto hex_complete = + ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Hex8, order); + const std::size_t hex_skeleton = + 8u + 12u * static_cast(order - 1); + ASSERT_LE(hex_skeleton, hex.nodes().size()) << "hex order=" << order; + ASSERT_LE(hex_skeleton, hex_complete.size()) << "hex order=" << order; + for (std::size_t i = 0; i < hex_skeleton; ++i) { + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(hex.nodes()[i][d], hex_complete[i][d]) + << "hex order=" << order << " node=" << i << " d=" << d; + } + } + } +} + +// The generated node set is unisolvent for the hex serendipity span at every +// supported order: the Vandermonde of the re-derived monomials at the generated +// nodes has full rank. +TEST(SerendipityBasis, HexahedralSerendipityVandermondeHasFullRankThroughOrderSix) { + for (int order = 1; order <= 6; ++order) { + SerendipityBasis basis(BasisTopology::Hexahedron, order); + const auto exponents = hex_serendipity_exponents_for_test(order); + const std::size_t n = basis.size(); + ASSERT_EQ(exponents.size(), n) << "order=" << order; + const auto vandermonde = vandermonde_3d_for_test(basis.nodes(), exponents); + ASSERT_EQ(vandermonde.size(), n * n) << "order=" << order; + EXPECT_EQ(math::dense_matrix_rank(vandermonde, n, n), n) << "order=" << order; + } +} + +TEST(SerendipityBasis, HexahedralTopologyIsNodalAndPartitionsUnity) { + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + }; + for (int order = 1; order <= 5; ++order) { + SerendipityBasis basis(BasisTopology::Hexahedron, order); + EXPECT_EQ(basis.dimension(), 3); + EXPECT_EQ(basis.order(), order); + EXPECT_EQ(basis.size(), expected_hex_serendipity_size(order)) << "order=" << order; + ASSERT_EQ(basis.nodes().size(), basis.size()); + + expect_nodal_delta(basis, basis.nodes(), double(1e-9)); + for (const auto& xi : points) { + expect_partition_of_unity(basis, xi, double(1e-9)); + } + } +} + +// Non-nodal polynomial reproduction across orders: the basis reproduces every +// monomial in its span at interior points, pinning the production monomial space +// against the re-derived verification. +TEST(SerendipityBasis, HexahedralTopologyReproducesEverySerendipityMonomial) { + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + {double(0.11), double(0.23), double(-0.42)}, + }; + for (int order = 1; order <= 5; ++order) { + SerendipityBasis basis(BasisTopology::Hexahedron, order); + const auto exponents = hex_serendipity_exponents_for_test(order); + ASSERT_EQ(exponents.size(), basis.size()) << "order=" << order; + for (const auto& exponent : exponents) { + for (const auto& xi : points) { + const double interpolated = interpolate_nodal_function( + basis, xi, + [&exponent](const math::Vector& node) { + return monomial_value_3d_for_test(node, exponent); + }); + EXPECT_NEAR(interpolated, monomial_value_3d_for_test(xi, exponent), + double(1e-9)) + << "order=" << order << " ax=" << exponent[0] + << " ay=" << exponent[1] << " az=" << exponent[2]; + } + } + } +} + + +TEST(SerendipityBasis, NamedHexLayoutsMatchTopologyConstruction) { + const struct Case { ElementType type; int order; } cases[] = { + {ElementType::Hex8, 1}, + {ElementType::Hex20, 2}, + }; + const std::vector> points = { + {double(0.2), double(-0.1), double(0.3)}, + {double(-0.35), double(0.25), double(-0.15)}, + {double(0.11), double(0.23), double(-0.42)}, + }; + for (const auto& c : cases) { + SerendipityBasis named(c.type, c.order); + SerendipityBasis topo(BasisTopology::Hexahedron, c.order); + + ASSERT_EQ(named.size(), topo.size()); + ASSERT_EQ(named.nodes().size(), topo.nodes().size()); + for (std::size_t i = 0; i < named.nodes().size(); ++i) { + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(named.nodes()[i][d], topo.nodes()[i][d]) + << "node=" << i << " d=" << d; + } + } + + for (const auto& xi : points) { + std::vector nv, tv; + std::vector ng, tg; + std::vector nh, th; + named.evaluate_all(xi, nv, ng, nh); + topo.evaluate_all(xi, tv, tg, th); + ASSERT_EQ(nv.size(), tv.size()); + for (std::size_t i = 0; i < nv.size(); ++i) { + EXPECT_EQ(nv[i], tv[i]) << "value i=" << i; + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(ng[i][d], tg[i][d]) << "grad i=" << i << " d=" << d; + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_EQ(nh[i](d, e), th[i](d, e)) + << "hess i=" << i << " (" << d << "," << e << ")"; + } + } + } + } + } +} + +// Conditioning is a tested quantity, not a tolerance that quietly loosens. With +// the Legendre modal basis and Gauss-Lobatto-Legendre nodes, both the Vandermonde +// condition number and the Lebesgue constant stay small across the recommended +// range -- a logarithmic-style growth instead of the exponential blow-up of the +// previous equispaced/monomial construction (which lost ~8 digits by order 10). +TEST(SerendipityBasis, SerendipityStaysWellConditionedAcrossRecommendedRange) { + for (int order = 1; order <= 10; ++order) { + SerendipityBasis basis(BasisTopology::Quadrilateral, order); + const double cond = legendre_vandermonde_condition( + basis.nodes(), quad_serendipity_modes_3d_for_test(order)); + const double lebesgue = serendipity_lebesgue_constant(basis, 24); + EXPECT_LT(cond, double(2.5e4)) << "quad order=" << order; + EXPECT_LT(lebesgue, double(9e2)) << "quad order=" << order; + } + for (int order = 1; order <= 8; ++order) { + SerendipityBasis basis(BasisTopology::Hexahedron, order); + const double cond = legendre_vandermonde_condition( + basis.nodes(), hex_serendipity_exponents_for_test(order)); + const double lebesgue = serendipity_lebesgue_constant(basis, 12); + EXPECT_LT(cond, double(2e4)) << "hex order=" << order; + EXPECT_LT(lebesgue, double(3.5e2)) << "hex order=" << order; + } +} + +// The condition-number guard is the numerical-soundness backstop: orders pushed +// far past the well-conditioned range throw rather than return shape functions +// whose coefficients have lost all precision. The recommended orders construct +// without complaint. +TEST(SerendipityBasis, RejectsOrdersBeyondTheWellConditionedRange) { + EXPECT_NO_THROW((void)SerendipityBasis(BasisTopology::Quadrilateral, 10)); + EXPECT_NO_THROW((void)SerendipityBasis(BasisTopology::Hexahedron, 8)); + EXPECT_THROW((void)SerendipityBasis(BasisTopology::Quadrilateral, 20), + BasisConstructionException); + EXPECT_THROW((void)SerendipityBasis(BasisTopology::Hexahedron, 16), + BasisConstructionException); +} diff --git a/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp b/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp new file mode 100644 index 000000000..b029a55b4 --- /dev/null +++ b/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp @@ -0,0 +1,373 @@ +/** + * @file test_DenseLinearAlgebra.cpp + * @brief Tests for shared dense linear algebra utilities. + */ + +#include + +#include "FE/Common/FEException.h" +#include "FE/Math/DenseLinearAlgebra.h" + +#include +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::math; + +namespace { + +double multiply_entry(const std::vector& A, + const std::vector& B, + std::size_t n, + std::size_t row, + std::size_t col) { + double sum = double(0); + for (std::size_t k = 0; k < n; ++k) { + sum += A[row * n + k] * B[k * n + col]; + } + return sum; +} + +} // namespace + +TEST(DenseLinearAlgebra, InvertsScaledMatrix) { + const std::vector A{ + double(1.0e9), double(2.0e6), + double(3.0e3), double(4.0) + }; + + const auto inv = invert_dense_matrix(A, 2u, "scaled 2x2"); + for (std::size_t row = 0; row < 2u; ++row) { + for (std::size_t col = 0; col < 2u; ++col) { + const double expected = (row == col) ? double(1) : double(0); + EXPECT_NEAR(multiply_entry(A, inv, 2u, row, col), expected, double(1.0e-10)); + } + } +} + +TEST(DenseLinearAlgebra, FactorizationSolvesMultipleRightHandSides) { + const std::vector A{ + double(4), double(2), double(0), + double(2), double(5), double(1), + double(0), double(1), double(3) + }; + + const auto solver = factor_dense_matrix(A, 3u, "symmetric 3x3"); + EXPECT_EQ(solver.diagnostics.rank, 3u); + + const std::vector rhs{double(2), double(4), double(6)}; + const auto x = solver.solve(std::span(rhs.data(), rhs.size())); + ASSERT_EQ(x.size(), 3u); + + for (std::size_t row = 0; row < 3u; ++row) { + double ax = double(0); + for (std::size_t col = 0; col < 3u; ++col) { + ax += A[row * 3u + col] * x[col]; + } + EXPECT_NEAR(ax, rhs[row], double(1.0e-12)); + } + + std::vector second_rhs{double(1), double(-2), double(0.5)}; + const auto original_second_rhs = second_rhs; + solver.solve_in_place(std::span(second_rhs.data(), second_rhs.size())); + for (std::size_t row = 0; row < 3u; ++row) { + double ax = double(0); + for (std::size_t col = 0; col < 3u; ++col) { + ax += A[row * 3u + col] * second_rhs[col]; + } + EXPECT_NEAR(ax, original_second_rhs[row], double(1.0e-12)); + } +} + +TEST(DenseLinearAlgebra, FactorizationSolvesDenseRightHandSideBlock) { + const std::vector A{ + double(4), double(2), double(0), + double(2), double(5), double(1), + double(0), double(1), double(3) + }; + + const auto solver = factor_dense_matrix(A, 3u, "symmetric 3x3 block"); + + std::vector rhs{ + double(2), double(1), + double(4), double(-2), + double(6), double(0.5) + }; + const auto original_rhs = rhs; + solver.solve_in_place(std::span(rhs.data(), rhs.size()), 2u); + + for (std::size_t rhs_col = 0; rhs_col < 2u; ++rhs_col) { + for (std::size_t row = 0; row < 3u; ++row) { + double ax = double(0); + for (std::size_t col = 0; col < 3u; ++col) { + ax += A[row * 3u + col] * rhs[col * 2u + rhs_col]; + } + EXPECT_NEAR(ax, original_rhs[row * 2u + rhs_col], double(1.0e-12)); + } + } +} + +// Every other matrix in this file already has its largest pivot on the +// diagonal, so these cases cover the row-exchange branch in factor_dense_matrix, +// the inverse path used by SerendipityBasis, and the permutation replay in +// solve_in_place. +TEST(DenseLinearAlgebra, FactorizationPivotsThroughZeroLeadingDiagonal) { + const std::vector swap_2x2{ + double(0), double(1), + double(1), double(0) + }; + + const auto solver = factor_dense_matrix(swap_2x2, 2u, "swap 2x2"); + const std::vector rhs{double(3), double(7)}; + const auto x = solver.solve(std::span(rhs.data(), rhs.size())); + ASSERT_EQ(x.size(), 2u); + EXPECT_NEAR(x[0], double(7), double(1.0e-14)); + EXPECT_NEAR(x[1], double(3), double(1.0e-14)); + + const auto inv = invert_dense_matrix(swap_2x2, 2u, "swap 2x2"); + for (std::size_t row = 0; row < 2u; ++row) { + for (std::size_t col = 0; col < 2u; ++col) { + EXPECT_NEAR(inv[row * 2u + col], swap_2x2[row * 2u + col], double(1.0e-14)); + } + } + + // Every column requires a row exchange during elimination. + const std::vector permuted_scaled{ + double(0), double(0), double(1), double(0), + double(1), double(0), double(0), double(0), + double(0), double(0), double(0), double(2), + double(0), double(3), double(0), double(0) + }; + + const auto inv4 = invert_dense_matrix(permuted_scaled, 4u, "permuted scaled 4x4"); + for (std::size_t row = 0; row < 4u; ++row) { + for (std::size_t col = 0; col < 4u; ++col) { + const double expected = (row == col) ? double(1) : double(0); + EXPECT_NEAR(multiply_entry(permuted_scaled, inv4, 4u, row, col), + expected, + double(1.0e-14)); + } + } +} + +TEST(DenseLinearAlgebra, WideMultiRhsSolveWithPivoting) { + // Requires a row swap in column 0 and uses a wide right-hand-side block to + // exercise the row-interleaved multi-RHS layout end to end. + const std::vector A{ + double(0), double(2), double(1), + double(4), double(1), double(0), + double(1), double(0), double(3) + }; + constexpr std::size_t kRhsCount = 33u; + + const auto solver = factor_dense_matrix(A, 3u, "pivoting 3x3"); + + std::vector rhs(3u * kRhsCount, double(0)); + for (std::size_t row = 0; row < 3u; ++row) { + for (std::size_t r = 0; r < kRhsCount; ++r) { + rhs[row * kRhsCount + r] = + double(1) + static_cast(row) - double(0.25) * static_cast(r % 7u); + } + } + const auto original_rhs = rhs; + + solver.solve_in_place(std::span(rhs.data(), rhs.size()), kRhsCount); + + for (std::size_t r = 0; r < kRhsCount; ++r) { + for (std::size_t row = 0; row < 3u; ++row) { + double ax = double(0); + for (std::size_t col = 0; col < 3u; ++col) { + ax += A[row * 3u + col] * rhs[col * kRhsCount + r]; + } + EXPECT_NEAR(ax, original_rhs[row * kRhsCount + r], double(1.0e-12)) + << "rhs column " << r << ", row " << row; + } + } +} + +TEST(DenseLinearAlgebra, SolveInPlaceValidatesInputs) { + const std::vector identity{ + double(1), double(0), + double(0), double(1) + }; + const auto solver = factor_dense_matrix(identity, 2u, "identity 2x2"); + + std::vector rhs{double(1), double(2)}; + EXPECT_THROW(solver.solve_in_place(std::span(rhs.data(), rhs.size()), 0u), + FEException); + + std::vector wrong_size{double(1), double(2), double(3)}; + EXPECT_THROW( + solver.solve_in_place(std::span(wrong_size.data(), wrong_size.size()), 1u), + FEException); + + DenseLUSolver unfactored; + unfactored.n = 2u; + unfactored.error_message_label = "unfactored"; + EXPECT_FALSE(unfactored.empty()); + EXPECT_THROW(unfactored.solve_in_place(std::span(rhs.data(), rhs.size()), 1u), + FEException); +} + +TEST(DenseLinearAlgebra, DiagnosticValidationRejectsRankMismatch) { + DenseInverseResult result; + result.diagnostics.rank = 1u; + + EXPECT_THROW(validate_dense_inverse_diagnostics(result, 2u, "rank mismatch"), + FEException); +} + +TEST(DenseLinearAlgebra, RankHandlesNonSquareMatrices) { + const std::vector wide_full{ + double(1), double(0), double(2), + double(0), double(1), double(-1) + }; + EXPECT_EQ(dense_matrix_rank(wide_full, 2u, 3u), 2u); + + const std::vector tall_rank_one{ + double(1), double(2), + double(2), double(4), + double(3), double(6) + }; + EXPECT_EQ(dense_matrix_rank(tall_rank_one, 3u, 2u), 1u); +} + +TEST(DenseLinearAlgebra, HighConditionInverseUsesSvdFallback) { + const std::vector high_condition{ + double(1), double(0), + double(0), double(1.0e-13) + }; + + const auto result = + invert_dense_matrix_with_diagnostics(high_condition, 2u, "high-condition diagonal"); + EXPECT_EQ(result.diagnostics.rank, 2u); + EXPECT_GT(result.diagnostics.condition_estimate, + dense_matrix_condition_fallback_threshold()); + EXPECT_TRUE(result.used_svd_fallback); + + for (std::size_t row = 0; row < 2u; ++row) { + for (std::size_t col = 0; col < 2u; ++col) { + const double expected = (row == col) ? double(1) : double(0); + EXPECT_NEAR(multiply_entry(high_condition, result.inverse, 2u, row, col), + expected, + double(1.0e-12)); + } + } +} + +TEST(DenseLinearAlgebra, DiagnosticValidationRejectsUnsupportedCondition) { + DenseInverseResult result; + result.diagnostics.rank = 2u; + result.diagnostics.condition_estimate = + dense_matrix_condition_error_threshold() * double(10); + + EXPECT_GT(result.diagnostics.condition_estimate, + dense_matrix_condition_error_threshold()); + EXPECT_THROW(validate_dense_inverse_diagnostics( + result, 2u, "excessive-condition diagonal"), + FEException); +} + +TEST(DenseLinearAlgebra, ThrowsForScaleAwareSingularPivot) { + const std::vector singular{ + double(1.0e12), double(2.0e12), + double(0.5e12), double(1.0e12) + }; + + EXPECT_THROW((void)invert_dense_matrix(singular, 2u, "singular 2x2"), + FEException); +} + +TEST(DenseLinearAlgebra, FactorizationThrowsForRankDeficientMatrix) { + const std::vector singular{ + double(1), double(2), + double(2), double(4) + }; + + EXPECT_THROW((void)factor_dense_matrix(singular, 2u, "rank-one 2x2"), + FEException); +} + +TEST(DenseLinearAlgebra, RankUsesScaleAwareTolerance) { + const std::vector rank_one{ + double(1.0e8), double(2.0e8), + double(3.0e8), double(6.0e8) + }; + EXPECT_EQ(dense_matrix_rank(rank_one, 2u, 2u), 1u); + + const std::vector full_rank{ + double(1.0e8), double(2.0e8), + double(3.0e8), double(6.1e8) + }; + EXPECT_EQ(dense_matrix_rank(full_rank, 2u, 2u), 2u); +} + +TEST(DenseLinearAlgebra, DiagnosticsReportRankAndConditionEstimate) { + const std::vector diagonal{ + double(4), double(0), + double(0), double(0.5) + }; + const auto full = + dense_matrix_diagnostics(diagonal, 2u, 2u, "diagonal 2x2"); + EXPECT_EQ(full.rank, 2u); + EXPECT_NEAR(full.largest_singular_value, double(4), double(1.0e-14)); + EXPECT_NEAR(full.smallest_retained_singular_value, double(0.5), double(1.0e-14)); + EXPECT_NEAR(full.condition_estimate, double(8), double(1.0e-14)); + + const std::vector rank_one{ + double(1), double(2), + double(2), double(4) + }; + const auto deficient = + dense_matrix_diagnostics(rank_one, 2u, 2u, "rank-one 2x2"); + EXPECT_EQ(deficient.rank, 1u); + EXPECT_TRUE(std::isinf(deficient.condition_estimate)); +} + +TEST(DenseLinearAlgebra, PseudoInverseHandlesSingularMatrixWithoutNormalEquations) { + const std::vector rank_one{ + double(1), double(2), + double(2), double(4) + }; + + const auto pinv = + rank_revealing_pseudo_inverse(rank_one, 2u, 2u, "rank-one 2x2"); + EXPECT_EQ(pinv.rank, 1u); + EXPECT_NEAR(pinv.inverse[0], double(0.04), double(1.0e-13)); + EXPECT_NEAR(pinv.inverse[1], double(0.08), double(1.0e-13)); + EXPECT_NEAR(pinv.inverse[2], double(0.08), double(1.0e-13)); + EXPECT_NEAR(pinv.inverse[3], double(0.16), double(1.0e-13)); + + std::vector projection(4u, double(0)); + for (std::size_t row = 0; row < 2u; ++row) { + for (std::size_t col = 0; col < 2u; ++col) { + for (std::size_t a = 0; a < 2u; ++a) { + for (std::size_t b = 0; b < 2u; ++b) { + projection[row * 2u + col] += + rank_one[row * 2u + a] * pinv.inverse[a * 2u + b] * + rank_one[b * 2u + col]; + } + } + EXPECT_NEAR(projection[row * 2u + col], + rank_one[row * 2u + col], + double(1.0e-12)); + } + } +} + +TEST(DenseLinearAlgebra, PseudoInverseDropsNearZeroSingularValues) { + const std::vector near_singular{ + double(1), double(0), + double(0), double(1.0e-18) + }; + + const auto pinv = + rank_revealing_pseudo_inverse(near_singular, 2u, 2u, "near-singular 2x2"); + EXPECT_EQ(pinv.rank, 1u); + EXPECT_GT(pinv.tolerance, double(1.0e-18)); + EXPECT_NEAR(pinv.inverse[0], double(1), double(1.0e-14)); + EXPECT_NEAR(pinv.inverse[1], double(0), double(1.0e-14)); + EXPECT_NEAR(pinv.inverse[2], double(0), double(1.0e-14)); + EXPECT_NEAR(pinv.inverse[3], double(0), double(1.0e-14)); +} diff --git a/tests/unitTests/test_common.h b/tests/unitTests/test_common.h index 98709f600..ce6ffed4b 100644 --- a/tests/unitTests/test_common.h +++ b/tests/unitTests/test_common.h @@ -96,4 +96,4 @@ class TestBase { }; -#endif \ No newline at end of file +#endif