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..eace4d0b2 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++20") 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/FE/Basis/BasisExceptions.h b/Code/Source/solver/FE/Basis/BasisExceptions.h new file mode 100644 index 000000000..8f8fd3c3c --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisExceptions.h @@ -0,0 +1,90 @@ +// 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 { + +/** + * @brief Base exception type for errors originating in the Basis module + */ +class BasisException : public FEException { +public: + BasisException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "", + StatusCode status = StatusCode::Unknown) + : FEException(message, status, file, line, function) {} +}; + +/** + * @brief Invalid Basis request or configuration + */ +class BasisConfigurationException : public BasisException { +public: + BasisConfigurationException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "") + : BasisException(message, file, line, function, StatusCode::InvalidArgument) {} +}; + +/** + * @brief Requested element topology is incompatible with the basis family + */ +class BasisElementCompatibilityException : public BasisException { +public: + BasisElementCompatibilityException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "") + : BasisException(message, file, line, function, StatusCode::InvalidArgument) {} +}; + +/** + * @brief Basis evaluation request cannot be satisfied + */ +class BasisEvaluationException : public BasisException { +public: + BasisEvaluationException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "") + : BasisException(message, file, line, function, StatusCode::InvalidArgument) {} +}; + +/** + * @brief Public-to-canonical node ordering or coordinate lookup failure + */ +class BasisNodeOrderingException : public BasisException { +public: + BasisNodeOrderingException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "") + : BasisException(message, file, line, function, StatusCode::InvalidArgument) {} +}; + +/** + * @brief Internal basis construction or transform setup failure + */ +class BasisConstructionException : public BasisException { +public: + BasisConstructionException(const std::string& message, + const char* file = "", + int line = 0, + const char* function = "") + : BasisException(message, file, line, function, 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..c3130d16f --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFactory.cpp @@ -0,0 +1,98 @@ +// 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 { + +int require_basis_order(const BasisRequest& req, + const char* missing_message, + const char* negative_message) { + FE::throw_if(!req.order.has_value(), SVMP_HERE, + missing_message); + FE::throw_if(*req.order < 0, SVMP_HERE, + negative_message); + return *req.order; +} + +void require_scalar_c0_request(const BasisRequest& req) { + FE::throw_if( + req.field_type != FieldType::Scalar, SVMP_HERE, + "BasisFactory: Lagrange/Serendipity bases support scalar fields only"); + FE::throw_if( + req.continuity != Continuity::C0, SVMP_HERE, + "BasisFactory: Lagrange/Serendipity bases support C0 continuity only"); +} + +std::shared_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"); + return std::make_shared(req.element_type, order); +} + +std::shared_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"); + return std::make_shared(req.element_type, order); +} + +} // namespace + +namespace basis_factory { + +std::shared_ptr create(const BasisRequest& req) { + switch (req.basis_type) { + case BasisType::Lagrange: + return create_lagrange(req); + case BasisType::Serendipity: + return create_serendipity(req); + default: + FE::raise(SVMP_HERE, + "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}; + } + FE::raise(SVMP_HERE, + "BasisFactory: no default basis is defined for the requested element type"); + } + } +} + +std::shared_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..3922d5ced --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFactory.h @@ -0,0 +1,71 @@ +// 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 { + +struct BasisRequest { + ElementType element_type; + BasisType basis_type; + std::optional order{}; + Continuity continuity{Continuity::C0}; + FieldType field_type{FieldType::Scalar}; + std::vector knot_vector{}; + std::vector weights{}; + std::vector axis_orders{}; + std::vector> axis_knot_vectors{}; + std::vector> axis_weights{}; + std::vector tensor_extents{}; + std::string custom_id{}; +}; + +namespace basis_factory { + +[[nodiscard]] std::shared_ptr create(const BasisRequest& req); + +/// \brief Return the default basis request (family and order) for an element type. +/// +/// \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. +/// +/// \details Equivalent to create(default_basis_request(element_type)). +/// +/// \param element_type Element type to create a default basis for. +/// \return Shared basis instance. +[[nodiscard]] std::shared_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..1c8c31e5d --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFunction.cpp @@ -0,0 +1,143 @@ +// 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 { + +namespace { + +struct BasisFunctionScratch { + std::vector values; + std::vector gradients; + std::vector hessians; +}; + +BasisFunctionScratch& scratch() { + static thread_local BasisFunctionScratch data; + return data; +} + +void require_span_size(std::size_t actual, + std::size_t expected, + const char* label) { + FE::throw_if(actual < expected, SVMP_HERE, + std::string("BasisFunction::") + label + ": output span is smaller than basis size"); +} + +} // namespace + +void BasisFunction::evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const { + (void)xi; + (void)gradients; + FE::raise(SVMP_HERE, + "Analytic gradient evaluation is not implemented for this basis"); +} + +void BasisFunction::evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const { + (void)xi; + (void)hessians; + FE::raise(SVMP_HERE, + "Analytic Hessian evaluation is not implemented for this basis"); +} + +void BasisFunction::evaluate_all(const math::Vector& xi, + std::vector& values, + std::vector& gradients, + std::vector& hessians) const { + evaluate_values(xi, values); + evaluate_gradients(xi, gradients); + evaluate_hessians(xi, hessians); +} + +void BasisFunction::evaluate_values_to(const math::Vector& xi, + std::span values_out) const { + require_span_size(values_out.size(), size(), "evaluate_values_to"); + auto& tmp = scratch().values; + tmp.resize(size()); + evaluate_values(xi, tmp); + std::copy_n(tmp.begin(), tmp.size(), values_out.begin()); +} + +void BasisFunction::evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const { + require_span_size(gradients_out.size(), size(), "evaluate_gradients_to"); + auto& tmp = scratch().gradients; + tmp.resize(size()); + evaluate_gradients(xi, tmp); + std::copy_n(tmp.begin(), tmp.size(), gradients_out.begin()); +} + +void BasisFunction::evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const { + require_span_size(hessians_out.size(), size(), "evaluate_hessians_to"); + auto& tmp = scratch().hessians; + tmp.resize(size()); + evaluate_hessians(xi, tmp); + std::copy_n(tmp.begin(), tmp.size(), hessians_out.begin()); +} + +void BasisFunction::numerical_gradient(const math::Vector& xi, + std::vector& gradients, + Real 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]) / (Real(2) * eps); + } + } +} + +void BasisFunction::numerical_hessian(const math::Vector& xi, + std::vector& hessians, + Real 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]) / (Real(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..8327ffda9 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisFunction.h @@ -0,0 +1,278 @@ +// 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 "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: +/// - 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 BasisRequest. +/// basis_factory::default_basis_request() centralizes the family/order that +/// matches each supported element's public node layout. +/// - ReferenceNodeLayout (NodeOrderingConventions.h): canonical reference-node +/// coordinates and the output ordering used by every basis evaluator. +/// - BasisTraits.h and 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, basis family, 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 std::vector overloads are +/// convenient for setup, tests, and adapter code. The *_to overloads write to +/// caller-owned spans and are the allocation-free path for assembly. +/// +/// 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 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; + +[[nodiscard]] inline Hessian make_symmetric_hessian(Real xx, + Real yy, + Real zz, + Real xy, + Real xz, + Real 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; +} + +inline void add_scaled_hessian(Hessian& target, + const Hessian& source, + Real scale) noexcept { + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + target(r, c) += scale * source(r, c); + } + } +} + +/// \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. Derived +/// classes provide values at minimum and can override analytical gradients, +/// Hessians, combined evaluation, and span output paths. 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 canonical element type represented by this basis. + /// \return Element type used for node layout and evaluation. + virtual ElementType element_type() 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 Effective polynomial order after any element-family normalization. + 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 Evaluate basis function values at a reference coordinate. + /// \param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + /// \param values Receives one value per basis function. + virtual void evaluate_values(const math::Vector& xi, + std::vector& values) const = 0; + + /// \brief Evaluate basis gradients at a reference coordinate. + /// \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. + virtual void evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const; + + /// \brief Evaluate basis Hessians at a reference coordinate. + /// \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. + virtual void evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const; + + /// \brief Evaluate values, gradients, and Hessians together. + /// \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. + virtual 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. + /// \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; + + /// \brief Evaluate 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. + virtual void evaluate_gradients_to(const math::Vector& xi, + std::span gradients_out) const; + + /// \brief Evaluate 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. + virtual void evaluate_hessians_to(const math::Vector& xi, + std::span hessians_out) const; + +protected: + /// \brief Approximate gradients by centered finite differences of values. + /// + /// \details This helper exists as a development and fallback utility for + /// basis implementations that do not yet provide analytical gradients. It + /// is useful for prototyping new basis families and for checking analytical + /// derivative formulas in tests. Production element assembly should prefer + /// analytical gradients when available because finite differences introduce + /// truncation/roundoff sensitivity and require multiple value evaluations + /// per reference coordinate. + void numerical_gradient(const math::Vector& xi, + std::vector& gradients, + Real eps = Real(1e-6)) const; + + /// \brief Approximate Hessians by centered finite differences of gradients. + /// + /// \details This helper exists for the same reason as numerical_gradient: + /// it provides a simple reference implementation for prototyping and + /// derivative verification when analytical second derivatives are not yet + /// implemented. It depends on evaluate_gradients(), so it is only available + /// for basis implementations that can already provide gradients. Analytical + /// Hessians should be used in performance-sensitive solver paths because + /// finite-difference Hessians amplify numerical error and require repeated + /// gradient evaluations. + void numerical_hessian(const math::Vector& xi, + std::vector& hessians, + Real eps = Real(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..eca5c1c69 --- /dev/null +++ b/Code/Source/solver/FE/Basis/BasisTraits.h @@ -0,0 +1,227 @@ +// 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 + +#include "Types.h" + +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +enum class BasisTopology { + Unknown, + Point, + Line, + Triangle, + Quadrilateral, + Tetrahedron, + Hexahedron, + Wedge, +}; + +namespace detail { + +[[nodiscard]] constexpr Real basis_abs(Real value) noexcept { + return value < Real(0) ? -value : value; +} + +[[nodiscard]] constexpr Real basis_max(Real lhs, Real rhs) noexcept { + return lhs < rhs ? rhs : lhs; +} + +[[nodiscard]] constexpr Real basis_scaled_tolerance(Real scale = Real(1), + Real multiplier = Real(64)) noexcept { + return multiplier * std::numeric_limits::epsilon() * + basis_max(Real(1), basis_abs(scale)); +} + +[[nodiscard]] constexpr bool basis_near_zero(Real value, + Real scale = Real(1), + Real multiplier = Real(64)) noexcept { + return basis_abs(value) <= basis_scaled_tolerance(scale, multiplier); +} + +[[nodiscard]] constexpr bool basis_nearly_equal(Real a, + Real b, + Real multiplier = Real(64)) noexcept { + const Real scale = basis_max(Real(1), basis_max(basis_abs(a), basis_abs(b))); + return basis_abs(a - b) <= basis_scaled_tolerance(scale, multiplier); +} + +} // namespace detail + +[[nodiscard]] constexpr bool is_point(ElementType type) noexcept { + return type == ElementType::Point1; +} + +[[nodiscard]] constexpr bool is_line(ElementType type) noexcept { + return type == ElementType::Line2 || type == ElementType::Line3; +} + +[[nodiscard]] constexpr bool is_triangle(ElementType type) noexcept { + return type == ElementType::Triangle3 || type == ElementType::Triangle6; +} + +[[nodiscard]] constexpr bool is_quadrilateral(ElementType type) noexcept { + return type == ElementType::Quad4 || type == ElementType::Quad8 || + type == ElementType::Quad9; +} + +[[nodiscard]] constexpr bool is_tetrahedron(ElementType type) noexcept { + return type == ElementType::Tetra4 || type == ElementType::Tetra10; +} + +[[nodiscard]] constexpr bool is_hexahedron(ElementType type) noexcept { + return type == ElementType::Hex8 || type == ElementType::Hex20 || + type == ElementType::Hex27; +} + +[[nodiscard]] constexpr bool is_wedge(ElementType type) noexcept { + return type == ElementType::Wedge6 || type == ElementType::Wedge15 || + type == ElementType::Wedge18; +} + +[[nodiscard]] constexpr bool is_pyramid(ElementType type) noexcept { + (void)type; + return false; +} + +[[nodiscard]] constexpr bool is_simplex(ElementType type) noexcept { + return is_triangle(type) || is_tetrahedron(type); +} + +[[nodiscard]] constexpr bool is_tensor_product(ElementType type) noexcept { + return is_line(type) || is_quadrilateral(type) || is_hexahedron(type); +} + +[[nodiscard]] constexpr int reference_dimension(ElementType type) noexcept { + return element_dimension(type); +} + +[[nodiscard]] constexpr BasisTopology topology(ElementType type) noexcept { + if (is_point(type)) { + return BasisTopology::Point; + } + if (is_line(type)) { + return BasisTopology::Line; + } + if (is_triangle(type)) { + return BasisTopology::Triangle; + } + if (is_quadrilateral(type)) { + return BasisTopology::Quadrilateral; + } + if (is_tetrahedron(type)) { + return BasisTopology::Tetrahedron; + } + if (is_hexahedron(type)) { + return BasisTopology::Hexahedron; + } + if (is_wedge(type)) { + return BasisTopology::Wedge; + } + 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: + return -1; + } +} + +[[nodiscard]] constexpr std::size_t line_lagrange_size(int order) noexcept { + return order >= 0 ? static_cast(order + 1) : 0u; +} + +[[nodiscard]] constexpr std::size_t triangle_lagrange_size(int order) noexcept { + return order >= 0 ? static_cast((order + 1) * (order + 2) / 2) : 0u; +} + +[[nodiscard]] constexpr std::size_t quad_lagrange_size(int order) noexcept { + return order >= 0 ? static_cast((order + 1) * (order + 1)) : 0u; +} + +[[nodiscard]] constexpr std::size_t tetra_lagrange_size(int order) noexcept { + return order >= 0 ? static_cast((order + 1) * (order + 2) * (order + 3) / 6) : 0u; +} + +[[nodiscard]] constexpr std::size_t hex_lagrange_size(int order) noexcept { + return order >= 0 ? static_cast((order + 1) * (order + 1) * (order + 1)) : 0u; +} + +[[nodiscard]] constexpr std::size_t wedge_lagrange_size(int order) noexcept { + return triangle_lagrange_size(order) * line_lagrange_size(order); +} + +[[nodiscard]] constexpr std::size_t complete_lagrange_alias_size(ElementType type) noexcept { + const int order = complete_lagrange_alias_order(type); + switch (canonical_lagrange_type(type)) { + case ElementType::Point1: + return 1u; + case ElementType::Line2: + return line_lagrange_size(order); + case ElementType::Triangle3: + return triangle_lagrange_size(order); + case ElementType::Quad4: + return quad_lagrange_size(order); + case ElementType::Tetra4: + return tetra_lagrange_size(order); + case ElementType::Hex8: + return hex_lagrange_size(order); + case ElementType::Wedge6: + return wedge_lagrange_size(order); + default: + return 0u; + } +} + +} // 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..ab5e73ac7 --- /dev/null +++ b/Code/Source/solver/FE/Basis/LagrangeBasis.cpp @@ -0,0 +1,636 @@ +// 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 +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +namespace { + +using Vec3 = math::Vector; + +// Return the equispaced 1D reference coordinate in [-1, 1]. +inline constexpr Real equispaced_pm_one_coord(int i, int order) { + if (order <= 0) { + return Real(0); + } + return Real(-1) + Real(2) * static_cast(i) / static_cast(order); +} + +struct AxisEval { + std::vector value; + std::vector first; + std::vector second; +}; + +struct SimplexEval { + std::vector value; + std::vector gradient; + std::vector hessian; +}; + +struct NormalizedLagrangeRequest { + ElementType element_type; + int order; +}; + +// Validate and return the supported basis topology for a Lagrange element type. +BasisTopology supported_lagrange_topology(ElementType type) { + const BasisTopology top = topology(type); + FE::throw_if(top == BasisTopology::Unknown, SVMP_HERE, + "LagrangeBasis: unsupported element type"); + return top; +} + +// Normalize named higher-order element requests to base Lagrange topologies. +NormalizedLagrangeRequest normalize_lagrange_request(ElementType element_type, int order) { + switch (element_type) { + case ElementType::Line3: + return {ElementType::Line2, std::max(order, 2)}; + case ElementType::Triangle6: + return {ElementType::Triangle3, std::max(order, 2)}; + case ElementType::Quad9: + return {ElementType::Quad4, std::max(order, 2)}; + case ElementType::Tetra10: + return {ElementType::Tetra4, std::max(order, 2)}; + case ElementType::Hex27: + return {ElementType::Hex8, std::max(order, 2)}; + case ElementType::Wedge18: + return {ElementType::Wedge6, std::max(order, 2)}; + case ElementType::Quad8: + FE::raise(SVMP_HERE, + "LagrangeBasis: Quad8 is serendipity; use SerendipityBasis"); + case ElementType::Hex20: + FE::raise(SVMP_HERE, + "LagrangeBasis: Hex20 is serendipity; use SerendipityBasis"); + case ElementType::Wedge15: + FE::raise(SVMP_HERE, + "LagrangeBasis: Wedge15 is serendipity; use SerendipityBasis"); + case ElementType::Pyramid5: + case ElementType::Pyramid13: + case ElementType::Pyramid14: + FE::raise(SVMP_HERE, + "LagrangeBasis: pyramid support is not within the current solver basis scope"); + default: + return {element_type, order}; + } +} + +// Convert a coordinate on [-1, 1] to an equispaced axis node index. +std::size_t axis_index_pm_one(Real coord, int order) { + if (order <= 0) { + return 0u; + } + const Real scaled = (coord + Real(1)) * Real(order) / Real(2); + return static_cast(std::llround(scaled)); +} + +// Convert a simplex barycentric coordinate to a lattice index. +int simplex_lattice_index(Real value, int order) { + if (order <= 0) { + return 0; + } + return static_cast(std::llround(value * Real(order))); +} + +// Compute simplex interpolation exponents from a reference node. +LagrangeBasis::SimplexExponent simplex_exponent_from_point(const Vec3& p, + BasisTopology top, + int order) { + LagrangeBasis::SimplexExponent e{0, 0, 0, 0}; + if (order <= 0) { + return e; + } + if (top == BasisTopology::Triangle) { + e[1] = simplex_lattice_index(p[0], order); + e[2] = simplex_lattice_index(p[1], order); + e[0] = order - e[1] - e[2]; + } else { + e[1] = simplex_lattice_index(p[0], order); + e[2] = simplex_lattice_index(p[1], order); + e[3] = simplex_lattice_index(p[2], order); + e[0] = order - e[1] - e[2] - e[3]; + } + return e; +} + +// Sentinel node index meaning "skip nothing" in product_excluding below. +constexpr std::size_t kNoSkip = std::numeric_limits::max(); + +// Evaluate 1D Lagrange polynomials and derivatives at a point. +void evaluate_1d_lagrange(Real x, const std::vector& nodes, AxisEval& out) { + const std::size_t n = nodes.size(); + out.value.assign(n, Real(0)); + out.first.assign(n, Real(0)); + out.second.assign(n, Real(0)); + + if (n == 1u) { + out.value[0] = Real(1); + return; + } + + for (std::size_t i = 0; i < n; ++i) { + // Product of (x - nodes[j]) over all j except i and the listed skips. + // Each derivative order drops one additional factor from the product. + const auto product_excluding = [&](std::size_t skip1 = kNoSkip, + std::size_t skip2 = kNoSkip) { + Real product = Real(1); + for (std::size_t j = 0; j < n; ++j) { + if (j != i && j != skip1 && j != skip2) { + product *= x - nodes[j]; + } + } + return product; + }; + + Real denom = Real(1); + for (std::size_t j = 0; j < n; ++j) { + if (j != i) { + denom *= nodes[i] - nodes[j]; + } + } + + out.value[i] = product_excluding() / denom; + + Real first = Real(0); + for (std::size_t m = 0; m < n; ++m) { + if (m != i) { + first += product_excluding(m); + } + } + out.first[i] = first / denom; + + Real second = Real(0); + for (std::size_t m = 0; m < n; ++m) { + if (m == i) { + continue; + } + for (std::size_t l = 0; l < n; ++l) { + if (l != i && l != m) { + second += product_excluding(m, l); + } + } + } + out.second[i] = second / denom; + } +} + +// Evaluate one barycentric polynomial factor and derivatives. +std::array simplex_factor(int alpha, Real lambda, int order) { + Real value = Real(1); + Real first = Real(0); + Real second = Real(0); + + for (int m = 0; m < alpha; ++m) { + const Real factor = Real(order) * lambda - Real(m); + const Real inv = Real(1) / Real(m + 1); + const Real old_value = value; + const Real old_first = first; + const Real old_second = second; + value = old_value * factor * inv; + first = (old_first * factor + old_value * Real(order)) * inv; + second = (old_second * factor + Real(2) * old_first * Real(order)) * inv; + } + + return {value, first, second}; +} + +// Evaluate simplex Lagrange basis functions and derivatives. +void evaluate_simplex(const Vec3& xi, + BasisTopology top, + int order, + const std::vector& exponents, + SimplexEval& out) { + const std::size_t n = exponents.size(); + out.value.assign(n, Real(0)); + out.gradient.assign(n, Gradient::Zero()); + out.hessian.assign(n, Hessian::Zero()); + + if (n == 1u && order == 0) { + out.value[0] = Real(1); + return; + } + + const std::size_t bary_count = top == BasisTopology::Triangle ? 3u : 4u; + std::array lambda{Real(0), Real(0), Real(0), Real(0)}; + std::array lambda_grad; + lambda_grad.fill(Gradient::Zero()); + + lambda[1] = xi[0]; + lambda[2] = xi[1]; + lambda_grad[1][0] = Real(1); + lambda_grad[2][1] = Real(1); + if (top == BasisTopology::Triangle) { + lambda[0] = Real(1) - xi[0] - xi[1]; + lambda_grad[0][0] = Real(-1); + lambda_grad[0][1] = Real(-1); + } else { + lambda[3] = xi[2]; + lambda[0] = Real(1) - xi[0] - xi[1] - xi[2]; + lambda_grad[0][0] = Real(-1); + lambda_grad[0][1] = Real(-1); + lambda_grad[0][2] = Real(-1); + lambda_grad[3][2] = Real(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); + } + + Real value = Real(1); + for (std::size_t a = 0; a < bary_count; ++a) { + value *= f[a][0]; + } + out.value[i] = value; + + for (std::size_t a = 0; a < bary_count; ++a) { + Real 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]; + } + } + + for (std::size_t a = 0; a < bary_count; ++a) { + for (std::size_t b = 0; b < bary_count; ++b) { + Real 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]; + } + } + } + } + } +} + +void require_output_span_size(std::size_t actual, + std::size_t expected, + const char* label) { + FE::throw_if(actual < expected, SVMP_HERE, + std::string(label) + ": output span is smaller than basis size"); +} + +template +void require_requested_span_size(std::span output, + std::size_t expected, + const char* label) { + if (!output.empty()) { + require_output_span_size(output.size(), expected, label); + } +} + +} // namespace + +LagrangeBasis::LagrangeBasis(ElementType type, int order) + : element_type_(type), order_(order) { + const auto normalized = normalize_lagrange_request(element_type_, order_); + element_type_ = normalized.element_type; + order_ = normalized.order; + FE::throw_if(order_ < 0, SVMP_HERE, + "LagrangeBasis requires non-negative polynomial order"); + + topology_ = supported_lagrange_topology(element_type_); + dimension_ = reference_dimension(element_type_); + init_nodes(); +} + +// Initialize equispaced 1D interpolation nodes for tensor-product axes. +void LagrangeBasis::init_equispaced_1d_nodes() { + nodes_1d_.resize(static_cast(order_ + 1)); + for (int i = 0; i <= order_; ++i) { + nodes_1d_[static_cast(i)] = + equispaced_pm_one_coord(i, order_); + } +} + +// Initialize reference nodes and topology-specific lookup data. +void LagrangeBasis::init_nodes() { + nodes_.clear(); + nodes_1d_.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; + } + + FE::raise(SVMP_HERE, + "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{Real(0), Real(0), Real(0)}); +} + +// Build nodes and axis indices for tensor-product elements. +void LagrangeBasis::build_tensor_product_nodes() { + init_equispaced_1d_nodes(); + nodes_ = ReferenceNodeLayout::get_lagrange_node_coords(element_type_, order_); + tensor_indices_.reserve(nodes_.size()); + for (const auto& node : nodes_) { + TensorNodeIndex idx{0u, 0u, 0u}; + idx[0] = axis_index_pm_one(node[0], order_); + if (dimension_ >= 2) { + idx[1] = axis_index_pm_one(node[1], order_); + } + if (dimension_ >= 3) { + idx[2] = axis_index_pm_one(node[2], order_); + } + tensor_indices_.push_back(idx); + } +} + +// Build nodes and barycentric exponents for simplex elements. +void LagrangeBasis::build_simplex_nodes() { + nodes_ = ReferenceNodeLayout::get_lagrange_node_coords(element_type_, order_); + simplex_exponents_.reserve(nodes_.size()); + for (const auto& node : nodes_) { + simplex_exponents_.push_back(simplex_exponent_from_point(node, topology_, order_)); + } +} + +// Build nodes and mixed triangle-axis lookup data for wedge elements. +void LagrangeBasis::build_wedge_nodes() { + init_equispaced_1d_nodes(); + nodes_ = ReferenceNodeLayout::get_lagrange_node_coords(element_type_, order_); + const auto tri_nodes = + ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Triangle3, order_); + simplex_exponents_.reserve(tri_nodes.size()); + for (const auto& tri_node : tri_nodes) { + simplex_exponents_.push_back( + simplex_exponent_from_point(tri_node, BasisTopology::Triangle, order_)); + } + + wedge_indices_.reserve(nodes_.size()); + for (const auto& node : nodes_) { + const auto tri_exp = + simplex_exponent_from_point(node, BasisTopology::Triangle, order_); + auto it = std::find(simplex_exponents_.begin(), simplex_exponents_.end(), tri_exp); + FE::throw_if(it == simplex_exponents_.end(), SVMP_HERE, + "LagrangeBasis: wedge node triangle index lookup failed"); + const std::size_t tri_index = + static_cast(std::distance(simplex_exponents_.begin(), it)); + wedge_indices_.push_back({tri_index, axis_index_pm_one(node[2], order_)}); + } +} + +// 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] = Real(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 { + AxisEval ax; + AxisEval ay; + AxisEval az; + evaluate_1d_lagrange(xi[0], nodes_1d_, ax); + if (dimension_ >= 2) { + evaluate_1d_lagrange(xi[1], nodes_1d_, ay); + } + if (dimension_ >= 3) { + evaluate_1d_lagrange(xi[2], nodes_1d_, az); + } + + for (std::size_t node = 0; node < tensor_indices_.size(); ++node) { + const auto& idx = tensor_indices_[node]; + const Real vx = ax.value[idx[0]]; + const Real dx = ax.first[idx[0]]; + const Real d2x = ax.second[idx[0]]; + const Real vy = dimension_ >= 2 ? ay.value[idx[1]] : Real(1); + const Real dy = dimension_ >= 2 ? ay.first[idx[1]] : Real(0); + const Real d2y = dimension_ >= 2 ? ay.second[idx[1]] : Real(0); + const Real vz = dimension_ >= 3 ? az.value[idx[2]] : Real(1); + const Real dz = dimension_ >= 3 ? az.first[idx[2]] : Real(0); + const Real d2z = dimension_ >= 3 ? az.second[idx[2]] : Real(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 { + SimplexEval simplex; + evaluate_simplex(xi, topology_, order_, simplex_exponents_, simplex); + for (std::size_t i = 0; i < simplex.value.size(); ++i) { + if (!values_out.empty()) { + values_out[i] = simplex.value[i]; + } + if (!gradients_out.empty()) { + gradients_out[i] = simplex.gradient[i]; + } + if (!hessians_out.empty()) { + 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 { + SimplexEval tri; + AxisEval z_axis; + evaluate_simplex(xi, BasisTopology::Triangle, order_, simplex_exponents_, tri); + evaluate_1d_lagrange(xi[2], nodes_1d_, z_axis); + + for (std::size_t node = 0; node < wedge_indices_.size(); ++node) { + const auto [tri_idx, z_idx] = wedge_indices_[node]; + const Real tv = tri.value[tri_idx]; + const Real zv = z_axis.value[z_idx]; + const Real dz = z_axis.first[z_idx]; + const Real d2z = z_axis.second[z_idx]; + + if (!values_out.empty()) { + values_out[node] = tv * zv; + } + if (!gradients_out.empty()) { + 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 (!hessians_out.empty()) { + 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 { + require_requested_span_size(values_out, size(), "LagrangeBasis::evaluate_all_to values"); + require_requested_span_size(gradients_out, size(), "LagrangeBasis::evaluate_all_to gradients"); + require_requested_span_size(hessians_out, size(), "LagrangeBasis::evaluate_all_to hessians"); + + 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; + } + + FE::raise(SVMP_HERE, + "Unsupported element in LagrangeBasis evaluation"); +} + +void LagrangeBasis::evaluate_values(const Vec3& xi, + std::vector& values) const { + values.resize(size()); + evaluate_values_to(xi, std::span(values.data(), values.size())); +} + +void LagrangeBasis::evaluate_gradients(const Vec3& xi, + std::vector& gradients) const { + gradients.resize(size()); + evaluate_gradients_to(xi, std::span(gradients.data(), gradients.size())); +} + +void LagrangeBasis::evaluate_hessians(const Vec3& xi, + std::vector& hessians) const { + hessians.resize(size()); + evaluate_hessians_to(xi, std::span(hessians.data(), hessians.size())); +} + +void LagrangeBasis::evaluate_all(const Vec3& 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())); +} + +void LagrangeBasis::evaluate_values_to(const Vec3& xi, + std::span values_out) const { + require_output_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_output_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_output_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..6137a557a --- /dev/null +++ b/Code/Source/solver/FE/Basis/LagrangeBasis.h @@ -0,0 +1,252 @@ +// 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 nodal interpolation basis associated +/// with an equispaced reference-node lattice. It supports point, line, +/// quadrilateral, hexahedron, triangle, tetrahedron, and wedge reference +/// elements. Named complete quadratic elements such as Line3, Triangle6, +/// Quad9, Tetra10, Hex27, and Wedge18 are normalized to their canonical +/// linear topology plus effective order 2. +/// +/// 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 equispaced coordinates in \f$[-1, 1]\f$. 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. 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$. +/// +/// 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 : 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 for an element type and polynomial order. + /// + /// \details The constructor normalizes complete higher-order aliases to the + /// canonical topology and effective polynomial order, builds the reference + /// node coordinates, and precomputes 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. + /// + /// \param type Element type used to determine topology and reference-node layout. + /// \param order Requested polynomial order. + /// \throws BasisConfigurationException If the effective order is negative. + /// \throws BasisElementCompatibilityException If the element type is unsupported. + LagrangeBasis(ElementType type, int order); + + /// \copydoc BasisFunction::basis_type() + BasisType basis_type() const noexcept override { return BasisType::Lagrange; } + + /// \copydoc BasisFunction::element_type() + ElementType element_type() const noexcept override { return element_type_; } + + /// \copydoc BasisFunction::dimension() + int dimension() const noexcept override { return dimension_; } + + /// \copydoc BasisFunction::order() + int order() const noexcept override { return order_; } + + /// \copydoc BasisFunction::size() + std::size_t size() const noexcept override { 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. Coordinates are reference-element coordinates: + /// tensor-product axes use \f$[-1,1]\f$, triangles and tetrahedra use the + /// repository's simplex reference coordinates, and wedges combine triangle + /// reference coordinates with a \f$[-1,1]\f$ through-axis coordinate. + /// + /// \return Reference node coordinates, one per basis function. + const std::vector>& nodes() const noexcept { return nodes_; } + + /// \brief Evaluate Lagrange basis function values at a reference coordinate. + /// + /// \details Values satisfy the nodal interpolation property + /// \f$N_i(x_j)=\delta_{ij}\f$ at the basis nodes. Tensor-product values are + /// products of one-dimensional Lagrange polynomials. Simplex values are + /// products of barycentric falling-factorial factors. Wedge values are + /// products of triangle simplex values and through-axis Lagrange values. + /// + /// \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 final; + + /// \brief Evaluate analytical Lagrange basis gradients at a reference coordinate. + /// + /// \details Gradients are derivatives with respect to reference + /// coordinates, not physical coordinates. Tensor-product gradients apply + /// the product rule to the active axis polynomials. Simplex gradients + /// differentiate the barycentric factors and multiply by the constant + /// gradients of the barycentric coordinates. Wedge gradients combine the + /// triangle gradient in the first two components with the through-axis + /// derivative in the third component. + /// + /// \param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + /// \param gradients Receives one three-component gradient per basis function. + void evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const final; + + /// \brief Evaluate analytical Lagrange basis Hessians at a reference coordinate. + /// + /// \details Hessians are second derivatives in reference coordinates and + /// are stored as 3-by-3 matrices. Tensor-product Hessians contain pure + /// second axis derivatives on the diagonal and mixed product-rule terms + /// off diagonal. Simplex Hessians are assembled from first and second + /// derivatives of the barycentric factors. Wedge Hessians contain triangle + /// Hessian terms, through-axis second derivatives, and mixed + /// triangle/through-axis derivative products. + /// + /// \param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + /// \param hessians Receives one 3-by-3 Hessian per basis function. + void evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const final; + + /// \brief Evaluate Lagrange values, gradients, and Hessians together. + /// + /// \details This is the allocation-friendly vector API for callers that + /// need all basis quantities at the same quadrature point. The underlying + /// evaluator computes only topology-local polynomial data once and then + /// fills all requested outputs. + /// + /// \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 final; + + /// \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: + ElementType element_type_; + BasisTopology topology_{BasisTopology::Unknown}; + int dimension_{0}; + int order_{0}; + + std::vector nodes_1d_; + 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_equispaced_1d_nodes(); + + void evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const; + 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..850f8cd0a --- /dev/null +++ b/Code/Source/solver/FE/Basis/NodeOrderingConventions.cpp @@ -0,0 +1,416 @@ +// 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 + +namespace svmp { +namespace FE { +namespace basis { + +namespace { + +using Point = math::Vector; + +constexpr std::array kHex20MeshToBasisOrder = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 13, 10, 12, + 9, 15, 11, 14, + 16, 17, 19, 18 +}; + +Real line_coord_pm_one(int i, int order) { + if (order <= 0) { + return Real(0); + } + return Real(-1) + Real(2) * static_cast(i) / static_cast(order); +} + +Real line_coord_zero_one(int i, int order) { + if (order <= 0) { + return Real(0); + } + return static_cast(i) / static_cast(order); +} + +void append_triangle_face_interior(std::vector& nodes, + const Point& v0, + const Point& v1, + const Point& v2, + 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 Real inv = Real(1) / Real(order); + nodes.push_back(v0 * (Real(a) * inv) + + v1 * (Real(b) * inv) + + v2 * (Real(c) * inv)); + } + } +} + +std::vector generate_line_nodes(int order) { + if (order == 0) { + return {Point{Real(0), Real(0), Real(0)}}; + } + + std::vector nodes; + nodes.reserve(static_cast(order + 1)); + nodes.push_back(Point{Real(-1), Real(0), Real(0)}); + nodes.push_back(Point{Real(1), Real(0), Real(0)}); + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), Real(0), Real(0)}); + } + return nodes; +} + +std::vector generate_triangle_nodes(int order) { + if (order == 0) { + return {Point{Real(1) / Real(3), Real(1) / Real(3), Real(0)}}; + } + + std::vector nodes; + nodes.reserve(static_cast((order + 1) * (order + 2) / 2)); + nodes.push_back(Point{Real(0), Real(0), Real(0)}); + nodes.push_back(Point{Real(1), Real(0), Real(0)}); + nodes.push_back(Point{Real(0), Real(1), Real(0)}); + + for (int m = 1; m < order; ++m) { + nodes.push_back(Point{line_coord_zero_one(m, order), Real(0), Real(0)}); + } + for (int m = 1; m < order; ++m) { + nodes.push_back(Point{line_coord_zero_one(order - m, order), + line_coord_zero_one(m, order), Real(0)}); + } + for (int m = 1; m < order; ++m) { + nodes.push_back(Point{Real(0), line_coord_zero_one(order - m, order), Real(0)}); + } + + append_triangle_face_interior(nodes, + Point{Real(0), Real(0), Real(0)}, + Point{Real(1), Real(0), Real(0)}, + Point{Real(0), Real(1), Real(0)}, + order); + return nodes; +} + +std::vector generate_quad_nodes(int order) { + if (order == 0) { + return {Point{Real(0), Real(0), Real(0)}}; + } + + std::vector nodes; + nodes.reserve(static_cast((order + 1) * (order + 1))); + nodes.push_back(Point{Real(-1), Real(-1), Real(0)}); + nodes.push_back(Point{Real(1), Real(-1), Real(0)}); + nodes.push_back(Point{Real(1), Real(1), Real(0)}); + nodes.push_back(Point{Real(-1), Real(1), Real(0)}); + + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), Real(-1), Real(0)}); + } + for (int j = 1; j < order; ++j) { + nodes.push_back(Point{Real(1), line_coord_pm_one(j, order), Real(0)}); + } + for (int i = order - 1; i >= 1; --i) { + nodes.push_back(Point{line_coord_pm_one(i, order), Real(1), Real(0)}); + } + for (int j = order - 1; j >= 1; --j) { + nodes.push_back(Point{Real(-1), line_coord_pm_one(j, order), Real(0)}); + } + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), + line_coord_pm_one(j, order), Real(0)}); + } + } + return nodes; +} + +std::vector generate_tetra_nodes(int order) { + if (order == 0) { + return {Point{Real(0.25), Real(0.25), Real(0.25)}}; + } + + const Point verts[] = { + Point{Real(0), Real(0), Real(0)}, + Point{Real(1), Real(0), Real(0)}, + Point{Real(0), Real(1), Real(0)}, + Point{Real(0), Real(0), Real(1)}, + }; + + std::vector nodes; + nodes.reserve(static_cast((order + 1) * (order + 2) * (order + 3) / 6)); + for (const auto& v : verts) { + nodes.push_back(v); + } + + 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 Real t = static_cast(m) / static_cast(order); + nodes.push_back(verts[edge[0]] * (Real(1) - t) + verts[edge[1]] * t); + } + } + + 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(nodes, + verts[face[0]], + verts[face[1]], + verts[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) { + nodes.push_back(Point{Real(j) / Real(order), + Real(k) / Real(order), + Real(l) / Real(order)}); + } + } + } + return nodes; +} + +std::vector generate_hex_nodes(int order) { + if (order == 0) { + return {Point{Real(0), Real(0), Real(0)}}; + } + + const Point verts[] = { + Point{Real(-1), Real(-1), Real(-1)}, + Point{Real(1), Real(-1), Real(-1)}, + Point{Real(1), Real(1), Real(-1)}, + Point{Real(-1), Real(1), Real(-1)}, + Point{Real(-1), Real(-1), Real(1)}, + Point{Real(1), Real(-1), Real(1)}, + Point{Real(1), Real(1), Real(1)}, + Point{Real(-1), Real(1), Real(1)}, + }; + + std::vector nodes; + nodes.reserve(static_cast((order + 1) * (order + 1) * (order + 1))); + for (const auto& v : verts) { + nodes.push_back(v); + } + + 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}, + }; + for (const auto& edge : edges) { + for (int m = 1; m < order; ++m) { + const Real t = static_cast(m) / static_cast(order); + nodes.push_back(verts[edge[0]] * (Real(1) - t) + verts[edge[1]] * t); + } + } + + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), line_coord_pm_one(j, order), Real(-1)}); + } + } + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), line_coord_pm_one(j, order), Real(1)}); + } + } + for (int k = 1; k < order; ++k) { + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), Real(-1), line_coord_pm_one(k, order)}); + } + } + for (int k = 1; k < order; ++k) { + for (int j = 1; j < order; ++j) { + nodes.push_back(Point{Real(1), line_coord_pm_one(j, order), line_coord_pm_one(k, order)}); + } + } + for (int k = 1; k < order; ++k) { + for (int i = order - 1; i >= 1; --i) { + nodes.push_back(Point{line_coord_pm_one(i, order), Real(1), line_coord_pm_one(k, order)}); + } + } + for (int k = 1; k < order; ++k) { + for (int j = order - 1; j >= 1; --j) { + nodes.push_back(Point{Real(-1), line_coord_pm_one(j, order), line_coord_pm_one(k, order)}); + } + } + for (int k = 1; k < order; ++k) { + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + nodes.push_back(Point{line_coord_pm_one(i, order), + line_coord_pm_one(j, order), + line_coord_pm_one(k, order)}); + } + } + } + return nodes; +} + +std::vector generate_wedge_nodes(int order) { + if (order == 0) { + return {Point{Real(1) / Real(3), Real(1) / Real(3), Real(0)}}; + } + + const Point verts[] = { + Point{Real(0), Real(0), Real(-1)}, + Point{Real(1), Real(0), Real(-1)}, + Point{Real(0), Real(1), Real(-1)}, + Point{Real(0), Real(0), Real(1)}, + Point{Real(1), Real(0), Real(1)}, + Point{Real(0), Real(1), Real(1)}, + }; + + std::vector nodes; + nodes.reserve(static_cast((order + 1) * (order + 1) * (order + 2) / 2)); + for (const auto& v : verts) { + nodes.push_back(v); + } + + const int edges[9][2] = { + {0, 1}, {1, 2}, {2, 0}, + {3, 4}, {4, 5}, {5, 3}, + {0, 3}, {1, 4}, {2, 5}, + }; + for (const auto& edge : edges) { + for (int m = 1; m < order; ++m) { + const Real t = static_cast(m) / static_cast(order); + nodes.push_back(verts[edge[0]] * (Real(1) - t) + verts[edge[1]] * t); + } + } + + append_triangle_face_interior(nodes, verts[0], verts[1], verts[2], order); + append_triangle_face_interior(nodes, verts[3], verts[4], verts[5], order); + + for (int r = 1; r < order; ++r) { + const Real z = line_coord_pm_one(r, order); + for (int m = 1; m < order; ++m) { + const Real t = static_cast(m) / static_cast(order); + nodes.push_back(Point{t, Real(0), z}); + } + for (int m = 1; m < order; ++m) { + const Real t = static_cast(m) / static_cast(order); + nodes.push_back(Point{Real(1) - t, t, z}); + } + for (int m = 1; m < order; ++m) { + const Real t = static_cast(m) / static_cast(order); + nodes.push_back(Point{Real(0), Real(1) - t, z}); + } + } + + for (int r = 1; r < order; ++r) { + const Real z = line_coord_pm_one(r, order); + for (int c = 1; c <= order - 2; ++c) { + for (int b = 1; b <= order - c - 1; ++b) { + nodes.push_back(Point{Real(b) / Real(order), + Real(c) / Real(order), + z}); + } + } + } + return nodes; +} + +std::vector complete_lagrange_nodes(ElementType canonical_type, int order) { + FE::throw_if(order < 0, SVMP_HERE, + "ReferenceNodeLayout requires non-negative Lagrange order"); + const ElementType type = canonical_lagrange_type(canonical_type); + switch (type) { + case ElementType::Point1: + return {Point{Real(0), Real(0), Real(0)}}; + 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: + FE::raise(SVMP_HERE, + "ReferenceNodeLayout: pyramid node ordering is disabled"); + default: + FE::raise(SVMP_HERE, + "ReferenceNodeLayout: unsupported Lagrange topology"); + } +} + +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); + } + + switch (elem_type) { + case ElementType::Quad8: { + auto nodes = generate_quad_nodes(2); + nodes.resize(8u); + return nodes; + } + case ElementType::Hex20: { + auto nodes = generate_hex_nodes(2); + nodes.resize(20u); + return nodes; + } + case ElementType::Wedge15: { + auto nodes = generate_wedge_nodes(2); + nodes.resize(15u); + return nodes; + } + case ElementType::Pyramid13: + FE::raise(SVMP_HERE, + "ReferenceNodeLayout: pyramid node ordering is disabled"); + default: + FE::raise(SVMP_HERE, + "ReferenceNodeLayout: unknown element type"); + } +} + +} // namespace + +math::Vector ReferenceNodeLayout::get_node_coords(ElementType elem_type, + std::size_t local_node) { + const auto nodes = element_nodes(elem_type); + FE::throw_if(local_node >= nodes.size(), SVMP_HERE, + "ReferenceNodeLayout::get_node_coords: 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::get_lagrange_node_coords(ElementType canonical_type, int order) { + return complete_lagrange_nodes(canonical_type, order); +} + +std::span ReferenceNodeLayout::mesh_to_basis_ordering(ElementType elem_type) { + if (elem_type == ElementType::Hex20) { + return std::span(kHex20MeshToBasisOrder.data(), + kHex20MeshToBasisOrder.size()); + } + return {}; +} + +bool ReferenceNodeLayout::is_simplex(ElementType elem_type) { + return svmp::FE::basis::is_simplex(elem_type); +} + +bool ReferenceNodeLayout::is_tensor_product(ElementType elem_type) { + return svmp::FE::basis::is_tensor_product(elem_type); +} + +} // 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..4b11cca32 --- /dev/null +++ b/Code/Source/solver/FE/Basis/NodeOrderingConventions.h @@ -0,0 +1,36 @@ +// 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 "Math/Vector.h" +#include "Types.h" + +#include +#include +#include + +namespace svmp { +namespace FE { +namespace basis { + +class ReferenceNodeLayout { +public: + static math::Vector get_node_coords(ElementType elem_type, + std::size_t local_node); + static std::size_t num_nodes(ElementType elem_type); + + static std::vector> + get_lagrange_node_coords(ElementType canonical_type, int order); + + static std::span mesh_to_basis_ordering(ElementType elem_type); + static bool is_simplex(ElementType elem_type); + static bool is_tensor_product(ElementType elem_type); +}; + +} // 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..ae505c2cf --- /dev/null +++ b/Code/Source/solver/FE/Basis/SerendipityBasis.cpp @@ -0,0 +1,728 @@ +// 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; + +void evaluate_hex8_reference(Real r, + Real s, + Real t, + std::span values, + std::span gradients, + std::span hessians) { + static constexpr int signs[8][3] = { + {-1, -1, -1}, + { 1, -1, -1}, + { 1, 1, -1}, + {-1, 1, -1}, + {-1, -1, 1}, + { 1, -1, 1}, + { 1, 1, 1}, + {-1, 1, 1}, + }; + + for (std::size_t i = 0; i < 8u; ++i) { + const Real a = Real(signs[i][0]); + const Real b = Real(signs[i][1]); + const Real c = Real(signs[i][2]); + const Real ar = Real(1) + a * r; + const Real bs = Real(1) + b * s; + const Real ct = Real(1) + c * t; + + if (!values.empty()) { + values[i] = Real(0.125) * ar * bs * ct; + } + if (!gradients.empty()) { + Gradient& g = gradients[i]; + g[0] = Real(0.125) * a * bs * ct; + g[1] = Real(0.125) * b * ar * ct; + g[2] = Real(0.125) * c * ar * bs; + } + if (!hessians.empty()) { + Hessian& h = hessians[i]; + h(0, 0) = Real(0); + h(0, 1) = Real(0.125) * a * b * ct; + h(0, 2) = Real(0.125) * a * c * bs; + h(1, 0) = h(0, 1); + h(1, 1) = Real(0); + h(1, 2) = Real(0.125) * b * c * ar; + h(2, 0) = h(0, 2); + h(2, 1) = h(1, 2); + h(2, 2) = Real(0); + } + } +} + +int quad_serendipity_superlinear_degree(int ax, int ay) { + return (ax > 1 ? ax : 0) + (ay > 1 ? ay : 0); +} + +std::vector> quad_serendipity_exponents(int order) { + std::vector> exponents; + for (int ay = 0; ay <= order; ++ay) { + for (int ax = 0; ax <= order; ++ax) { + if (quad_serendipity_superlinear_degree(ax, ay) <= order) { + exponents.push_back({ax, ay}); + } + } + } + return exponents; +} + +std::vector quad_serendipity_nodes(int order, std::size_t total_size) { + std::vector nodes; + if (order <= 0) { + return nodes; + } + + const Real inv_order = Real(1) / Real(order); + + nodes.push_back(Vec3{Real(-1), Real(-1), Real(0)}); + nodes.push_back(Vec3{Real(1), Real(-1), Real(0)}); + nodes.push_back(Vec3{Real(1), Real(1), Real(0)}); + nodes.push_back(Vec3{Real(-1), Real(1), Real(0)}); + + for (int i = 1; i < order; ++i) { + nodes.push_back(Vec3{Real(-1) + Real(2 * i) * inv_order, Real(-1), Real(0)}); + } + for (int i = 1; i < order; ++i) { + nodes.push_back(Vec3{Real(1), Real(-1) + Real(2 * i) * inv_order, Real(0)}); + } + for (int i = 1; i < order; ++i) { + nodes.push_back(Vec3{Real(1) - Real(2 * i) * inv_order, Real(1), Real(0)}); + } + for (int i = 1; i < order; ++i) { + nodes.push_back(Vec3{Real(-1), Real(1) - Real(2 * i) * inv_order, Real(0)}); + } + + FE::throw_if( + nodes.size() > total_size, SVMP_HERE, + "SerendipityBasis: quadrilateral serendipity boundary nodes exceed requested size"); + + const std::size_t interior_count = total_size - nodes.size(); + if (interior_count == 0u) { + return nodes; + } + + std::vector interior_candidates; + interior_candidates.reserve(static_cast((order - 1) * (order - 1))); + for (int j = 1; j < order; ++j) { + for (int i = 1; i < order; ++i) { + interior_candidates.push_back( + Vec3{Real(-1) + Real(2 * i) * inv_order, + Real(-1) + Real(2 * j) * inv_order, + Real(0)}); + } + } + + std::sort(interior_candidates.begin(), interior_candidates.end(), + [](const Vec3& a, const Vec3& b) { + const Real a_linf = std::max(std::abs(a[0]), std::abs(a[1])); + const Real b_linf = std::max(std::abs(b[0]), std::abs(b[1])); + if (a_linf != b_linf) { + return a_linf < b_linf; + } + + const Real a_l1 = std::abs(a[0]) + std::abs(a[1]); + const Real b_l1 = std::abs(b[0]) + std::abs(b[1]); + if (a_l1 != b_l1) { + return a_l1 < b_l1; + } + + if (a[1] != b[1]) { + return a[1] < b[1]; + } + return a[0] < b[0]; + }); + + FE::throw_if( + interior_count > interior_candidates.size(), SVMP_HERE, + "SerendipityBasis: insufficient quadrilateral interior nodes for requested serendipity order"); + + nodes.insert(nodes.end(), + interior_candidates.begin(), + interior_candidates.begin() + static_cast(interior_count)); + return nodes; +} + +std::vector invert_dense_matrix(std::vector matrix, int n, const char* label) { + return math::invert_dense_matrix( + std::move(matrix), + static_cast(n), + std::string("SerendipityBasis interpolation matrix for ") + label); +} + +std::vector quad_serendipity_inverse_vandermonde( + std::span nodes, + std::span> exponents, + int order) { + const int n = static_cast(nodes.size()); + FE::throw_if( + n == 0 || exponents.size() != nodes.size(), SVMP_HERE, + "SerendipityBasis: invalid quadrilateral serendipity interpolation setup"); + + std::vector vandermonde(static_cast(n * n), Real(0)); + auto idx = [n](int row, int col) -> std::size_t { + return static_cast(row * n + col); + }; + + for (int row = 0; row < n; ++row) { + const Real x = nodes[static_cast(row)][0]; + const Real y = nodes[static_cast(row)][1]; + for (int col = 0; col < n; ++col) { + const auto [ax, ay] = exponents[static_cast(col)]; + vandermonde[idx(row, col)] = std::pow(x, ax) * std::pow(y, ay); + } + } + + const std::string label = "Quad order " + std::to_string(order); + return invert_dense_matrix(std::move(vandermonde), n, label.c_str()); +} +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}} +}}; + +constexpr std::array, 15> kWedge15Coefficients = {{ + {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0}}, + {{-0.5, 0, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {{0.5, -0, -0, 0.5, -0, -0, -0, -0, -0, -0, -0, -0, -1, -0, -0}}, + {{-1, 0, -1, -1, 0, -1, 0, 0, 2, 0, 0, 2, -1, 0, 1}}, + {{1.5, 0, 0.5, -1.5, 0, -0.5, 0, 0, -2, 0, 0, 2, 0, 0, 0}}, + {{-0.5, -0, 0.5, -0.5, -0, 0.5, -0, -0, -0, -0, -0, -0, 1, -0, -1}}, + {{1, 0, 1, 1, 0, 1, 0, 0, -2, 0, 0, -2, 0, 0, 0}}, + {{-1, 0, -1, 1, 0, 1, 0, 0, 2, 0, 0, -2, 0, 0, 0}}, + {{-1, -1, 0, -1, -1, 0, 2, 0, 0, 2, 0, 0, -1, 1, 0}}, + {{1.5, 0.5, 0, -1.5, -0.5, 0, -2, 0, 0, 2, 0, 0, 0, 0, 0}}, + {{-0.5, 0.5, -0, -0.5, 0.5, -0, -0, -0, -0, -0, -0, -0, 1, -1, -0}}, + {{2, 0, -0, 2, 0, -0, -2, 2, -2, -2, 2, -2, -0, -0, -0}}, + {{-2, 0, 0, 2, 0, 0, 2, -2, 2, -2, 2, -2, 0, 0, 0}}, + {{1, 1, -0, 1, 1, -0, -2, -0, -0, -2, -0, -0, -0, -0, -0}}, + {{-1, -1, -0, 1, 1, -0, 2, -0, -0, -2, -0, -0, -0, -0, -0}} +}}; + +static const int hex20_monomial_exponents[20][3] = { + {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}, {1, 1, 2}, {1, 2, 0}, + {1, 2, 1}, {2, 0, 0}, {2, 0, 1}, {2, 1, 0}, {2, 1, 1} +}; + +static const Real hex20_coeffs[20][20] = { + {-0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25}, + {0.125, 0.125, 0.125, 0.125, -0.125, -0.125, -0.125, -0.125, -0.25, 0.25, -0.25, 0.25, -0.25, -0.25, 0.25, 0.25, 0, 0, 0, 0}, + {0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0, 0, 0, 0, 0, 0, 0, 0, -0.25, -0.25, -0.25, -0.25}, + {0.125, 0.125, -0.125, -0.125, 0.125, 0.125, -0.125, -0.125, -0.25, -0.25, 0.25, 0.25, 0, 0, 0, 0, -0.25, -0.25, 0.25, 0.25}, + {0, 0, 0, 0, 0, 0, 0, 0, 0.25, -0.25, -0.25, 0.25, 0, 0, 0, 0, 0, 0, 0, 0}, + {-0.125, -0.125, 0.125, 0.125, -0.125, -0.125, 0.125, 0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0.25, 0.25, -0.25, -0.25}, + {0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0, 0, 0, 0, -0.25, -0.25, -0.25, -0.25, 0, 0, 0, 0}, + {-0.125, -0.125, -0.125, -0.125, 0.125, 0.125, 0.125, 0.125, 0, 0, 0, 0, 0.25, 0.25, -0.25, -0.25, 0, 0, 0, 0}, + {0.125, -0.125, -0.125, 0.125, 0.125, -0.125, -0.125, 0.125, 0, 0, 0, 0, -0.25, 0.25, -0.25, 0.25, -0.25, 0.25, -0.25, 0.25}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.25, -0.25, -0.25, 0.25, 0, 0, 0, 0}, + {-0.125, 0.125, 0.125, -0.125, -0.125, 0.125, 0.125, -0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0.25, -0.25, 0.25, -0.25}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.25, -0.25, -0.25, 0.25}, + {-0.125, 0.125, -0.125, 0.125, 0.125, -0.125, 0.125, -0.125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0.125, -0.125, 0.125, -0.125, 0.125, -0.125, 0.125, -0.125, 0, 0, 0, 0, 0, 0, 0, 0, -0.25, 0.25, 0.25, -0.25}, + {-0.125, 0.125, 0.125, -0.125, -0.125, 0.125, 0.125, -0.125, 0, 0, 0, 0, 0.25, -0.25, 0.25, -0.25, 0, 0, 0, 0}, + {0.125, -0.125, -0.125, 0.125, -0.125, 0.125, 0.125, -0.125, 0, 0, 0, 0, -0.25, 0.25, 0.25, -0.25, 0, 0, 0, 0}, + {0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, -0.25, -0.25, -0.25, -0.25, 0, 0, 0, 0, 0, 0, 0, 0}, + {-0.125, -0.125, -0.125, -0.125, 0.125, 0.125, 0.125, 0.125, 0.25, -0.25, 0.25, -0.25, 0, 0, 0, 0, 0, 0, 0, 0}, + {-0.125, -0.125, 0.125, 0.125, -0.125, -0.125, 0.125, 0.125, 0.25, 0.25, -0.25, -0.25, 0, 0, 0, 0, 0, 0, 0, 0}, + {0.125, 0.125, -0.125, -0.125, -0.125, -0.125, 0.125, 0.125, -0.25, 0.25, 0.25, -0.25, 0, 0, 0, 0, 0, 0, 0, 0} +}; + +inline std::array quadratic_powers(Real x) { + return {Real(1), x, x * x}; +} + +void eval_hex20_internal(Real r, Real s, Real t, std::span internal_vals) { + const auto rp = quadratic_powers(r); + const auto sp = quadratic_powers(s); + const auto tp = quadratic_powers(t); + Real phi[20]; + for (int j = 0; j < 20; ++j) { + const int a = hex20_monomial_exponents[j][0]; + const int b = hex20_monomial_exponents[j][1]; + const int c = hex20_monomial_exponents[j][2]; + phi[j] = rp[static_cast(a)] * + sp[static_cast(b)] * + tp[static_cast(c)]; + } + for (int i = 0; i < 20; ++i) { + Real v = Real(0); + for (int j = 0; j < 20; ++j) { + v += hex20_coeffs[j][i] * phi[j]; + } + internal_vals[i] = v; + } +} + +void eval_hex20_grad_internal(Real r, Real s, Real t, std::span internal_grads) { + const auto rp = quadratic_powers(r); + const auto sp = quadratic_powers(s); + const auto tp = quadratic_powers(t); + Real dphi_dr[20], dphi_ds[20], dphi_dt[20]; + for (int j = 0; j < 20; ++j) { + const int a = hex20_monomial_exponents[j][0]; + const int b = hex20_monomial_exponents[j][1]; + const int c = hex20_monomial_exponents[j][2]; + + dphi_dr[j] = (a > 0) ? Real(a) * rp[static_cast(a - 1)] * + sp[static_cast(b)] * + tp[static_cast(c)] + : Real(0); + dphi_ds[j] = (b > 0) ? rp[static_cast(a)] * + Real(b) * sp[static_cast(b - 1)] * + tp[static_cast(c)] + : Real(0); + dphi_dt[j] = (c > 0) ? rp[static_cast(a)] * + sp[static_cast(b)] * + Real(c) * tp[static_cast(c - 1)] + : Real(0); + } + + for (int i = 0; i < 20; ++i) { + Real gr = Real(0), gs = Real(0), gt = Real(0); + for (int j = 0; j < 20; ++j) { + gr += hex20_coeffs[j][i] * dphi_dr[j]; + gs += hex20_coeffs[j][i] * dphi_ds[j]; + gt += hex20_coeffs[j][i] * dphi_dt[j]; + } + internal_grads[i][0] = gr; + internal_grads[i][1] = gs; + internal_grads[i][2] = gt; + } +} + +void eval_hex20_hess_internal(Real r, Real s, Real t, std::span internal_hessians) { + const auto rp = quadratic_powers(r); + const auto sp = quadratic_powers(s); + const auto tp = quadratic_powers(t); + Real d2phi_drr[20], d2phi_dss[20], d2phi_dtt[20]; + Real d2phi_drs[20], d2phi_drt[20], d2phi_dst[20]; + for (int j = 0; j < 20; ++j) { + const int a = hex20_monomial_exponents[j][0]; + const int b = hex20_monomial_exponents[j][1]; + const int c = hex20_monomial_exponents[j][2]; + + d2phi_drr[j] = (a > 1) ? Real(a * (a - 1)) * + rp[static_cast(a - 2)] * + sp[static_cast(b)] * + tp[static_cast(c)] + : Real(0); + d2phi_dss[j] = (b > 1) ? rp[static_cast(a)] * + Real(b * (b - 1)) * + sp[static_cast(b - 2)] * + tp[static_cast(c)] + : Real(0); + d2phi_dtt[j] = (c > 1) ? rp[static_cast(a)] * + sp[static_cast(b)] * + Real(c * (c - 1)) * + tp[static_cast(c - 2)] + : Real(0); + d2phi_drs[j] = (a > 0 && b > 0) ? Real(a * b) * + rp[static_cast(a - 1)] * + sp[static_cast(b - 1)] * + tp[static_cast(c)] + : Real(0); + d2phi_drt[j] = (a > 0 && c > 0) ? Real(a * c) * + rp[static_cast(a - 1)] * + sp[static_cast(b)] * + tp[static_cast(c - 1)] + : Real(0); + d2phi_dst[j] = (b > 0 && c > 0) ? rp[static_cast(a)] * + Real(b * c) * + sp[static_cast(b - 1)] * + tp[static_cast(c - 1)] + : Real(0); + } + + for (int i = 0; i < 20; ++i) { + Hessian H = Hessian::Zero(); + for (int j = 0; j < 20; ++j) { + H(0, 0) += hex20_coeffs[j][i] * d2phi_drr[j]; + H(1, 1) += hex20_coeffs[j][i] * d2phi_dss[j]; + H(2, 2) += hex20_coeffs[j][i] * d2phi_dtt[j]; + H(0, 1) += hex20_coeffs[j][i] * d2phi_drs[j]; + H(0, 2) += hex20_coeffs[j][i] * d2phi_drt[j]; + H(1, 2) += hex20_coeffs[j][i] * d2phi_dst[j]; + } + H(1, 0) = H(0, 1); + H(2, 0) = H(0, 2); + H(2, 1) = H(1, 2); + internal_hessians[i] = H; + } +} + +void eval_wedge15_polynomial(Real r, + Real s, + Real t, + std::span values, + std::span gradients, + std::span hessians) { + Real phi[15]{}; + Real dr[15]{}; + Real ds[15]{}; + Real dt[15]{}; + Real drr[15]{}; + Real dss[15]{}; + Real dtt[15]{}; + Real drs[15]{}; + Real drt[15]{}; + Real dst[15]{}; + + const auto rp = quadratic_powers(r); + const auto sp = quadratic_powers(s); + const auto tp = quadratic_powers(t); + + for (int j = 0; j < 15; ++j) { + const auto& exponent = kWedge15MonomialExponents[static_cast(j)]; + const int a = exponent[0]; + const int b = exponent[1]; + const int c = exponent[2]; + const auto ar = static_cast(a); + const auto bs = static_cast(b); + const auto ct = static_cast(c); + + const Real ra = rp[ar]; + const Real sb = sp[bs]; + const Real tc = tp[ct]; + + if (!values.empty()) { + phi[j] = ra * sb * tc; + } + if (!gradients.empty()) { + dr[j] = (a > 0) ? Real(a) * rp[ar - 1u] * sb * tc : Real(0); + ds[j] = (b > 0) ? ra * Real(b) * sp[bs - 1u] * tc : Real(0); + dt[j] = (c > 0) ? ra * sb * Real(c) * tp[ct - 1u] : Real(0); + } + if (!hessians.empty()) { + drr[j] = (a > 1) ? Real(a * (a - 1)) * rp[ar - 2u] * sb * tc : Real(0); + dss[j] = (b > 1) ? ra * Real(b * (b - 1)) * sp[bs - 2u] * tc : Real(0); + dtt[j] = (c > 1) ? ra * sb * Real(c * (c - 1)) * tp[ct - 2u] : Real(0); + drs[j] = (a > 0 && b > 0) ? Real(a * b) * rp[ar - 1u] * sp[bs - 1u] * tc : Real(0); + drt[j] = (a > 0 && c > 0) ? Real(a * c) * rp[ar - 1u] * sb * tp[ct - 1u] : Real(0); + dst[j] = (b > 0 && c > 0) ? ra * Real(b * c) * sp[bs - 1u] * tp[ct - 1u] : Real(0); + } + } + + for (int i = 0; i < 15; ++i) { + Real value = Real(0); + Real gr = Real(0); + Real gs = Real(0); + Real gt = Real(0); + Hessian H = Hessian::Zero(); + for (int j = 0; j < 15; ++j) { + const Real coefficient = + kWedge15Coefficients[static_cast(j)][static_cast(i)]; + if (!values.empty()) { + value += coefficient * phi[j]; + } + if (!gradients.empty()) { + gr += coefficient * dr[j]; + gs += coefficient * ds[j]; + gt += coefficient * dt[j]; + } + if (!hessians.empty()) { + H(0, 0) += coefficient * drr[j]; + H(1, 1) += coefficient * dss[j]; + H(2, 2) += coefficient * dtt[j]; + H(0, 1) += coefficient * drs[j]; + H(0, 2) += coefficient * drt[j]; + H(1, 2) += coefficient * dst[j]; + } + } + + const std::size_t index = static_cast(i); + if (!values.empty()) { + values[index] = value; + } + if (!gradients.empty()) { + gradients[index][0] = gr; + gradients[index][1] = gs; + gradients[index][2] = gt; + } + if (!hessians.empty()) { + H(1, 0) = H(0, 1); + H(2, 0) = H(0, 2); + H(2, 1) = H(1, 2); + hessians[index] = H; + } + } +} + +void require_output_span_size(std::size_t actual, + std::size_t expected, + const char* label) { + FE::throw_if(actual < expected, SVMP_HERE, + std::string(label) + ": output span is smaller than basis size"); +} + +template +void require_requested_span_size(std::span output, + std::size_t expected, + const char* label) { + if (!output.empty()) { + require_output_span_size(output.size(), expected, label); + } +} + +} // namespace + +SerendipityBasis::SerendipityBasis(ElementType type, int order, bool geometry_mode) + : element_type_(type), dimension_(0), order_(order), size_(0), geometry_mode_(geometry_mode) { + if (type == ElementType::Quad4 || type == ElementType::Quad8) { + dimension_ = 2; + if (order_ < 1) { + order_ = 1; + } + FE::throw_if( + type == ElementType::Quad8 && order_ != 2, SVMP_HERE, + "SerendipityBasis: Quad8 is only valid for quadratic order 2; use Quad4 for higher-order quadrilateral serendipity"); + quad_monomial_exponents_ = quad_serendipity_exponents(order_); + size_ = quad_monomial_exponents_.size(); + nodes_ = quad_serendipity_nodes(order_, size_); + FE::throw_if( + nodes_.size() != size_, SVMP_HERE, + "SerendipityBasis: quadrilateral serendipity setup produced inconsistent sizes"); + quad_inv_vandermonde_ = quad_serendipity_inverse_vandermonde(nodes_, quad_monomial_exponents_, order_); + } else if (type == ElementType::Hex8 || type == ElementType::Hex20) { + dimension_ = 3; + if (order_ < 1) order_ = 1; + if (order_ == 1) { + size_ = 8; + } else if (order_ == 2) { + size_ = 20; + } else { + FE::raise(SVMP_HERE, + "SerendipityBasis supports up to quadratic on hexahedra"); + } + } else if (type == ElementType::Wedge15) { + dimension_ = 3; + if (order_ < 2) { + order_ = 2; + } + if (order_ == 2) { + size_ = 15; + } else { + FE::raise(SVMP_HERE, + "SerendipityBasis supports up to quadratic on wedge15"); + } + } else { + FE::raise(SVMP_HERE, + "SerendipityBasis supports Quad4/Quad8, Hex8/Hex20, and Wedge15 elements"); + } + + if (nodes_.empty()) { + nodes_.reserve(size_); + for (std::size_t i = 0; i < size_; ++i) { + nodes_.push_back(ReferenceNodeLayout::get_node_coords(element_type_, i)); + } + } +} + +void SerendipityBasis::evaluate_all_to(const math::Vector& xi, + std::span values_out, + std::span gradients_out, + std::span hessians_out) const { + require_requested_span_size(values_out, size_, "SerendipityBasis::evaluate_all_to values"); + require_requested_span_size(gradients_out, size_, "SerendipityBasis::evaluate_all_to gradients"); + require_requested_span_size(hessians_out, size_, "SerendipityBasis::evaluate_all_to hessians"); + + if (values_out.empty() && gradients_out.empty() && hessians_out.empty()) { + return; + } + + if (!values_out.empty()) { + std::fill(values_out.begin(), values_out.end(), Real(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 Real x = xi[0]; + const Real y = xi[1]; + const Real z = xi[2]; + + if (dimension_ == 2) { + FE::throw_if( + quad_monomial_exponents_.size() != size_ || + quad_inv_vandermonde_.size() != size_ * size_, + SVMP_HERE, + "SerendipityBasis: quadrilateral interpolation tables are not initialized for value evaluation"); + + for (std::size_t j = 0; j < size_; ++j) { + const auto [ax, ay] = quad_monomial_exponents_[j]; + const Real value = std::pow(x, ax) * std::pow(y, ay); + const Real dx = + (ax > 0) ? Real(ax) * std::pow(x, ax - 1) * std::pow(y, ay) : Real(0); + const Real dy = + (ay > 0) ? std::pow(x, ax) * Real(ay) * std::pow(y, ay - 1) : Real(0); + const Real dxx = + (ax > 1) ? Real(ax * (ax - 1)) * std::pow(x, ax - 2) * std::pow(y, ay) + : Real(0); + const Real dxy = + (ax > 0 && ay > 0) + ? Real(ax * ay) * std::pow(x, ax - 1) * std::pow(y, ay - 1) + : Real(0); + const Real dyy = + (ay > 1) ? Real(ay * (ay - 1)) * std::pow(x, ax) * std::pow(y, ay - 2) + : Real(0); + + for (std::size_t i = 0; i < size_; ++i) { + const Real coeff = quad_inv_vandermonde_[j * size_ + i]; + if (!values_out.empty()) { + values_out[i] += value * coeff; + } + if (!gradients_out.empty()) { + Gradient& g = gradients_out[i]; + g[0] += dx * coeff; + g[1] += dy * coeff; + } + if (!hessians_out.empty()) { + Hessian& h = hessians_out[i]; + h(0, 0) += dxx * coeff; + h(0, 1) += dxy * coeff; + h(1, 0) += dxy * coeff; + h(1, 1) += dyy * coeff; + } + } + } + return; + } + + if (dimension_ == 3 && order_ == 1) { + evaluate_hex8_reference(x, y, z, values_out, gradients_out, hessians_out); + return; + } + + if (geometry_mode_ && element_type_ == ElementType::Hex20) { + evaluate_hex8_reference(x, y, z, values_out, gradients_out, hessians_out); + return; + } + + if (element_type_ == ElementType::Hex20) { + const auto mesh_to_basis = ReferenceNodeLayout::mesh_to_basis_ordering(element_type_); + FE::throw_if(mesh_to_basis.size() != size_, SVMP_HERE, + "Hex20 mesh-to-basis ordering is not registered"); + + if (!values_out.empty()) { + std::array internal_vals{}; + eval_hex20_internal(x, y, z, internal_vals); + for (std::size_t i = 0; i < 20u; ++i) { + values_out[i] = internal_vals[mesh_to_basis[i]]; + } + } + if (!gradients_out.empty()) { + std::array internal_grads{}; + eval_hex20_grad_internal(x, y, z, internal_grads); + for (std::size_t i = 0; i < 20u; ++i) { + gradients_out[i] = internal_grads[mesh_to_basis[i]]; + } + } + if (!hessians_out.empty()) { + std::array internal_hessians{}; + eval_hex20_hess_internal(x, y, z, internal_hessians); + for (std::size_t i = 0; i < 20u; ++i) { + hessians_out[i] = internal_hessians[mesh_to_basis[i]]; + } + } + return; + } + + if (element_type_ == ElementType::Wedge15) { + eval_wedge15_polynomial(x, + y, + z, + values_out, + gradients_out, + hessians_out); + return; + } + + FE::raise(SVMP_HERE, + "SerendipityBasis::evaluate_all_to: unsupported serendipity configuration"); +} + +void SerendipityBasis::evaluate_values(const math::Vector& xi, + std::vector& values) const { + values.resize(size_); + evaluate_values_to(xi, std::span(values.data(), values.size())); +} + +void SerendipityBasis::evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const { + gradients.resize(size_); + evaluate_gradients_to(xi, std::span(gradients.data(), gradients.size())); +} + +void SerendipityBasis::evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const { + hessians.resize(size_); + evaluate_hessians_to(xi, std::span(hessians.data(), hessians.size())); +} + +void SerendipityBasis::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())); +} + +void SerendipityBasis::evaluate_values_to(const math::Vector& xi, + std::span values_out) const { + require_output_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_output_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_output_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..e231ed833 --- /dev/null +++ b/Code/Source/solver/FE/Basis/SerendipityBasis.h @@ -0,0 +1,214 @@ +// 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 Quad4/Quad8, +/// Hex8/Hex20, and Wedge15. 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. +/// +/// Quadrilateral serendipity bases are built from monomials +/// \f$x^{a_x}y^{a_y}\f$ whose superlinear degree is at most the requested +/// order. In this implementation 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 Vandermonde interpolation +/// matrix at the selected reference nodes. Values, gradients, and Hessians are +/// then evaluated by differentiating the monomial vector and applying the +/// inverse Vandermonde coefficients. +/// +/// Hex8 uses the standard trilinear corner basis +/// \f$(1 \pm r)(1 \pm s)(1 \pm t)/8\f$. Hex20 and Wedge15 use tabulated +/// polynomial coefficient tables over monomial bases; analytical gradients and +/// Hessians are obtained by differentiating those monomials. Hex20 evaluation +/// is reordered through ReferenceNodeLayout so the output matches the public +/// basis ordering. +/// +/// When `geometry_mode` is enabled for Hex20, the basis uses the trilinear +/// Hex8 corner functions for geometry mapping and assigns zero contribution to +/// the quadratic edge nodes. This preserves the public Hex20 node count while +/// intentionally reducing the geometry interpolation order. +class SerendipityBasis final : public BasisFunction { +public: + /// \brief Construct a serendipity basis for an element type and polynomial order. + /// + /// \details The constructor selects the topology-specific interpolation + /// space, computes the reference node coordinates, and initializes any + /// coefficient tables needed for evaluation. Quadrilateral bases build and + /// invert a Vandermonde matrix for the selected serendipity monomials. + /// Hex20 and Wedge15 use fixed coefficient tables. For hexahedra, only + /// linear Hex8 and quadratic Hex20 serendipity spaces are supported. For + /// wedges, only quadratic Wedge15 is supported. + /// + /// \param type Element type used to determine topology and reference-node layout. + /// \param order Requested polynomial order. + /// \param geometry_mode When true, allow reduced geometry-mapping behavior for supported elements. + /// \throws BasisConfigurationException If the requested order or mode is invalid. + /// \throws BasisElementCompatibilityException If the element type is unsupported. + SerendipityBasis(ElementType type, int order, bool geometry_mode = false); + + /// \copydoc BasisFunction::basis_type() + BasisType basis_type() const noexcept final { return BasisType::Serendipity; } + + /// \copydoc BasisFunction::element_type() + ElementType element_type() const noexcept final { return element_type_; } + + /// \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. Quadrilateral nodes are + /// placed first on the boundary and then, for higher order requests, at the + /// selected interior points needed to make the reduced monomial space + /// unisolvent. Hexahedral and wedge nodes are taken from + /// ReferenceNodeLayout. + /// + /// \return Reference node coordinates, one per basis function. + const std::vector>& nodes() const noexcept { return nodes_; } + + /// \brief Evaluate serendipity basis function values at a reference coordinate. + /// + /// \details For quadrilateral bases, this evaluates the serendipity + /// monomial vector and multiplies by the inverse Vandermonde matrix to + /// obtain nodal shape-function values. For Hex8, values are the standard + /// trilinear corner products. For Hex20 and Wedge15, values are evaluated + /// from the stored polynomial coefficient tables. In Hex20 geometry mode, + /// only the first eight corner values are nonzero and they match Hex8. + /// + /// \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 final; + + /// \brief Evaluate analytical serendipity basis gradients at a reference coordinate. + /// + /// \details Gradients are derivatives with respect to reference + /// coordinates. Quadrilateral gradients differentiate the monomial vector + /// before applying the inverse Vandermonde coefficients. Hex8 gradients are + /// direct derivatives of the trilinear corner products. Hex20 and Wedge15 + /// gradients are computed by differentiating the tabulated monomial + /// expansions. In Hex20 geometry mode, edge-node gradients are zero and the + /// corner gradients match Hex8. + /// + /// \param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + /// \param gradients Receives one three-component gradient per basis function. + void evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const final; + + /// \brief Evaluate analytical serendipity basis Hessians at a reference coordinate. + /// + /// \details Hessians are second derivatives in reference coordinates and + /// are stored as 3-by-3 matrices. Quadrilateral Hessians use second + /// derivatives of the monomial vector and inverse Vandermonde coefficients. + /// Hex8 Hessians are delegated to the linear Lagrange Hex8 basis. Hex20 and + /// Wedge15 Hessians are computed by differentiating their polynomial + /// coefficient tables twice. In Hex20 geometry mode, only the corner + /// Hessians from the Hex8 geometry mapping are populated. + /// + /// \param xi Reference coordinate. Lower-dimensional elements use the active prefix components. + /// \param hessians Receives one 3-by-3 Hessian per basis function. + void evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const final; + + /// \brief Evaluate serendipity values, gradients, and Hessians together. + /// + /// \details This vector API is backed by the same span-based evaluator as + /// the assembly-oriented `*_to` methods, so topology-specific polynomial + /// setup can be shared for a quadrature point. + /// + /// \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 final; + + /// \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: + ElementType element_type_; + int dimension_; + int order_; + std::size_t size_; + std::vector> nodes_; + std::vector> quad_monomial_exponents_; + // Row-major inverse Vandermonde, indexed as [monomial, basis]. + std::vector quad_inv_vandermonde_; + + // When true, this basis is used purely for geometry mapping and may use + // reduced polynomial order (e.g., Hex20 geometry as Hex8). + bool geometry_mode_; + + void evaluate_all_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_SERENDIPITYBASIS_H diff --git a/Code/Source/solver/FE/Common/FEException.h b/Code/Source/solver/FE/Common/FEException.h index 67b7da234..033b85eb1 100644 --- a/Code/Source/solver/FE/Common/FEException.h +++ b/Code/Source/solver/FE/Common/FEException.h @@ -22,8 +22,34 @@ namespace svmp { namespace FE { +/// \defgroup FE_CommonExceptions Exceptions +/// \ingroup FE_Common +/// \brief FE exception hierarchy and throw/check helper functions. +/// +/// \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. The free helper templates raise(), throw_if(), +/// check_arg(), check_not_null(), and check_index() wrap common validation +/// patterns with source-location capture. +/// @{ + +/** + * @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: + /// @brief Construct with a message and optional status code and source location. + /// @param message Human-readable error description. + /// @param status Status code classifying the failure. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. FEException(const std::string& message, StatusCode status = StatusCode::Unknown, const char* file = "", @@ -38,6 +64,11 @@ class FEException : public ExceptionBase { { } + /// @brief Construct with a message and source location, using an Unknown status. + /// @param message Human-readable error description. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. FEException(const std::string& message, const char* file, int line, @@ -46,11 +77,21 @@ class FEException : public ExceptionBase { { } + /// @brief Status code classifying the failure. + /// @return The status code recorded at construction. StatusCode status() const noexcept { return status_code(); } }; +/** + * @brief An argument failed validation + */ class InvalidArgumentException : public FEException { public: + /// @brief Construct with a message and optional source location. + /// @param message Human-readable error description. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. InvalidArgumentException(const std::string& message, const char* file = "", int line = 0, @@ -61,8 +102,19 @@ class InvalidArgumentException : public FEException { } }; +/** + * @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. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. InvalidElementException(const std::string& message, std::string element_type = "", const char* file = "", @@ -77,6 +129,8 @@ class InvalidElementException : public FEException { { } + /// @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,8 +147,19 @@ 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(). + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. DofException(const std::string& message, long long dof_index = invalid_dof_index(), const char* file = "", @@ -109,7 +174,11 @@ class DofException : public FEException { { } + /// @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,8 +195,16 @@ class DofException : public FEException { long long dof_index_ = invalid_dof_index(); }; +/** + * @brief Global assembly failure + */ class AssemblyException : public FEException { public: + /// @brief Construct with a message and optional source location. + /// @param message Human-readable error description. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. AssemblyException(const std::string& message, const char* file = "", int line = 0, @@ -137,8 +214,21 @@ class AssemblyException : public FEException { } }; +/** + * @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. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. BackendException(const std::string& message, std::string backend_name = "", int error_code = 0, @@ -155,7 +245,11 @@ class BackendException : public FEException { { } + /// @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,8 +279,16 @@ class BackendException : public FEException { int error_code_ = 0; }; +/** + * @brief Requested feature is not implemented + */ class NotImplementedException : public FEException { public: + /// @brief Construct from the name of the missing feature. + /// @param feature Description of the unimplemented feature. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. NotImplementedException(const std::string& feature, const char* file = "", int line = 0, @@ -200,8 +302,16 @@ class NotImplementedException : public FEException { } }; +/** + * @brief Required initialization step has not been performed + */ class NotInitializedException : public FEException { public: + /// @brief Construct from the name of the uninitialized feature. + /// @param feature Description of the missing initialization. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. NotInitializedException(const std::string &feature, const char *file, int line = 0, @@ -215,8 +325,21 @@ class NotInitializedException : public FEException { } }; +/** + * @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. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. ConvergenceException(const std::string& message, int iteration = -1, double residual = 0.0, @@ -233,7 +356,11 @@ class ConvergenceException : public FEException { { } + /// @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,8 +384,20 @@ 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. + /// @param file Source file where the error was raised. + /// @param line Source line where the error was raised. + /// @param function Function where the error was raised. SingularMappingException(const std::string& message, double jacobian_det = 0.0, const char* file = "", @@ -273,6 +412,8 @@ class SingularMappingException : public FEException { { } + /// @brief Jacobian determinant at the failure point. + /// @return The determinant recorded at construction. double jacobian_det() const noexcept { return jacobian_det_; } private: @@ -285,12 +426,27 @@ class SingularMappingException : public FEException { double jacobian_det_ = 0.0; }; +/** + * @brief Throw an FE exception with source-location capture + * @tparam ExceptionT Exception type to throw. + * @tparam Args Constructor argument types forwarded to the exception. + * @param location Source location to record in the exception. + * @param args Arguments forwarded to the exception constructor. + */ template [[noreturn]] inline void raise(SourceLocation location, Args&&... args) { ::svmp::raise(location, std::forward(args)...); } +/** + * @brief Throw an FE exception when a condition holds + * @tparam ExceptionT Exception type to throw; defaults to FEException. + * @tparam Args Constructor argument types forwarded to the exception. + * @param condition Condition that triggers the throw when true. + * @param location Source location to record in the exception. + * @param args Arguments forwarded to the exception constructor. + */ template inline void throw_if(bool condition, SourceLocation location, Args&&... args) { @@ -299,6 +455,14 @@ inline void throw_if(bool condition, SourceLocation location, Args&&... args) } } +/** + * @brief Validate an argument condition, throwing when it fails + * @tparam ExceptionT Exception type to throw; defaults to InvalidArgumentException. + * @tparam Args Constructor argument types forwarded to the exception. + * @param condition Condition that must hold for the argument to be valid. + * @param location Source location to record in the exception. + * @param args Arguments forwarded to the exception constructor. + */ template inline void check_arg(bool condition, SourceLocation location, Args&&... args) { @@ -306,6 +470,15 @@ inline void check_arg(bool condition, SourceLocation location, Args&&... args) std::forward(args)...); } +/** + * @brief Validate that a pointer is non-null, throwing when it is null + * @tparam ExceptionT Exception type to throw; defaults to InvalidArgumentException. + * @tparam PointerT Pointer-like type being checked. + * @tparam Args Constructor argument types forwarded to the exception. + * @param ptr Pointer to validate. + * @param location Source location to record in the exception. + * @param args Arguments forwarded to the exception constructor. + */ template inline void check_not_null(PointerT ptr, SourceLocation location, @@ -314,6 +487,15 @@ inline void check_not_null(PointerT ptr, SourceLocation location, ::svmp::check_not_null(ptr, location, std::forward(args)...); } +/** + * @brief Validate that an index lies in [0, size), throwing when out of bounds + * @tparam ExceptionT Exception type to throw; defaults to InvalidArgumentException. + * @tparam IndexT Integral index type. + * @tparam SizeT Integral size type. + * @param index Index to validate. + * @param size Exclusive upper bound for the index. + * @param location Source location to record in the exception. + */ template inline void check_index(IndexT index, SizeT size, SourceLocation location) @@ -329,12 +511,19 @@ inline void check_index(IndexT index, SizeT size, SourceLocation location) " out of bounds [0, " + std::to_string(fe_check_size_value) + ")"); } +/** + * @brief Throw NotImplementedException for a missing feature + * @param feature Description of the unimplemented feature. + * @param location Source location to record in the exception. + */ [[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..462b7ca76 --- /dev/null +++ b/Code/Source/solver/FE/Common/Types.h @@ -0,0 +1,578 @@ +// 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. + */ + +#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. + */ +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. + */ +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. +using Real = svmp::real_t; ///< Floating-point scalar type; same precision as the Mesh library. +#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. +using Real = double; ///< Floating-point scalar type; same precision as the Mesh library. +#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. + Real 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. + */ +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 + Hierarchical, ///< Hierarchical/modal basis + Bernstein, ///< Bernstein polynomials + NURBS, ///< Non-uniform rational B-splines + BSpline, ///< Non-rational B-spline basis + Spectral, ///< Spectral element basis + Serendipity, ///< Serendipity elements + Hermite, ///< Hermite C1 continuity basis + RaviartThomas, ///< H(div) Raviart-Thomas family + Nedelec, ///< H(curl) Nedelec edge elements + BDM, ///< H(div) Brezzi-Douglas-Marini family + Bubble, ///< Interior bubble functions for enrichment + 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 +}; + +/** + * @brief Status codes for FE operations + */ +enum class FEStatus : std::uint8_t { + Success = 0, ///< Operation completed successfully + InvalidArgument = 1, ///< An argument failed validation + InvalidElement = 2, ///< Unsupported or malformed element + SingularMapping = 3, ///< Element mapping Jacobian is singular + QuadratureError = 4, ///< Quadrature rule construction or evaluation failed + AssemblyError = 5, ///< Global assembly failure + BackendError = 6, ///< Linear-algebra backend failure + NotImplemented = 7, ///< Requested feature is not implemented + ConvergenceError = 8, ///< Iterative process failed to converge + AllocationError = 9, ///< Memory allocation failure + MPIError = 10, ///< MPI communication failure + IOError = 11, ///< File or stream I/O failure + Unknown = 255 ///< Unclassified error +}; + +// ============================================================================ +// 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; + } +} + +/** + * @brief Convert status code to string for error reporting + * @param status Status code to describe. + * @return Static human-readable description of the status. + */ +inline const char* status_to_string(FEStatus status) noexcept { + switch(status) { + case FEStatus::Success: return "Success"; + case FEStatus::InvalidArgument: return "Invalid argument"; + case FEStatus::InvalidElement: return "Invalid element"; + case FEStatus::SingularMapping: return "Singular mapping"; + case FEStatus::QuadratureError: return "Quadrature error"; + case FEStatus::AssemblyError: return "Assembly error"; + case FEStatus::BackendError: return "Backend error"; + case FEStatus::NotImplemented: return "Not implemented"; + case FEStatus::ConvergenceError: return "Convergence error"; + case FEStatus::AllocationError: return "Allocation error"; + case FEStatus::MPIError: return "MPI error"; + case FEStatus::IOError: return "I/O error"; + default: return "Unknown error"; + } +} + +/// @} + +} // 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..1d3bba72b --- /dev/null +++ b/Code/Source/solver/FE/FE.h @@ -0,0 +1,22 @@ +// 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..fb27ad7bf --- /dev/null +++ b/Code/Source/solver/FE/Math/DenseLinearAlgebra.cpp @@ -0,0 +1,338 @@ +// 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 + +#define DENSE_LINALG_CHECK(condition, message) \ + ::svmp::FE::throw_if<::svmp::FE::FEException>(!(condition), SVMP_HERE, (message)) + +namespace svmp { +namespace FE { +namespace math { + +namespace { + +using DenseMatrix = DenseLUSolver::DenseMatrix; +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 + +Real dense_matrix_max_abs(std::span matrix) noexcept { + Real max_abs = Real(0); + for (const Real value : matrix) { + max_abs = std::max(max_abs, std::abs(value)); + } + return max_abs; +} + +Real dense_matrix_pivot_tolerance(std::size_t rows, + std::size_t cols, + Real max_abs, + Real multiplier) noexcept { + const Real size_scale = static_cast(std::max(rows, cols)); + const Real value_scale = std::max(Real(1), max_abs); + return multiplier * std::numeric_limits::epsilon() * + std::max(Real(1), size_scale) * value_scale; +} + +Real dense_matrix_singular_value_tolerance(std::size_t rows, + std::size_t cols, + Real largest_singular_value, + Real multiplier) noexcept { + const Real size_scale = static_cast(std::max(rows, cols)); + return multiplier * std::numeric_limits::epsilon() * + std::max(Real(1), size_scale) * + std::max(Real(1), largest_singular_value); +} + +Real dense_matrix_condition_fallback_threshold() noexcept { + return Real(1.0e12); +} + +Real dense_matrix_condition_error_threshold() noexcept { + return Real(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 { + DENSE_LINALG_CHECK(rhs_count > 0, + label + ": dense solve requires at least one right-hand side"); + DENSE_LINALG_CHECK(rhs.size() == n * rhs_count, + label + ": dense multi-RHS solve size mismatch"); + DENSE_LINALG_CHECK(lu.rows() == static_cast(n), + 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 = 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 label) { + DENSE_LINALG_CHECK(matrix.size() == rows * cols, + std::string(label) + ": diagnostic size mismatch"); + DENSE_LINALG_CHECK(rows > 0 && cols > 0, + std::string(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] : Real(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 Real 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 > Real(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 label) { + DENSE_LINALG_CHECK(matrix.size() == n * n, + std::string(label) + ": dense factorization size mismatch"); + + DenseLUSolver solver; + solver.n = n; + solver.label = std::string(label); + const Real max_abs = + dense_matrix_max_abs(std::span(matrix.data(), matrix.size())); + solver.pivot_tolerance = dense_matrix_pivot_tolerance(n, n, max_abs); + + solver.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. + Real max_pivot_abs = Real(0); + Real min_pivot_abs = std::numeric_limits::infinity(); + const auto diagonal = solver.lu.matrixLU().diagonal(); + for (Eigen::Index col = 0; col < diagonal.size(); ++col) { + const Real pivot_magnitude = std::abs(diagonal[col]); + DENSE_LINALG_CHECK( + pivot_magnitude > solver.pivot_tolerance, + solver.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); + } + + solver.diagnostics.rank = n; + solver.diagnostics.tolerance = solver.pivot_tolerance; + solver.diagnostics.largest_singular_value = max_abs; + solver.diagnostics.smallest_retained_singular_value = + std::isfinite(min_pivot_abs) ? min_pivot_abs : Real(0); + if (solver.diagnostics.smallest_retained_singular_value > Real(0)) { + solver.diagnostics.condition_estimate = + max_pivot_abs / solver.diagnostics.smallest_retained_singular_value; + } + return solver; +} + +DenseInverseResult invert_dense_matrix_with_diagnostics( + std::vector matrix, + std::size_t n, + std::string_view label) { + DENSE_LINALG_CHECK(matrix.size() == n * n, + std::string(label) + ": dense inverse size mismatch"); + std::vector matrix_for_lu = matrix; + const DenseLUSolver solver = + factor_dense_matrix(std::move(matrix_for_lu), n, label); + + DenseInverseResult result; + result.diagnostics = + dense_matrix_diagnostics(std::span(matrix.data(), matrix.size()), + n, n, label); + + if (std::isfinite(solver.diagnostics.condition_estimate) && + 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) { + DENSE_LINALG_CHECK( + singular_values[i] > solver.diagnostics.tolerance, + std::string(label) + ": high-condition SVD fallback encountered a dropped singular value"); + sigma_inverse(i, i) = Real(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.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 label, + Real max_condition) { + DENSE_LINALG_CHECK( + result.diagnostics.rank == expected_rank, + std::string(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; + } + + DENSE_LINALG_CHECK( + result.diagnostics.condition_estimate <= max_condition, + std::string(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 label) { + const DenseLUSolver solver = factor_dense_matrix(std::move(matrix), n, label); + const DenseMatrix inverse = solver.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) { + DENSE_LINALG_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 Real largest = + (singular_values.size() > 0) ? singular_values[0] : Real(0); + const Real 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 label) { + DENSE_LINALG_CHECK(matrix.size() == rows * cols, + std::string(label) + ": pseudo-inverse size mismatch"); + DENSE_LINALG_CHECK(rows > 0 && cols > 0, + std::string(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] : Real(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 Real sigma = singular_values[i]; + if (sigma <= result.tolerance) { + continue; + } + sigma_inverse(i, i) = Real(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 + +#undef DENSE_LINALG_CHECK diff --git a/Code/Source/solver/FE/Math/DenseLinearAlgebra.h b/Code/Source/solver/FE/Math/DenseLinearAlgebra.h new file mode 100644 index 000000000..d322ef958 --- /dev/null +++ b/Code/Source/solver/FE/Math/DenseLinearAlgebra.h @@ -0,0 +1,118 @@ +// 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, backed by Eigen. Matrices are row-major: matrix[row * cols + col]. +[[nodiscard]] Real dense_matrix_max_abs(std::span matrix) noexcept; + +[[nodiscard]] Real dense_matrix_pivot_tolerance(std::size_t rows, + std::size_t cols, + Real max_abs, + Real multiplier = Real(64)) noexcept; + +[[nodiscard]] Real dense_matrix_singular_value_tolerance(std::size_t rows, + std::size_t cols, + Real largest_singular_value, + Real multiplier = Real(64)) noexcept; + +struct DensePseudoInverseResult { + std::vector inverse; + std::size_t rank{0}; + Real tolerance{0}; + Real largest_singular_value{0}; + Real smallest_retained_singular_value{0}; +}; + +struct DenseMatrixDiagnostics { + std::size_t rank{0}; + Real tolerance{0}; + Real largest_singular_value{0}; + Real smallest_retained_singular_value{0}; + Real condition_estimate{std::numeric_limits::infinity()}; +}; + +struct DenseInverseResult { + std::vector inverse; + DenseMatrixDiagnostics diagnostics; + bool used_svd_fallback{false}; +}; + +[[nodiscard]] Real dense_matrix_condition_fallback_threshold() noexcept; +[[nodiscard]] Real dense_matrix_condition_error_threshold() noexcept; + +struct DenseLUSolver { + using DenseMatrix = Eigen::Matrix; + + std::size_t n{0}; + Eigen::PartialPivLU lu; + DenseMatrixDiagnostics diagnostics; + Real pivot_tolerance{0}; + std::string label; + + [[nodiscard]] bool empty() const noexcept { return n == 0; } + + void solve_in_place(std::span rhs) const; + void solve_in_place(std::span rhs, std::size_t rhs_count) const; + [[nodiscard]] std::vector solve(std::span rhs) const; +}; + +// Inverses and pseudo-inverses keep the same row-major convention for their +// returned dimensions. +[[nodiscard]] DenseMatrixDiagnostics dense_matrix_diagnostics( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view label = "dense matrix"); + +[[nodiscard]] DenseLUSolver factor_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view label = "dense matrix"); + +[[nodiscard]] std::vector invert_dense_matrix(std::vector matrix, + std::size_t n, + std::string_view label = "dense matrix"); + +[[nodiscard]] DenseInverseResult invert_dense_matrix_with_diagnostics( + std::vector matrix, + std::size_t n, + std::string_view label = "dense matrix"); + +void validate_dense_inverse_diagnostics( + const DenseInverseResult& result, + std::size_t expected_rank, + std::string_view label = "dense matrix", + Real max_condition = dense_matrix_condition_error_threshold()); + +[[nodiscard]] std::size_t dense_matrix_rank(std::vector matrix, + std::size_t rows, + std::size_t cols); + +[[nodiscard]] DensePseudoInverseResult rank_revealing_pseudo_inverse( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::string_view label = "dense matrix"); + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_DENSELINEARALGEBRA_H diff --git a/Code/Source/solver/FE/Math/DenseTransformKernels.h b/Code/Source/solver/FE/Math/DenseTransformKernels.h new file mode 100644 index 000000000..f6639dcd3 --- /dev/null +++ b/Code/Source/solver/FE/Math/DenseTransformKernels.h @@ -0,0 +1,81 @@ +// 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_DENSETRANSFORMKERNELS_H +#define SVMP_FE_MATH_DENSETRANSFORMKERNELS_H + +#include "FEException.h" +#include "Types.h" + +#include + +#include +#include + +namespace svmp { +namespace FE { +namespace math { + +/// \brief Apply a row-major dense matrix to a batch of right-hand sides. +/// +/// Computes output = matrix * input where matrix is rows-by-cols (row-major), +/// input holds cols rows of rhs_count values each (row stride +/// input_row_stride), and output holds rows rows of rhs_count values each +/// (row stride output_row_stride). Strides may exceed rhs_count for padded +/// layouts; padding entries are left untouched. +inline void dense_transform_batched_row_major( + std::span matrix, + std::size_t rows, + std::size_t cols, + std::span input, + std::size_t input_row_stride, + std::span output, + std::size_t output_row_stride, + std::size_t rhs_count) { + if (rows == 0u || cols == 0u || rhs_count == 0u) { + return; + } + + FE::throw_if(matrix.size() < rows * cols, SVMP_HERE, + "dense_transform_batched_row_major: matrix span is too small"); + FE::throw_if(input_row_stride < rhs_count, SVMP_HERE, + "dense_transform_batched_row_major: input stride is smaller than RHS count"); + FE::throw_if(output_row_stride < rhs_count, SVMP_HERE, + "dense_transform_batched_row_major: output stride is smaller than RHS count"); + FE::throw_if( + input.size() < (cols - 1u) * input_row_stride + rhs_count, SVMP_HERE, + "dense_transform_batched_row_major: input span is too small"); + FE::throw_if( + output.size() < (rows - 1u) * output_row_stride + rhs_count, SVMP_HERE, + "dense_transform_batched_row_major: output span is too small"); + + using RowMajorMatrix = + Eigen::Matrix; + using ConstMap = Eigen::Map; + using ConstStridedMap = + Eigen::Map>; + using StridedMap = + Eigen::Map>; + + const ConstMap matrix_map(matrix.data(), + static_cast(rows), + static_cast(cols)); + const ConstStridedMap input_map( + input.data(), + static_cast(cols), + static_cast(rhs_count), + Eigen::OuterStride<>(static_cast(input_row_stride))); + StridedMap output_map( + output.data(), + static_cast(rows), + static_cast(rhs_count), + Eigen::OuterStride<>(static_cast(output_row_stride))); + + output_map.noalias() = matrix_map * input_map; +} + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_DENSETRANSFORMKERNELS_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..ce1d4a612 --- /dev/null +++ b/Code/Source/solver/FE/Math/Matrix.h @@ -0,0 +1,66 @@ +// 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)>; + +// Type aliases for common matrix types +template using Matrix2x2 = Matrix; +template using Matrix3x3 = Matrix; +template using Matrix4x4 = Matrix; +template using Matrix2x3 = Matrix; +template using Matrix3x2 = Matrix; +template using Matrix3x4 = Matrix; +template using Matrix4x3 = Matrix; + +// Double precision aliases +using Matrix2x2d = Matrix2x2; +using Matrix3x3d = Matrix3x3; +using Matrix4x4d = Matrix4x4; + +// Single precision aliases +using Matrix2x2f = Matrix2x2; +using Matrix3x3f = Matrix3x3; +using Matrix4x4f = Matrix4x4; + +} // 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..b234bac49 --- /dev/null +++ b/Code/Source/solver/FE/Math/Vector.h @@ -0,0 +1,71 @@ +// 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. + */ + +#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>; + +// Type aliases for common vector types +template using Vector2 = Vector; +template using Vector3 = Vector; +template using Vector4 = Vector; + +// Double precision aliases +using Vector2d = Vector2; +using Vector3d = Vector3; +using Vector4d = Vector4; + +// Single precision aliases +using Vector2f = Vector2; +using Vector3f = Vector3; +using Vector4f = Vector4; + +// Integer aliases +using Vector2i = Vector2; +using Vector3i = Vector3; +using Vector4i = Vector4; + +} // namespace math +} // namespace FE +} // namespace svmp + +#endif // SVMP_FE_MATH_VECTOR_H 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/fs.cpp b/Code/Source/solver/fs.cpp index d592a8b96..abe1992df 100644 --- a/Code/Source/solver/fs.cpp +++ b/Code/Source/solver/fs.cpp @@ -5,10 +5,66 @@ #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)) + ")"; +} + +bool supports_reference_hessians(consts::ElementType eType) +{ + using namespace consts; + + switch (eType) { + case ElementType::LIN1: + case ElementType::LIN2: + case ElementType::TRI3: + case ElementType::TRI6: + case ElementType::QUD4: + case ElementType::QUD8: + case ElementType::QUD9: + case ElementType::TET4: + case ElementType::TET10: + case ElementType::HEX8: + case ElementType::HEX20: + case ElementType::HEX27: + case ElementType::WDG: + return true; + default: + return false; + } +} + +void populate_reference_hessians_if_supported(fsType& fs, const int insd) +{ + if (fs.Nxx.size() == 0 || !supports_reference_hessians(fs.eType)) { + 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 +159,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_if_supported(fs[1], nsd); } else if (iOpt == 2) { fs[1].nG = lM.fs[1].nG; @@ -133,6 +190,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_if_supported(fs[0], nsd); } } } @@ -275,14 +333,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_if_supported(lM.fs[0], insd); // Sets Taylor-Hood basis [fluid, stokes, ustruct, FSI) if (lM.nFs == 2) { @@ -291,6 +342,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_if_supported(lM.fs[1], insd); } } @@ -343,7 +395,8 @@ void set_thood_fs(fsType& fs, consts::ElementType eType) break; default: - throw std::runtime_error("Cannot choose Taylor-Hood basis"); + throw fe::InvalidElementException("Cannot choose Taylor-Hood basis", + element_name(eType), __FILE__, __LINE__, __func__); break; } } 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/nn.cpp b/Code/Source/solver/nn.cpp index 652923cf2..547310703 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,361 @@ 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. +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; + } + return std::nullopt; +} + +bool use_basis_adapter_for(consts::ElementType eType) +{ + return to_fe_element_type(eType).has_value(); +} + +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 use_basis_adapter_for(eType); + 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) { + fe::raise(SVMP_HERE, + "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; +} + +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}; + static constexpr std::array hex27{ + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 25, 23, 22, 24, 20, 21, 26}; + + 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; + case consts::ElementType::HEX27: + return hex27; + default: + return {}; + } +} + +std::size_t basis_index_for_solver_node(consts::ElementType eType, const int solver_node) +{ + if (solver_node < 0) { + fe::raise(SVMP_HERE, + "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]; + } + fe::raise(SVMP_HERE, + "Solver node " + std::to_string(solver_node) + + " is outside node map for " + solver_element_name(eType)); +} + +fe::math::Vector make_basis_point(const febasis::BasisFunction& basis, + const int g, + const Array& xi) +{ + if (xi.nrows() < basis.dimension()) { + fe::raise(SVMP_HERE, + "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; +} + +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)) { + fe::raise(SVMP_HERE, + "FE Basis value count " + std::to_string(values.size()) + + " does not match solver eNoN " + std::to_string(eNoN)); + } + if (gradients.size() != static_cast(eNoN)) { + fe::raise(SVMP_HERE, + "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()) { + fe::raise(SVMP_HERE, + "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)]; + } + } +} + +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()) { + fe::raise(SVMP_HERE, + "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); +} + +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); +} + +int required_nxx_components_for_dimension(const int dimension) +{ + switch (dimension) { + case 1: + return 1; + case 2: + return 3; + case 3: + return 6; + default: + fe::raise(SVMP_HERE, + "Unsupported FE Basis reference dimension " + std::to_string(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)) { + fe::raise(SVMP_HERE, + "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) { + fe::raise(SVMP_HERE, + "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()) { + fe::raise(SVMP_HERE, + "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); + } + } +} + +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()) { + fe::raise(SVMP_HERE, + "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) { + fe::raise(SVMP_HERE, + "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); +} + +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'."); + fe::raise(SVMP_HERE, + "No support in 'get_element_gauss_int_data'", + solver_element_name(eType)); } } @@ -65,7 +416,9 @@ 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'."); + fe::raise(SVMP_HERE, + "No support in 'set_element_gauss_int_data'", + solver_element_name(mesh.eType)); } } @@ -74,7 +427,9 @@ 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'."); + fe::raise(SVMP_HERE, + "No support in 'set_face_gauss_int_data'", + solver_element_name(face.eType)); } } @@ -83,15 +438,15 @@ 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 (!use_basis_adapter_for(eType)) { + fe::raise(SVMP_HERE, + "[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 +466,50 @@ 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; + + fe::throw_if(face.eType == ElementType::NRB, SVMP_HERE, + "[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; } + + fe::raise(SVMP_HERE, + "[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) { + // NRB/PNT and face-only Hessian paths remain intentionally unsupported here. + if (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 (!use_basis_adapter_for(eType)) { + fe::raise(SVMP_HERE, + "[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 +693,8 @@ 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"); - } + fe::throw_if(!l1, SVMP_HERE, + "Error in computing shape functions"); } /// @brief Inverse maps {xp} to {$\xi$} in an element with coordinates {xl} using Newton's method @@ -582,8 +942,10 @@ 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."); + fe::raise(SVMP_HERE, + "[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 +994,8 @@ 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"); + fe::raise(SVMP_HERE, + "gnnb: invalid MechanicalConfigurationType provided"); } } } @@ -820,9 +1183,8 @@ void gn_nxx(const int l, const int eNoN, const int nsd, const int insd, Array(INFO != 0, SVMP_HERE, + "[gn_nxx] Error in Lapack", "LAPACK dgesv", INFO); Nxx = B; @@ -891,9 +1253,8 @@ void gn_nxx(const int l, const int eNoN, const int nsd, const int insd, Array(INFO != 0, SVMP_HERE, + "[gn_nxx] Error in Lapack", "LAPACK dgesv", INFO); Nxx = B; } @@ -940,8 +1301,10 @@ 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."); + fe::raise(SVMP_HERE, + "[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 +1360,10 @@ 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'."); + fe::raise(SVMP_HERE, + "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 +1380,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/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..3c29a08f1 100644 --- a/Documentation/Doxyfile +++ b/Documentation/Doxyfile @@ -191,10 +191,10 @@ TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 USE_MATHJAX = YES -MATHJAX_VERSION = MathJax_3 -MATHJAX_FORMAT = chtml -MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@3 -MATHJAX_EXTENSIONS = ams +MATHJAX_VERSION = MathJax_2 +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 +MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols MATHJAX_CODEFILE = SEARCHENGINE = YES SERVER_BASED_SEARCH = NO 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..edeca5ac5 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_BasisErrorPaths.cpp @@ -0,0 +1,284 @@ +/** + * @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 + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +class MinimalScalarBasis : public BasisFunction { +public: + BasisType basis_type() const noexcept override { return BasisType::Lagrange; } + ElementType element_type() const noexcept override { return ElementType::Line2; } + 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(const math::Vector&, + std::vector& values) const override + { + values.assign(size(), Real(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; } + ElementType element_type() const noexcept override { return ElementType::Hex8; } + 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(const math::Vector& xi, + std::vector& values) const override + { + const Real x = xi[0]; + const Real y = xi[1]; + const Real z = xi[2]; + values.resize(size()); + values[0] = Real(1) + Real(2) * x - y + Real(0.5) * z + + x * x + Real(0.75) * y * y - Real(0.25) * z * z + + Real(0.2) * x * y - Real(0.3) * x * z + Real(0.4) * y * z; + values[1] = Real(3) - x + Real(2) * y + z + + Real(0.5) * x * x - y * y + z * z + + x * y + x * z - y * z; + } + + void evaluate_gradients(const math::Vector& xi, + std::vector& gradients) const override + { + const Real x = xi[0]; + const Real y = xi[1]; + const Real z = xi[2]; + gradients.assign(size(), Gradient::Zero()); + gradients[0][0] = Real(2) + Real(2) * x + Real(0.2) * y - Real(0.3) * z; + gradients[0][1] = Real(-1) + Real(1.5) * y + Real(0.2) * x + Real(0.4) * z; + gradients[0][2] = Real(0.5) - Real(0.5) * z - Real(0.3) * x + Real(0.4) * y; + gradients[1][0] = Real(-1) + x + y + z; + gradients[1][1] = Real(2) - Real(2) * y + x - z; + gradients[1][2] = Real(1) + Real(2) * z + x - y; + } + + void exact_hessians(std::vector& hessians) const + { + hessians.assign(size(), Hessian::Zero()); + hessians[0] = make_symmetric_hessian(Real(2), Real(1.5), Real(-0.5), + Real(0.2), Real(-0.3), Real(0.4)); + hessians[1] = make_symmetric_hessian(Real(1), Real(-2), Real(2), + Real(1), Real(1), Real(-1)); + } +}; + +class CompleteFallbackBasis : public BasisFunction { +public: + BasisType basis_type() const noexcept override { return BasisType::Lagrange; } + ElementType element_type() const noexcept override { return ElementType::Triangle3; } + 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(const math::Vector& xi, + std::vector& values) const override + { + values.resize(size()); + values[0] = Real(1) + xi[0]; + values[1] = Real(2) + xi[1]; + } + + void evaluate_gradients(const math::Vector&, + std::vector& gradients) const override + { + gradients.assign(size(), Gradient::Zero()); + gradients[0][0] = Real(1); + gradients[1][1] = Real(1); + } + + void evaluate_hessians(const math::Vector& xi, + std::vector& hessians) const override + { + hessians.assign(size(), Hessian::Zero()); + for (std::size_t d = 0; d < hessians.size(); ++d) { + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + hessians[d](r, c) = Real(100) * static_cast(d + 1u) + + Real(10) * static_cast(r) + + static_cast(c) + xi[2]; + } + } + } + } +}; + +} // 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); +} + +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); +} + +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); + EXPECT_THROW((void)basis_factory::create( + BasisRequest{ElementType::Line2, BasisType::Bernstein, 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 { + throw BasisConfigurationException("invalid config", __FILE__, __LINE__, __func__); + } catch (const FEException& e) { + EXPECT_EQ(e.status(), svmp::StatusCode::InvalidArgument); + } + + try { + throw BasisConstructionException("construction failure", __FILE__, __LINE__, __func__); + } catch (const FEException& e) { + EXPECT_EQ(e.status(), svmp::StatusCode::InternalError); + } +} + +TEST(BasisErrorPaths, NodeOrderingInvalidNodeThrows) { + EXPECT_THROW((void)ReferenceNodeLayout::get_node_coords(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{Real(0), Real(0), Real(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{Real(0.2), Real(-0.35), Real(0.4)}; + + 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], Real(1e-8)) + << "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), + Real(1e-8)) + << "basis=" << n << " component=(" << r << "," << c << ")"; + } + } + } +} + +TEST(BasisErrorPaths, BasisFunctionFallbackWritesSpanOutputs) { + CompleteFallbackBasis basis; + const math::Vector point{Real(0.25), Real(0.5), Real(-0.25)}; + + 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); + + std::vector expected_values; + std::vector expected_gradients; + std::vector expected_hessians; + basis.evaluate_all(point, expected_values, expected_gradients, expected_hessians); + for (std::size_t d = 0; d < basis.size(); ++d) { + EXPECT_EQ(span_values[d], expected_values[d]); + for (std::size_t c = 0; c < 3u; ++c) { + EXPECT_EQ(span_gradients[d][c], expected_gradients[d][c]); + } + for (std::size_t r = 0; r < 3u; ++r) { + for (std::size_t c = 0; c < 3u; ++c) { + EXPECT_EQ(span_hessians[d](r, c), expected_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..9ad458c0b --- /dev/null +++ b/tests/unitTests/FE/Basis/test_BasisHessians.cpp @@ -0,0 +1,415 @@ +/** + * @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 { + +void numerical_gradient_helper(const BasisFunction& basis, + const math::Vector& xi, + std::vector& gradients, + Real eps = Real(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]) / (Real(2) * eps); + } + } +} + +void numerical_hessian_helper(const BasisFunction& basis, + const math::Vector& xi, + std::vector& hessians, + Real eps = Real(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]) / (Real(2) * eps); + } + } + } +} + +std::vector> sample_points_for(ElementType type) { + switch (type) { + case ElementType::Line2: + return {{Real(-0.35), Real(0), Real(0)}, {Real(0.2), Real(0), Real(0)}}; + case ElementType::Triangle3: + return {{Real(0.15), Real(0.2), Real(0)}, {Real(0.25), Real(0.1), Real(0)}}; + case ElementType::Quad4: + return {{Real(0.2), Real(-0.3), Real(0)}, {Real(-0.45), Real(0.25), Real(0)}}; + case ElementType::Tetra4: + return {{Real(0.12), Real(0.18), Real(0.16)}, {Real(0.2), Real(0.1), Real(0.18)}}; + case ElementType::Hex8: + return {{Real(0.1), Real(-0.2), Real(0.3)}, {Real(-0.35), Real(0.25), Real(-0.15)}}; + case ElementType::Wedge6: + return {{Real(0.18), Real(0.22), Real(-0.2)}, {Real(0.12), Real(0.16), Real(0.1)}}; + default: + return {{Real(0), Real(0), Real(0)}}; + } +} + +void expect_gradients_match_numerical(const BasisFunction& basis, + const std::vector>& points, + Real tol, + Real eps = Real(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(basis.element_type()) + << ", order " << basis.order(); + } + } + } +} + +void expect_hessians_match_numerical(const BasisFunction& basis, + const std::vector>& points, + Real tol, + Real eps = Real(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(basis.element_type()) + << ", order " << basis.order(); + } + } + } + } +} + +void expect_partition_hessian_sum_zero(const LagrangeBasis& basis, + const math::Vector& xi, + Real 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)), + Real(0), + tol) + << "element " << static_cast(basis.element_type()) + << ", order " << basis.order(); + } + } +} + +void expect_hessians_symmetric(const LagrangeBasis& basis, + const math::Vector& xi, + Real 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_partition_hessian_sum_zero(const BasisFunction& basis, + const math::Vector& xi, + Real 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)), + Real(0), + tol) + << "element " << static_cast(basis.element_type()) + << ", order " << basis.order(); + } + } +} + +void expect_hessians_symmetric(const BasisFunction& basis, + const math::Vector& xi, + Real 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); + } + } + } +} + +std::vector> serendipity_sample_points(ElementType type) { + if (type == ElementType::Quad4 || type == ElementType::Quad8) { + return {{Real(0.17), Real(-0.31), Real(0)}, {Real(-0.45), Real(0.25), Real(0)}}; + } + if (type == ElementType::Hex8 || type == ElementType::Hex20) { + return {{Real(0.2), Real(-0.1), Real(0.3)}, {Real(-0.35), Real(0.25), Real(-0.15)}}; + } + return {{Real(0.2), Real(0.3), Real(0.1)}, {Real(0.12), Real(0.16), Real(-0.2)}}; +} + +} // namespace + +TEST(BasisHessians, LagrangeCanonicalTopologiesMatchNumericalHessians) { + const struct Case { + ElementType type; + int order; + Real tol; + Real eps; + } cases[] = { + {ElementType::Line2, 3, Real(1e-7), Real(1e-5)}, + {ElementType::Triangle3, 3, Real(2e-6), Real(1e-5)}, + {ElementType::Quad4, 3, Real(1e-6), Real(1e-5)}, + {ElementType::Tetra4, 2, Real(1e-6), Real(1e-5)}, + {ElementType::Hex8, 2, Real(1e-6), Real(1e-5)}, + {ElementType::Wedge6, 2, Real(1e-5), Real(1e-5)}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.type, c.order); + expect_hessians_match_numerical(basis, sample_points_for(c.type), c.tol, c.eps); + } +} + +TEST(BasisHessians, LagrangeHessiansSumToZeroAndAreSymmetric) { + const struct Case { + ElementType type; + int order; + math::Vector xi; + Real tol; + } cases[] = { + {ElementType::Line2, 3, {Real(0.15), Real(0), Real(0)}, Real(1e-12)}, + {ElementType::Triangle3, 3, {Real(0.2), Real(0.25), Real(0)}, Real(1e-10)}, + {ElementType::Quad4, 3, {Real(0.3), Real(-0.2), Real(0)}, Real(1e-12)}, + {ElementType::Tetra4, 2, {Real(0.15), Real(0.2), Real(0.1)}, Real(1e-10)}, + {ElementType::Hex8, 2, {Real(0.1), Real(-0.2), Real(0.3)}, Real(1e-12)}, + {ElementType::Wedge6, 2, {Real(0.2), Real(0.15), Real(-0.3)}, Real(1e-10)}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.type, c.order); + expect_partition_hessian_sum_zero(basis, c.xi, Real(10) * c.tol); + expect_hessians_symmetric(basis, c.xi, c.tol); + } +} + +TEST(BasisHessians, SerendipityHessiansSumToZeroAndAreSymmetric) { + const struct Case { + ElementType type; + int order; + math::Vector xi; + Real tol; + } cases[] = { + {ElementType::Quad8, 2, {Real(0.17), Real(-0.31), Real(0)}, Real(1e-10)}, + {ElementType::Hex20, 2, {Real(0.2), Real(-0.1), Real(0.3)}, Real(1e-10)}, + {ElementType::Wedge15, 2, {Real(0.2), Real(0.3), Real(0.1)}, Real(1e-10)}, + }; + + for (const auto& c : cases) { + SerendipityBasis basis(c.type, c.order); + expect_partition_hessian_sum_zero(basis, c.xi, c.tol); + expect_hessians_symmetric(basis, c.xi, c.tol); + } +} + +TEST(BasisHessians, SolverMappedVolumeSelectionsSatisfyInvariants) { + const struct Case { + ElementType type; + BasisType basis_type; + int order; + math::Vector xi; + Real tol; + } cases[] = { + {ElementType::Line2, BasisType::Lagrange, 1, {Real(0.15), Real(0), Real(0)}, Real(1e-12)}, + {ElementType::Line3, BasisType::Lagrange, 2, {Real(-0.25), Real(0), Real(0)}, Real(1e-12)}, + {ElementType::Triangle3, BasisType::Lagrange, 1, {Real(0.2), Real(0.25), Real(0)}, Real(1e-12)}, + {ElementType::Triangle6, BasisType::Lagrange, 2, {Real(0.2), Real(0.25), Real(0)}, Real(1e-12)}, + {ElementType::Quad4, BasisType::Lagrange, 1, {Real(0.3), Real(-0.2), Real(0)}, Real(1e-12)}, + {ElementType::Quad8, BasisType::Serendipity, 2, {Real(0.17), Real(-0.31), Real(0)}, Real(1e-10)}, + {ElementType::Quad9, BasisType::Lagrange, 2, {Real(0.3), Real(-0.2), Real(0)}, Real(1e-12)}, + {ElementType::Tetra4, BasisType::Lagrange, 1, {Real(0.15), Real(0.2), Real(0.1)}, Real(1e-12)}, + {ElementType::Tetra10, BasisType::Lagrange, 2, {Real(0.15), Real(0.2), Real(0.1)}, Real(1e-10)}, + {ElementType::Hex8, BasisType::Lagrange, 1, {Real(0.1), Real(-0.2), Real(0.3)}, Real(1e-12)}, + {ElementType::Hex20, BasisType::Serendipity, 2, {Real(0.2), Real(-0.1), Real(0.3)}, Real(1e-10)}, + {ElementType::Hex27, BasisType::Lagrange, 2, {Real(0.1), Real(-0.2), Real(0.3)}, Real(1e-12)}, + {ElementType::Wedge6, BasisType::Lagrange, 1, {Real(0.2), Real(0.15), Real(-0.3)}, Real(1e-12)}, + }; + + int covered = 0; + for (const auto& c : cases) { + auto basis = basis_factory::create(BasisRequest{c.type, c.basis_type, c.order}); + expect_partition_hessian_sum_zero(*basis, c.xi, c.tol); + expect_hessians_symmetric(*basis, c.xi, c.tol); + ++covered; + } + + EXPECT_EQ(covered, 13); +} + +// 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 { + ElementType type; + int order; + Real tol; + } cases[] = { + {ElementType::Line2, 3, Real(1e-8)}, + {ElementType::Triangle3, 3, Real(1e-7)}, + {ElementType::Quad4, 3, Real(1e-7)}, + {ElementType::Tetra4, 2, Real(1e-7)}, + {ElementType::Hex8, 2, Real(1e-7)}, + {ElementType::Wedge6, 2, Real(1e-7)}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.type, c.order); + expect_gradients_match_numerical(basis, sample_points_for(c.type), 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) { + const struct Case { + ElementType type; + int order; + Real tol; + } cases[] = { + {ElementType::Quad4, 1, Real(1e-8)}, + {ElementType::Quad8, 2, Real(1e-7)}, + {ElementType::Quad4, 3, Real(1e-7)}, + {ElementType::Quad4, 4, Real(5e-7)}, + {ElementType::Hex8, 1, Real(1e-8)}, + {ElementType::Hex20, 2, Real(1e-7)}, + {ElementType::Wedge15, 2, Real(1e-7)}, + }; + + for (const auto& c : cases) { + SerendipityBasis basis(c.type, c.order); + expect_gradients_match_numerical(basis, serendipity_sample_points(c.type), c.tol); + } +} + +TEST(BasisHessians, SerendipityFamiliesMatchNumericalHessians) { + const struct Case { + ElementType type; + int order; + Real tol; + } cases[] = { + {ElementType::Quad4, 1, Real(1e-6)}, + {ElementType::Quad8, 2, Real(1e-6)}, + {ElementType::Quad4, 3, Real(1e-6)}, + {ElementType::Quad4, 4, Real(5e-6)}, + {ElementType::Hex8, 1, Real(1e-6)}, + {ElementType::Hex20, 2, Real(1e-6)}, + {ElementType::Wedge15, 2, Real(1e-6)}, + }; + + for (const auto& c : cases) { + SerendipityBasis basis(c.type, c.order); + expect_hessians_match_numerical(basis, serendipity_sample_points(c.type), 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..44e588fdc --- /dev/null +++ b/tests/unitTests/FE/Basis/test_ConstexprBasis.cpp @@ -0,0 +1,129 @@ +/** + * @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 +#include + +namespace svmp { +namespace FE { +namespace basis { +namespace { + +static_assert(is_line(ElementType::Line2)); +static_assert(is_line(ElementType::Line3)); +static_assert(is_triangle(ElementType::Triangle6)); +static_assert(is_quadrilateral(ElementType::Quad8)); +static_assert(is_tetrahedron(ElementType::Tetra10)); +static_assert(is_hexahedron(ElementType::Hex20)); +static_assert(is_wedge(ElementType::Wedge18)); +static_assert(!is_pyramid(ElementType::Pyramid5)); +static_assert(!is_pyramid(ElementType::Pyramid14)); +static_assert(is_simplex(ElementType::Triangle3)); +static_assert(is_simplex(ElementType::Tetra4)); +static_assert(!is_simplex(ElementType::Wedge6)); +static_assert(is_tensor_product(ElementType::Line2)); +static_assert(is_tensor_product(ElementType::Quad9)); +static_assert(is_tensor_product(ElementType::Hex27)); +static_assert(!is_tensor_product(ElementType::Wedge6)); +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); +static_assert(line_lagrange_size(2) == 3u); +static_assert(triangle_lagrange_size(2) == 6u); +static_assert(quad_lagrange_size(2) == 9u); +static_assert(tetra_lagrange_size(2) == 10u); +static_assert(hex_lagrange_size(2) == 27u); +static_assert(wedge_lagrange_size(2) == 18u); +static_assert(complete_lagrange_alias_size(ElementType::Pyramid14) == 0u); +static_assert(detail::basis_abs(Real(-2)) == Real(2)); +static_assert(detail::basis_max(Real(2), Real(3)) == Real(3)); +static_assert(detail::basis_near_zero(std::numeric_limits::epsilon() * Real(32))); +static_assert(detail::basis_nearly_equal( + Real(1), + Real(1) + std::numeric_limits::epsilon() * Real(32))); + +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, TraitToleranceScalesWithRealPrecision) { + const Real eps = std::numeric_limits::epsilon(); + EXPECT_GT(detail::basis_scaled_tolerance(), eps); + EXPECT_TRUE(detail::basis_near_zero(eps * Real(32))); + EXPECT_FALSE(detail::basis_near_zero(eps * Real(128))); + EXPECT_TRUE(detail::basis_nearly_equal(Real(1), Real(1) + eps * Real(32))); + EXPECT_FALSE(detail::basis_nearly_equal(Real(1), Real(1) + eps * Real(128))); +} + +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::get_node_coords(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..8827eebb0 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_HigherOrderWedge.cpp @@ -0,0 +1,157 @@ +/** + * @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, + Real 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, Real 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 Real expected = (i == node) ? Real(1) : Real(0); + EXPECT_NEAR(values[i], expected, tol) + << "node " << node << ", basis " << i; + } + } +} + +void expect_partition_gradient_hessian_sums(const LagrangeBasis& basis, + const std::vector>& points, + Real value_tol, + Real derivative_tol) +{ + for (const auto& xi : points) { + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + Real value_sum = Real(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, Real(1), value_tol); + for (int d = 0; d < basis.dimension(); ++d) { + EXPECT_NEAR(gradient_sum[static_cast(d)], Real(0), derivative_tol); + for (int e = 0; e < basis.dimension(); ++e) { + EXPECT_NEAR(hessian_sum(static_cast(d), + static_cast(e)), + Real(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, 1); + const auto generated = + ReferenceNodeLayout::get_lagrange_node_coords(ElementType::Wedge6, 2); + + ASSERT_EQ(generated.size(), ReferenceNodeLayout::num_nodes(ElementType::Wedge18)); + EXPECT_EQ(alias_basis.element_type(), ElementType::Wedge6); + EXPECT_EQ(alias_basis.order(), 2); + expect_nodes_close(alias_basis.nodes(), generated, Real(1e-14)); +} + +TEST(HigherOrderWedge, OrderThreeIsNodalAndPartitionsUnity) { + LagrangeBasis wedge(ElementType::Wedge6, 3); + + expect_kronecker_at_nodes(wedge, Real(2e-10)); + expect_partition_gradient_hessian_sums( + wedge, + { + {Real(0.18), Real(0.22), Real(-0.2)}, + {Real(0.12), Real(0.16), Real(0.1)}, + {Real(0.25), Real(0.15), Real(0.45)}, + }, + Real(1e-12), + Real(1e-9)); +} + +TEST(HigherOrderWedge, OrderFourEvaluationsRemainFinite) { + LagrangeBasis wedge(ElementType::Wedge6, 4); + + expect_all_entries_finite(wedge, {Real(0.2), Real(0.1), Real(-0.6)}); + expect_all_entries_finite(wedge, {Real(0.05), Real(0.8), Real(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(ElementType::Wedge6, 4); + + EXPECT_EQ(wedge.size(), 75u); + expect_kronecker_at_nodes(wedge, Real(1e-9)); + expect_partition_gradient_hessian_sums( + wedge, + { + {Real(0.18), Real(0.22), Real(-0.2)}, + {Real(0.25), Real(0.15), Real(0.45)}, + }, + Real(1e-12), + Real(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..68232d216 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_LagrangeBasis.cpp @@ -0,0 +1,605 @@ +/** + * @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; + +struct CanonicalCase { + ElementType type; + int order; + std::size_t size; + int dimension; + std::vector points; + Real derivative_tol; +}; + +const std::vector& canonical_cases() { + static const std::vector cases = { + {ElementType::Line2, 3, 4u, 1, + {{Real(-0.35), Real(0), Real(0)}, {Real(0.2), Real(0), Real(0)}}, + Real(1e-11)}, + {ElementType::Triangle3, 3, 10u, 2, + {{Real(0.15), Real(0.2), Real(0)}, {Real(0.25), Real(0.1), Real(0)}}, + Real(1e-9)}, + {ElementType::Quad4, 3, 16u, 2, + {{Real(0.2), Real(-0.3), Real(0)}, {Real(-0.45), Real(0.25), Real(0)}}, + Real(1e-11)}, + {ElementType::Tetra4, 2, 10u, 3, + {{Real(0.12), Real(0.18), Real(0.16)}, {Real(0.2), Real(0.1), Real(0.18)}}, + Real(1e-9)}, + {ElementType::Hex8, 2, 27u, 3, + {{Real(0.1), Real(-0.2), Real(0.3)}, {Real(-0.35), Real(0.25), Real(-0.15)}}, + Real(1e-10)}, + {ElementType::Wedge6, 2, 18u, 3, + {{Real(0.18), Real(0.22), Real(-0.2)}, {Real(0.12), Real(0.16), Real(0.1)}}, + Real(1e-9)}, + }; + return cases; +} + +std::vector sample_points_for(ElementType type) { + for (const auto& c : canonical_cases()) { + if (c.type == type) { + return c.points; + } + } + return {}; +} + +void expect_kronecker_at_nodes(const LagrangeBasis& basis, Real 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 ? Real(1) : Real(0), tol) + << "node=" << node << " basis=" << i; + } + } +} + +void expect_partition_gradient_hessian_sums(const LagrangeBasis& basis, + const std::vector& points, + Real derivative_tol) +{ + for (const auto& xi : points) { + std::vector values; + std::vector gradients; + std::vector hessians; + basis.evaluate_all(xi, values, gradients, hessians); + + Real value_sum = Real(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, Real(1), Real(1e-12)); + for (int d = 0; d < basis.dimension(); ++d) { + EXPECT_NEAR(gradient_sum[static_cast(d)], Real(0), derivative_tol); + for (int e = 0; e < basis.dimension(); ++e) { + EXPECT_NEAR(hessian_sum(static_cast(d), + static_cast(e)), + Real(0), + derivative_tol); + } + } + } +} + +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], Real(1e-14)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(span_gradients[i][d], gradients[i][d], Real(1e-14)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(span_hessians[i](d, e), + hessians[i](d, e), + Real(1e-14)); + } + } + } +} + +void expect_nodes_close(const std::vector& lhs, + const std::vector& rhs, + Real 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, + Real 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); + } + } + } + } +} + +Real linear_function(const Point& p) { + return Real(2) + Real(3) * p[0] - Real(4) * p[1] + Real(5) * p[2]; +} + +Gradient linear_gradient() { + Gradient g = Gradient::Zero(); + g[0] = Real(3); + g[1] = Real(-4); + g[2] = Real(5); + return g; +} + +Real quadratic_function(const Point& p) { + return Real(1) + Real(2) * p[0] - p[1] + Real(0.5) * p[2] + + p[0] * p[0] + Real(0.75) * p[1] * p[1] - Real(0.25) * p[2] * p[2] + + Real(0.2) * p[0] * p[1] - Real(0.3) * p[0] * p[2] + + Real(0.4) * p[1] * p[2]; +} + +// Total degree three, so it lies in both the P3 simplex space and the Q3 +// tensor-product space. +Real cubic_function(const Point& p) { + return quadratic_function(p) + + Real(0.1) * p[0] * p[0] * p[0] - + Real(0.2) * p[1] * p[1] * p[1] + + Real(0.3) * p[2] * p[2] * p[2] + + Real(0.15) * p[0] * p[0] * p[1] - + Real(0.12) * p[0] * p[2] * p[2] + + Real(0.08) * p[0] * p[1] * p[2]; +} + +template +Real interpolate_value(const LagrangeBasis& basis, + const std::vector& values, + Function&& nodal_function) +{ + Real result = Real(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.type, c.order); + EXPECT_EQ(basis.basis_type(), BasisType::Lagrange); + EXPECT_EQ(basis.element_type(), c.type); + 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.type, c.order); + expect_kronecker_at_nodes(basis, Real(2e-10)); + expect_partition_gradient_hessian_sums(basis, c.points, c.derivative_tol); + } +} + +TEST(LagrangeBasis, SpanOutputSinksMatchVectorEvaluationAcrossTopologies) { + for (const auto& c : canonical_cases()) { + LagrangeBasis basis(c.type, c.order); + expect_span_sinks_match_vector_evaluation(basis, c.points.front()); + } +} + +TEST(LagrangeBasis, CompleteAliasesNormalizeToCanonicalBases) { + const std::vector> aliases = { + {ElementType::Line3, ElementType::Line2, 2}, + {ElementType::Triangle6, ElementType::Triangle3, 2}, + {ElementType::Quad9, ElementType::Quad4, 2}, + {ElementType::Tetra10, ElementType::Tetra4, 2}, + {ElementType::Hex27, ElementType::Hex8, 2}, + {ElementType::Wedge18, ElementType::Wedge6, 2}, + }; + + for (const auto& [alias, canonical, order] : aliases) { + LagrangeBasis alias_basis(alias, 1); + LagrangeBasis canonical_basis(canonical, order); + const auto generated = ReferenceNodeLayout::get_lagrange_node_coords(canonical, order); + + EXPECT_EQ(alias_basis.element_type(), canonical); + EXPECT_EQ(alias_basis.order(), order); + expect_nodes_close(alias_basis.nodes(), generated, Real(1e-14)); + expect_nodes_close(alias_basis.nodes(), canonical_basis.nodes(), Real(1e-14)); + expect_evaluations_match(alias_basis, + canonical_basis, + sample_points_for(canonical), + Real(1e-12)); + } +} + +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::get_node_coords(alias, i); + EXPECT_NEAR(public_node[0], generated[i][0], Real(1e-14)) << "node=" << i; + EXPECT_NEAR(public_node[1], generated[i][1], Real(1e-14)) << "node=" << i; + EXPECT_NEAR(public_node[2], generated[i][2], Real(1e-14)) << "node=" << i; + } + } +} + +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); + } +} + +TEST(LagrangeBasis, LinearPolynomialReproductionAcrossLinearTopologies) { + const std::vector> cases = { + {ElementType::Line2, {Real(-0.2), Real(0), Real(0)}}, + {ElementType::Triangle3, {Real(0.2), Real(0.3), Real(0)}}, + {ElementType::Quad4, {Real(0.25), Real(-0.4), Real(0)}}, + {ElementType::Tetra4, {Real(0.1), Real(0.2), Real(0.3)}}, + {ElementType::Hex8, {Real(0.15), Real(-0.2), Real(0.25)}}, + {ElementType::Wedge6, {Real(0.2), Real(0.15), Real(-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 Real interpolated = + interpolate_value(basis, values, linear_function); + EXPECT_NEAR(interpolated, linear_function(point), Real(1e-12)); + + Gradient interpolated_gradient = Gradient::Zero(); + for (std::size_t i = 0; i < gradients.size(); ++i) { + const Real 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)], + Real(1e-12)); + } + } +} + +TEST(LagrangeBasis, QuadraticPolynomialReproductionAcrossQuadraticAliases) { + const std::vector> cases = { + {ElementType::Line3, {Real(-0.2), Real(0), Real(0)}}, + {ElementType::Triangle6, {Real(0.2), Real(0.3), Real(0)}}, + {ElementType::Quad9, {Real(0.25), Real(-0.4), Real(0)}}, + {ElementType::Tetra10, {Real(0.1), Real(0.2), Real(0.3)}}, + {ElementType::Hex27, {Real(0.15), Real(-0.2), Real(0.25)}}, + {ElementType::Wedge18, {Real(0.2), Real(0.15), Real(-0.3)}}, + }; + + for (const auto& [type, point] : cases) { + LagrangeBasis basis(type, 1); + std::vector values; + basis.evaluate_values(point, values); + + const Real interpolated = + interpolate_value(basis, values, quadratic_function); + EXPECT_NEAR(interpolated, quadratic_function(point), Real(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 its llround-based +// inverse index mapping (a duplicated or missing node makes the basis +// non-nodal here). +TEST(LagrangeBasis, HigherOrderLatticesAreNodalAndPartitionUnity) { + const struct Case { + ElementType type; + int order; + std::size_t size; + Real kronecker_tol; + Real derivative_tol; + std::vector points; + } cases[] = { + {ElementType::Tetra4, 3, 20u, Real(5e-10), Real(1e-8), + {{Real(0.12), Real(0.18), Real(0.16)}, {Real(0.3), Real(0.2), Real(0.25)}}}, + {ElementType::Tetra4, 4, 35u, Real(1e-9), Real(1e-7), + {{Real(0.12), Real(0.18), Real(0.16)}, {Real(0.2), Real(0.1), Real(0.18)}}}, + {ElementType::Hex8, 3, 64u, Real(5e-10), Real(1e-8), + {{Real(0.1), Real(-0.2), Real(0.3)}, {Real(-0.35), Real(0.25), Real(-0.15)}}}, + }; + + for (const auto& c : cases) { + LagrangeBasis basis(c.type, c.order); + EXPECT_EQ(basis.size(), c.size); + expect_kronecker_at_nodes(basis, c.kronecker_tol); + expect_partition_gradient_hessian_sums(basis, c.points, c.derivative_tol); + } +} + +TEST(LagrangeBasis, CubicPolynomialReproductionAtOrderThree) { + const std::vector> cases = { + {ElementType::Tetra4, {Real(0.15), Real(0.2), Real(0.25)}}, + {ElementType::Hex8, {Real(0.15), Real(-0.2), Real(0.25)}}, + }; + + for (const auto& [type, point] : cases) { + LagrangeBasis basis(type, 3); + std::vector values; + basis.evaluate_values(point, values); + + const Real interpolated = interpolate_value(basis, values, cubic_function); + EXPECT_NEAR(interpolated, cubic_function(point), Real(1e-10)) + << "element=" << static_cast(type); + } +} + +TEST(LagrangeBasis, PointTopologyEvaluatesConstantUnity) { + LagrangeBasis basis(ElementType::Point1, 0); + + EXPECT_EQ(basis.element_type(), ElementType::Point1); + EXPECT_EQ(basis.size(), 1u); + EXPECT_EQ(basis.dimension(), 0); + ASSERT_EQ(basis.nodes().size(), 1u); + + const Point xi{Real(0.3), Real(-0.4), Real(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], Real(1)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(gradients[0][d], Real(0)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_EQ(hessians[0](d, e), Real(0)); + } + } + + Real span_value = Real(-1); + Gradient span_gradient; + span_gradient[0] = span_gradient[1] = span_gradient[2] = Real(-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) = Real(-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, Real(1)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(span_gradient[d], Real(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), Real(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 types = { + ElementType::Line2, + ElementType::Triangle3, + ElementType::Quad4, + ElementType::Tetra4, + ElementType::Hex8, + ElementType::Wedge6, + }; + + for (const auto type : types) { + LagrangeBasis basis(type, 0); + EXPECT_EQ(basis.order(), 0) << "element=" << static_cast(type); + EXPECT_EQ(basis.size(), 1u) << "element=" << static_cast(type); + + for (const auto& xi : sample_points_for(type)) { + 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], Real(1), Real(1e-14)) + << "element=" << static_cast(type); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(gradients[0][d], Real(0), Real(1e-14)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(hessians[0](d, e), Real(0), Real(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); +} + +TEST(LagrangeBasis, FactoryCreatesReducedScalarBasisFamilies) { + auto lagrange = + basis_factory::create(BasisRequest{ElementType::Hex27, BasisType::Lagrange, 1}); + ASSERT_NE(lagrange, nullptr); + EXPECT_EQ(lagrange->basis_type(), BasisType::Lagrange); + EXPECT_EQ(lagrange->element_type(), ElementType::Hex8); + EXPECT_EQ(lagrange->order(), 2); + + auto serendipity = + basis_factory::create(BasisRequest{ElementType::Quad8, BasisType::Serendipity, 2}); + ASSERT_NE(serendipity, nullptr); + EXPECT_EQ(serendipity->basis_type(), BasisType::Serendipity); + + 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); +} diff --git a/tests/unitTests/FE/Basis/test_SerendipityTensorModal.cpp b/tests/unitTests/FE/Basis/test_SerendipityTensorModal.cpp new file mode 100644 index 000000000..235dc8c40 --- /dev/null +++ b/tests/unitTests/FE/Basis/test_SerendipityTensorModal.cpp @@ -0,0 +1,289 @@ +/** + * @file test_SerendipityTensorModal.cpp + * @brief Tests for the migrated Serendipity basis subset. + */ + +#include + +#include "FE/Basis/LagrangeBasis.h" +#include "FE/Basis/NodeOrderingConventions.h" +#include "FE/Basis/SerendipityBasis.h" + +#include +#include + +using namespace svmp::FE; +using namespace svmp::FE::basis; + +namespace { + +void expect_partition_of_unity(const SerendipityBasis& basis, + const math::Vector& xi, + Real tolerance = Real(1e-10)) +{ + std::vector values; + std::vector gradients; + basis.evaluate_values(xi, values); + basis.evaluate_gradients(xi, gradients); + + Real value_sum = Real(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, Real(1), tolerance); + for (int component = 0; component < basis.dimension(); ++component) { + EXPECT_NEAR(gradient_sum[static_cast(component)], + Real(0), + tolerance); + } +} + +void expect_nodal_delta(const SerendipityBasis& basis, + const std::vector>& nodes, + Real 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 ? Real(1) : Real(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::get_node_coords(type, i)); + } + return nodes; +} + +template +Real interpolate_nodal_function(const SerendipityBasis& basis, + const math::Vector& xi, + Function&& nodal_function) +{ + std::vector values; + basis.evaluate_values(xi, values); + + Real result = Real(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; +} + +// Every monomial here has superlinear degree at most three, so it lies in the +// order-three quadrilateral serendipity space. +Real cubic_serendipity_function(const math::Vector& p) { + const Real x = p[0]; + const Real y = p[1]; + return Real(1) + Real(2) * x - y + Real(3) * x * y + + x * x * x - Real(2) * y * y * y + + Real(0.5) * x * x * x * y - Real(0.25) * x * y * y * y; +} + +Real bilinear_function(const math::Vector& p) { + return Real(2) - Real(3) * p[0] + Real(4) * p[1] + Real(0.5) * p[0] * p[1]; +} + +} // namespace + +TEST(SerendipityBasis, Quad8IsNodalAndPartitionsUnity) { + SerendipityBasis basis(ElementType::Quad8, 2); + + EXPECT_EQ(basis.size(), 8u); + expect_nodal_delta(basis, basis.nodes(), Real(1e-10)); + expect_partition_of_unity(basis, {Real(0.17), Real(-0.31), Real(0)}); +} + +TEST(SerendipityBasis, Hex20IsNodalAndPartitionsUnity) { + SerendipityBasis basis(ElementType::Hex20, 2); + + EXPECT_EQ(basis.size(), 20u); + expect_nodal_delta(basis, + reference_nodes(ElementType::Hex20, basis.size()), + Real(1e-10)); + expect_partition_of_unity(basis, {Real(0.2), Real(-0.1), Real(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()), + Real(1e-9)); + expect_partition_of_unity(basis, {Real(0.2), Real(0.3), Real(0.1)}); +} + +TEST(SerendipityBasis, RejectsUnsupportedSerendipityAliases) { + EXPECT_THROW(SerendipityBasis(ElementType::Quad9, 2), FEException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid13, 2), FEException); + EXPECT_THROW(SerendipityBasis(ElementType::Pyramid14, 2), FEException); + EXPECT_THROW(SerendipityBasis(ElementType::Quad8, 3), FEException); +} + +// Orders other than two run the generic quadrilateral path: serendipity +// monomial selection, boundary plus interior node placement, and a runtime +// Vandermonde inversion whose unisolvence is assumed rather than tabulated. +// Order four is the first order that selects 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(ElementType::Quad4, 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]), Real(1)); + EXPECT_LE(std::abs(node[1]), Real(1)); + } + + expect_nodal_delta(basis, basis.nodes(), Real(1e-9)); + expect_partition_of_unity(basis, {Real(0.17), Real(-0.31), Real(0)}, Real(1e-9)); + expect_partition_of_unity(basis, {Real(-0.45), Real(0.25), Real(0)}, Real(1e-9)); + } +} + +TEST(SerendipityBasis, QuadrilateralOrderOneReproducesBilinearFunctions) { + SerendipityBasis basis(ElementType::Quad4, 1); + + const std::vector> points = { + {Real(0.25), Real(-0.4), Real(0)}, + {Real(-0.7), Real(0.6), Real(0)}, + }; + for (const auto& xi : points) { + EXPECT_NEAR(interpolate_nodal_function(basis, xi, bilinear_function), + bilinear_function(xi), + Real(1e-12)); + } +} + +TEST(SerendipityBasis, QuadrilateralOrderThreeReproducesSerendipityCubics) { + SerendipityBasis basis(ElementType::Quad4, 3); + + const std::vector> points = { + {Real(0.25), Real(-0.4), Real(0)}, + {Real(-0.7), Real(0.6), Real(0)}, + }; + for (const auto& xi : points) { + EXPECT_NEAR(interpolate_nodal_function(basis, xi, cubic_serendipity_function), + cubic_serendipity_function(xi), + Real(1e-11)); + } +} + +// SerendipityBasis(Hex8, 1) is the only route to the hand-written trilinear +// corner evaluator (values, gradients, and Hessians); it must agree with the +// trilinear Lagrange basis on the same element. +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()), + Real(1e-12)); + + const std::vector> points = { + {Real(0.2), Real(-0.1), Real(0.3)}, + {Real(-0.35), Real(0.25), Real(-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], Real(1e-13)); + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(s_gradients[i][d], l_gradients[i][d], Real(1e-13)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(s_hessians[i](d, e), l_hessians[i](d, e), Real(1e-13)); + } + } + } + } +} + +// Geometry mode keeps the public Hex20 node count while mapping geometry with +// the trilinear corner functions: corners must match the Hex8 basis exactly +// and the quadratic edge nodes must contribute nothing. +TEST(SerendipityBasis, Hex20GeometryModeUsesTrilinearCornersOnly) { + SerendipityBasis geometry(ElementType::Hex20, 2, true); + SerendipityBasis trilinear(ElementType::Hex8, 1); + + EXPECT_EQ(geometry.size(), 20u); + EXPECT_EQ(geometry.order(), 2); + + const std::vector> points = { + {Real(0.2), Real(-0.1), Real(0.3)}, + {Real(-0.35), Real(0.25), Real(-0.15)}, + }; + for (const auto& xi : points) { + std::vector g_values; + std::vector g_gradients; + std::vector g_hessians; + geometry.evaluate_all(xi, g_values, g_gradients, g_hessians); + ASSERT_EQ(g_values.size(), 20u); + + std::vector t_values; + std::vector t_gradients; + std::vector t_hessians; + trilinear.evaluate_all(xi, t_values, t_gradients, t_hessians); + + Real value_sum = Real(0); + for (std::size_t i = 0; i < 20u; ++i) { + value_sum += g_values[i]; + if (i < 8u) { + EXPECT_NEAR(g_values[i], t_values[i], Real(1e-13)) << "corner=" << i; + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_NEAR(g_gradients[i][d], t_gradients[i][d], Real(1e-13)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_NEAR(g_hessians[i](d, e), t_hessians[i](d, e), Real(1e-13)); + } + } + } else { + EXPECT_EQ(g_values[i], Real(0)) << "edge node=" << i; + for (std::size_t d = 0; d < 3u; ++d) { + EXPECT_EQ(g_gradients[i][d], Real(0)); + for (std::size_t e = 0; e < 3u; ++e) { + EXPECT_EQ(g_hessians[i](d, e), Real(0)); + } + } + } + } + EXPECT_NEAR(value_sum, Real(1), Real(1e-13)); + } +} diff --git a/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp b/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp new file mode 100644 index 000000000..9e9e08e95 --- /dev/null +++ b/tests/unitTests/FE/Math/test_DenseLinearAlgebra.cpp @@ -0,0 +1,374 @@ +/** + * @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 { + +Real multiply_entry(const std::vector& A, + const std::vector& B, + std::size_t n, + std::size_t row, + std::size_t col) { + Real sum = Real(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{ + Real(1.0e9), Real(2.0e6), + Real(3.0e3), Real(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 Real expected = (row == col) ? Real(1) : Real(0); + EXPECT_NEAR(multiply_entry(A, inv, 2u, row, col), expected, Real(1.0e-10)); + } + } +} + +TEST(DenseLinearAlgebra, FactorizationSolvesMultipleRightHandSides) { + const std::vector A{ + Real(4), Real(2), Real(0), + Real(2), Real(5), Real(1), + Real(0), Real(1), Real(3) + }; + + const auto solver = factor_dense_matrix(A, 3u, "symmetric 3x3"); + EXPECT_EQ(solver.diagnostics.rank, 3u); + + const std::vector rhs{Real(2), Real(4), Real(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) { + Real ax = Real(0); + for (std::size_t col = 0; col < 3u; ++col) { + ax += A[row * 3u + col] * x[col]; + } + EXPECT_NEAR(ax, rhs[row], Real(1.0e-12)); + } + + std::vector second_rhs{Real(1), Real(-2), Real(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) { + Real ax = Real(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], Real(1.0e-12)); + } +} + +TEST(DenseLinearAlgebra, FactorizationSolvesDenseRightHandSideBlock) { + const std::vector A{ + Real(4), Real(2), Real(0), + Real(2), Real(5), Real(1), + Real(0), Real(1), Real(3) + }; + + const auto solver = factor_dense_matrix(A, 3u, "symmetric 3x3 block"); + + std::vector rhs{ + Real(2), Real(1), + Real(4), Real(-2), + Real(6), Real(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) { + Real ax = Real(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], Real(1.0e-12)); + } + } +} + +// Every other matrix in this file already has its largest pivot on the +// diagonal, so without these cases the row-exchange branch in +// factor_dense_matrix and the permutation replay in solve_in_place never +// execute. SerendipityBasis inverts its Vandermonde matrices through this +// code in production. +TEST(DenseLinearAlgebra, FactorizationPivotsThroughZeroLeadingDiagonal) { + const std::vector swap_2x2{ + Real(0), Real(1), + Real(1), Real(0) + }; + + const auto solver = factor_dense_matrix(swap_2x2, 2u, "swap 2x2"); + const std::vector rhs{Real(3), Real(7)}; + const auto x = solver.solve(std::span(rhs.data(), rhs.size())); + ASSERT_EQ(x.size(), 2u); + EXPECT_NEAR(x[0], Real(7), Real(1.0e-14)); + EXPECT_NEAR(x[1], Real(3), Real(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], Real(1.0e-14)); + } + } + + // Every column requires a row exchange during elimination. + const std::vector permuted_scaled{ + Real(0), Real(0), Real(1), Real(0), + Real(1), Real(0), Real(0), Real(0), + Real(0), Real(0), Real(0), Real(2), + Real(0), Real(3), Real(0), Real(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 Real expected = (row == col) ? Real(1) : Real(0); + EXPECT_NEAR(multiply_entry(permuted_scaled, inv4, 4u, row, col), + expected, + Real(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{ + Real(0), Real(2), Real(1), + Real(4), Real(1), Real(0), + Real(1), Real(0), Real(3) + }; + constexpr std::size_t kRhsCount = 33u; + + const auto solver = factor_dense_matrix(A, 3u, "pivoting 3x3"); + + std::vector rhs(3u * kRhsCount, Real(0)); + for (std::size_t row = 0; row < 3u; ++row) { + for (std::size_t r = 0; r < kRhsCount; ++r) { + rhs[row * kRhsCount + r] = + Real(1) + static_cast(row) - Real(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) { + Real ax = Real(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], Real(1.0e-12)) + << "rhs column " << r << ", row " << row; + } + } +} + +TEST(DenseLinearAlgebra, SolveInPlaceValidatesInputs) { + const std::vector identity{ + Real(1), Real(0), + Real(0), Real(1) + }; + const auto solver = factor_dense_matrix(identity, 2u, "identity 2x2"); + + std::vector rhs{Real(1), Real(2)}; + EXPECT_THROW(solver.solve_in_place(std::span(rhs.data(), rhs.size()), 0u), + FEException); + + std::vector wrong_size{Real(1), Real(2), Real(3)}; + EXPECT_THROW( + solver.solve_in_place(std::span(wrong_size.data(), wrong_size.size()), 1u), + FEException); + + DenseLUSolver unfactored; + unfactored.n = 2u; + unfactored.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{ + Real(1), Real(0), Real(2), + Real(0), Real(1), Real(-1) + }; + EXPECT_EQ(dense_matrix_rank(wide_full, 2u, 3u), 2u); + + const std::vector tall_rank_one{ + Real(1), Real(2), + Real(2), Real(4), + Real(3), Real(6) + }; + EXPECT_EQ(dense_matrix_rank(tall_rank_one, 3u, 2u), 1u); +} + +TEST(DenseLinearAlgebra, HighConditionInverseUsesSvdFallback) { + const std::vector high_condition{ + Real(1), Real(0), + Real(0), Real(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 Real expected = (row == col) ? Real(1) : Real(0); + EXPECT_NEAR(multiply_entry(high_condition, result.inverse, 2u, row, col), + expected, + Real(1.0e-12)); + } + } +} + +TEST(DenseLinearAlgebra, DiagnosticValidationRejectsUnsupportedCondition) { + DenseInverseResult result; + result.diagnostics.rank = 2u; + result.diagnostics.condition_estimate = + dense_matrix_condition_error_threshold() * Real(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{ + Real(1.0e12), Real(2.0e12), + Real(0.5e12), Real(1.0e12) + }; + + EXPECT_THROW((void)invert_dense_matrix(singular, 2u, "singular 2x2"), + FEException); +} + +TEST(DenseLinearAlgebra, FactorizationThrowsForRankDeficientMatrix) { + const std::vector singular{ + Real(1), Real(2), + Real(2), Real(4) + }; + + EXPECT_THROW((void)factor_dense_matrix(singular, 2u, "rank-one 2x2"), + FEException); +} + +TEST(DenseLinearAlgebra, RankUsesScaleAwareTolerance) { + const std::vector rank_one{ + Real(1.0e8), Real(2.0e8), + Real(3.0e8), Real(6.0e8) + }; + EXPECT_EQ(dense_matrix_rank(rank_one, 2u, 2u), 1u); + + const std::vector full_rank{ + Real(1.0e8), Real(2.0e8), + Real(3.0e8), Real(6.1e8) + }; + EXPECT_EQ(dense_matrix_rank(full_rank, 2u, 2u), 2u); +} + +TEST(DenseLinearAlgebra, DiagnosticsReportRankAndConditionEstimate) { + const std::vector diagonal{ + Real(4), Real(0), + Real(0), Real(0.5) + }; + const auto full = + dense_matrix_diagnostics(diagonal, 2u, 2u, "diagonal 2x2"); + EXPECT_EQ(full.rank, 2u); + EXPECT_NEAR(full.largest_singular_value, Real(4), Real(1.0e-14)); + EXPECT_NEAR(full.smallest_retained_singular_value, Real(0.5), Real(1.0e-14)); + EXPECT_NEAR(full.condition_estimate, Real(8), Real(1.0e-14)); + + const std::vector rank_one{ + Real(1), Real(2), + Real(2), Real(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{ + Real(1), Real(2), + Real(2), Real(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], Real(0.04), Real(1.0e-13)); + EXPECT_NEAR(pinv.inverse[1], Real(0.08), Real(1.0e-13)); + EXPECT_NEAR(pinv.inverse[2], Real(0.08), Real(1.0e-13)); + EXPECT_NEAR(pinv.inverse[3], Real(0.16), Real(1.0e-13)); + + std::vector projection(4u, Real(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], + Real(1.0e-12)); + } + } +} + +TEST(DenseLinearAlgebra, PseudoInverseDropsNearZeroSingularValues) { + const std::vector near_singular{ + Real(1), Real(0), + Real(0), Real(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, Real(1.0e-18)); + EXPECT_NEAR(pinv.inverse[0], Real(1), Real(1.0e-14)); + EXPECT_NEAR(pinv.inverse[1], Real(0), Real(1.0e-14)); + EXPECT_NEAR(pinv.inverse[2], Real(0), Real(1.0e-14)); + EXPECT_NEAR(pinv.inverse[3], Real(0), Real(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