diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d08355264..c3a55bf8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,8 @@ jobs: path: | ${{ github.workspace }}/hdf5 ${{ github.workspace }}/suitesparse/install - key: ${{ runner.os }}-deps-${{ steps.get-msvc.outputs.version }} + ${{ github.workspace }}/eigen/install + key: ${{ runner.os }}-deps-${{ steps.get-msvc.outputs.version }}-1 - name: Download MKL and TBB run: | cd "${env:BASE_DIR}" @@ -44,7 +45,7 @@ jobs: nuget install intelmkl.static.win-x64 -Version 2023.0.0.25930 Invoke-WebRequest -Uri "https://gitlab.com/libeigen/eigen/-/archive/master/eigen-master.zip" -OutFile eigen.zip 7z x eigen.zip - - name: Build UMFPACK and HDF5 + - name: Build UMFPACK and HDF5 and Eigen3 if: steps.cache.outputs.cache-hit != 'true' run: | $base_dir = $($env:BASE_DIR.Replace('\', '/')) @@ -69,6 +70,14 @@ jobs: $ENV:MKLROOT="${env:BASE_DIR}/intelmkl.static.win-x64.2023.0.0.25930/lib/native/win-x64".Replace('\', '/') cmake -DCMAKE_INSTALL_PREFIX="${base_dir}\suitesparse\install" -DBLA_VENDOR=Intel10_64lp_seq -DBLA_STATIC=ON -G "Ninja" -DCMAKE_C_FLAGS="/GL" -DCMAKE_STATIC_LINKER_FLAGS="/LTCG" -DCMAKE_BUILD_TYPE=Release -DBUILD_METIS=OFF ..\suitesparse-metis-for-windows-e8d953dffb8a99aa8b65ff3ff03e12a3ed72f90c\ ninja install + cd "${env:BASE_DIR}" + Invoke-WebRequest -Uri "https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip" -OutFile eigen.zip + 7z x eigen.zip -oeigen\code -y + cd eigen + mkdir build + cd build + cmake ../code/eigen-3.4.0 -DCMAKE_INSTALL_PREFIX="../install" + cmake --build . --target install - name: Build and Install run: | cd "${env:BASE_DIR}" @@ -78,6 +87,7 @@ jobs: $ENV:MKLROOT="${env:BASE_DIR}/intelmkl.static.win-x64.2023.0.0.25930/lib/native/win-x64".Replace('\', '/') $ENV:TBB_ROOT="${env:BASE_DIR}/inteltbb.devel.win.2021.8.0.25874/lib/native".Replace('\', '/') $ENV:UMFPACK_ROOT="${env:BASE_DIR}/suitesparse/install".Replace('\', '/') + $ENV:Eigen3_DIR="${env:BASE_DIR}/eigen/install".Replace('\', '/') $install_prefix = $($env:INSTALL_PREFIX.Replace('\', '/')) $src_dir = $($env:SRC_DIR.Replace('\', '/')) $base_dir = $($env:BASE_DIR.Replace('\', '/')) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b426b1eb..406cac7b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,9 @@ add_feature_info(ENABLE_DEBUG_THREADING ENABLE_DEBUG_THREADING "Use multi-thread option(ENABLE_GRM_2D "Build 2D general rate model" ON) add_feature_info(ENABLE_GRM_2D ENABLE_GRM_2D "Build 2D general rate model") +option(ENABLE_DG "Build DG variants of models" ON) +add_feature_info(ENABLE_DG ENABLE_DG "Build DG variants of models") + option(ENABLE_SUNDIALS_OPENMP "Prefer OpenMP vector implementation of SUNDIALS if available (for large problems)" OFF) add_feature_info(ENABLE_SUNDIALS_OPENMP ENABLE_SUNDIALS_OPENMP "Prefer OpenMP vector implementation of SUNDIALS if available (for large problems)") @@ -394,6 +397,21 @@ if (ENABLE_GRM_2D) ) endif() +set(EIGEN_TARGET "") +if (ENABLE_DG) + find_package(Eigen3 3.4 NO_MODULE) + + # Disable DG if Eigen is not present + if (NOT TARGET Eigen3::Eigen) + message(STATUS "Disabling DG support because Eigen3 could not be found") + set(ENABLE_DG OFF) + else() + set(EIGEN_TARGET "Eigen3::Eigen") + get_target_property(Eigen3_INCLUDE_DIRS Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) + include_directories(${Eigen3_INCLUDE_DIRS} "${Eigen3_INCLUDE_DIRS}/..") + endif() +endif() + set(IPO_AVAILABLE OFF) if (ENABLE_IPO) include(CheckIPOSupported) @@ -592,6 +610,14 @@ if (ENABLE_GRM_2D) endif() endif() +if (ENABLE_DG) + message("Found Eigen3: ${Eigen3_FOUND}") + if (TARGET Eigen3::Eigen) + message(" Version ${Eigen3_VERSION}") + message(" Includes ${Eigen3_INCLUDE_DIRS}") + endif() +endif() + message("Found HDF5: ${HDF5_FOUND}") if (HDF5_FOUND) message(" Version ${HDF5_VERSION}") diff --git a/doc/interface/sensitivities.rst b/doc/interface/sensitivities.rst index 958476ef1..e179ae8b9 100644 --- a/doc/interface/sensitivities.rst +++ b/doc/interface/sensitivities.rst @@ -39,7 +39,7 @@ Group /input/sensitivity/param_XXX ``SENS_NAME`` - Name of the parameter + Name of the parameter (Note that ``PAR_RADIUS`` and ``PAR_CORE_RADIUS`` sensitivities are only available for Finite Volume discretization) ================ =========================== **Type:** string **Length:** :math:`\geq 1` diff --git a/doc/interface/unit_operations/general_rate_model.rst b/doc/interface/unit_operations/general_rate_model.rst index 461225160..6f9a01658 100644 --- a/doc/interface/unit_operations/general_rate_model.rst +++ b/doc/interface/unit_operations/general_rate_model.rst @@ -454,13 +454,20 @@ For information on model equations, refer to :ref:`general_rate_model_model`. ================ ======================== ============================================================================= -Group /input/model/unit_XXX/discretization - UNIT_TYPE - GENERAL_RATE_MODEL ---------------------------------------------------------------------------- +Discretization Methods +---------------------- + +CADET has two discretization frameworks available, Finite Volumes (FV) and Discontinuous Galerkin (DG), only one needs to be specified. Both methods approximate the same solution to the same underlying model but can differ regarding computational performance. + +Group /input/model/unit_XXX/discretization - UNIT_TYPE - GENERAL_RATE_MODEL +---------------------------------------------------------------------------------------- +Finite Volumes (Default) +------------------------ ``NCOL`` - Number of axial column discretization cells + Number of axial column discretization points ============= ========================= ============= **Type:** int **Range:** :math:`\geq 1` **Length:** 1 @@ -468,7 +475,7 @@ Group /input/model/unit_XXX/discretization - UNIT_TYPE - GENERAL_RATE_MODEL ``NPAR`` - Number of particle (radial) discretization cells for each particle type + Number of particle (radial) discretization points for each particle type ============= ========================= ================================================= **Type:** int **Range:** :math:`\geq 1` **Length:** :math:`1` / :math:`\texttt{NPARTYPE}` @@ -556,3 +563,95 @@ Group /input/model/unit_XXX/discretization - UNIT_TYPE - GENERAL_RATE_MODEL For further discretization parameters, see also :ref:`flux_restruction_methods`, and :ref:`non_consistency_solver_parameters`. +Group /input/model/unit_XXX/discretization - UNIT_TYPE - GENERAL_RATE_MODEL_DG +---------------------------------------------------------------------------------------- +Discontinuous Galerkin +---------------------- + +``POLYDEG`` + + DG polynomial degree. Optional, defaults to 4. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``NCOL`` + + Number of axial column discretization DG cells\elements. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``EXACT_INTEGRATION`` + + Specifies the DG integration method. Optional, defaults to 0: Choose 1 for exact integration (more accurate but slower), 0 for LGL quadrature (less accurate but faster, typically more performant). + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= + +``NPARTYPE`` + + Number of particle types. Optional, inferred from the length of :math:`\texttt{NPAR}` or :math:`\texttt{NBOUND}` if left out. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``PARPOLYDEG`` + + DG particle (radial) polynomial degree. Optional, defaults to 3. The total number of particle (radial) discrete points is given by (``PARPOLYDEG`` + 1 ) * ``NPARCELL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``NPARCELL`` + + Number of particle (radial) discretization DG cells for each particle type. For the particle discretization, it is usually most performant to fix ``NPARCELL`` = 1 and to increase the polynomial degree for more accuracy. + + ============= ========================= ================================================= + **Type:** int **Range:** :math:`\geq 1` **Length:** :math:`1` / :math:`\texttt{NPARTYPE}` + ============= ========================= ================================================= + +``NBOUND`` + + Number of bound states for each component + + ============= ========================= ================================== + **Type:** int **Range:** :math:`\geq 0` **Length:** :math:`\texttt{NCOMP}` + ============= ========================= ================================== + +``PAR_GEOM`` + + Specifies the particle geometry for all or each particle type. Valid values are :math:`\texttt{SPHERE}`, :math:`\texttt{CYLINDER}`, :math:`\texttt{SLAB}`. Optional, defaults to :math:`\texttt{SPHERE}`. + + ================ ================================================= + **Type:** string **Length:** :math:`1` / :math:`\texttt{NPARTYPE}` + ================ ================================================= + +``PAR_DISC_TYPE`` + + Specifies the discretization scheme inside the particles for all or each particle type. Valid values are :math:`\texttt{EQUIDISTANT_PAR}`, :math:`\texttt{EQUIVOLUME_PAR}`, and :math:`\texttt{USER_DEFINED_PAR}`. + + ================ ================================================= + **Type:** string **Length:** :math:`1` / :math:`\texttt{NPARTYPE}` + ================ ================================================= + +``PAR_DISC_VECTOR`` + + Node coordinates for the cell boundaries (ignored if :math:`\texttt{PAR_DISC_TYPE} \neq \texttt{USER_DEFINED_PAR}`). The coordinates are relative and have to include the endpoints :math:`0` and :math:`1`. They are later linearly mapped to the true radial range :math:`[r_{c,j}, r_{p,j}]`. The coordinates for each particle type are appended to one long vector in type-major ordering. + + ================ ======================== ================================================ + **Type:** double **Range:** :math:`[0,1]` **Length:** :math:`\sum_i (\texttt{NPAR}_i + 1)` + ================ ======================== ================================================ + +``USE_ANALYTIC_JACOBIAN`` + + Determines whether analytically computed Jacobian matrix (faster) is used (value is 1) instead of Jacobians generated by algorithmic differentiation (slower, value is 0) + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= diff --git a/doc/interface/unit_operations/lumped_rate_model_with_pores.rst b/doc/interface/unit_operations/lumped_rate_model_with_pores.rst index f4378a972..f11d8b96a 100644 --- a/doc/interface/unit_operations/lumped_rate_model_with_pores.rst +++ b/doc/interface/unit_operations/lumped_rate_model_with_pores.rst @@ -247,13 +247,19 @@ For information on model equations, refer to :ref:`lumped_rate_model_with_pores_ ================ ======================== ======================================================================= +Discretization Methods +---------------------- + +CADET has two discretization frameworks available, Finite Volumes (FV) and Discontinuous Galerkin (DG), only one needs to be specified. Both methods approximate the same solution to the same underlying model but can differ regarding computational performance. + Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITH_PORES ------------------------------------------------------------------------------------- +Finite Volumes (Default) +------------------------ - ``NCOL`` - Number of axial column discretization cells + Number of axial column discretization points ============= ========================= ============= **Type:** int **Range:** :math:`\geq 1` **Length:** 1 @@ -308,3 +314,49 @@ Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITH_ ================ ========================= ============= For further discretization parameters, see also :ref:`flux_restruction_methods`, and :ref:`non_consistency_solver_parameters`. + + +Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITH_PORES_DG +---------------------------------------------------------------------------------------- +Discontinuous Galerkin +---------------------- + +``POLYDEG`` + + DG polynomial degree. Optional, defaults to 4. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``NCOL`` + + Number of axial column discretization DG cells\elements. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``EXACT_INTEGRATION`` + + Specifies the DG integration method. Optional, defaults to 0: Choose 1 for exact integration (more accurate but slower), 0 for LGL quadrature (less accurate but faster, typically more performant). + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= + +``NBOUND`` + + Number of bound states for each component + + ============= ========================= ================================== + **Type:** int **Range:** :math:`\geq 0` **Length:** :math:`\texttt{NCOMP}` + ============= ========================= ================================== + +``USE_ANALYTIC_JACOBIAN`` + + Determines whether analytically computed Jacobian matrix (faster) is used (value is 1) instead of Jacobians generated by algorithmic differentiation (slower, value is 0) + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= diff --git a/doc/interface/unit_operations/lumped_rate_model_without_pores.rst b/doc/interface/unit_operations/lumped_rate_model_without_pores.rst index e801fbb7a..5a65933f5 100644 --- a/doc/interface/unit_operations/lumped_rate_model_without_pores.rst +++ b/doc/interface/unit_operations/lumped_rate_model_without_pores.rst @@ -141,13 +141,19 @@ For information on model equations, refer to :ref:`lumped_rate_model_without_por ================ ===================== ============= +Discretization Methods +---------------------- + +CADET has two discretization frameworks available, Finite Volumes (FV) and Discontinuous Galerkin (DG), only one needs to be specified. Both methods approximate the same solution to the same underlying model but can differ regarding computational performance. + Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITHOUT_PORES ---------------------------------------------------------------------------------------- - +Finite Volumes (Default) +------------------------ ``NCOL`` - Number of axial column discretization cells + Number of axial column discretization points ============= ========================= ============= **Type:** int **Range:** :math:`\geq 1` **Length:** 1 @@ -163,7 +169,7 @@ Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITHO ``RECONSTRUCTION`` - Type of reconstruction method for fluxes + Type of reconstruction method for fluxes only (only needs to be specified for FV) ================ ================================ ============= **Type:** string **Range:** :math:`\texttt{WENO}` **Length:** 1 @@ -171,3 +177,39 @@ Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITHO For further discretization parameters, see also :ref:`flux_restruction_methods`, and :ref:`non_consistency_solver_parameters`. +Group /input/model/unit_XXX/discretization - UNIT_TYPE = LUMPED_RATE_MODEL_WITHOUT_PORES_DG +------------------------------------------------------------------------------------------- +Discontinuous Galerkin +---------------------- + +``POLYDEG`` + + DG polynomial degree. Optional, defaults to 4. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``NCOL`` + + Number of axial column discretization DG cells\elements. The total number of axial discrete points is given by (``POLYDEG`` + 1 ) * ``NCOL``. + + ============= ========================= ============= + **Type:** int **Range:** :math:`\geq 1` **Length:** 1 + ============= ========================= ============= + +``EXACT_INTEGRATION`` + + Specifies the DG integration method. Optional, defaults to 0: Choose 1 for exact integration (more accurate but slower), 0 for LGL quadrature (less accurate but faster, typically more performant). + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= + +``USE_ANALYTIC_JACOBIAN`` + + Determines whether analytically computed Jacobian matrix (faster) is used (value is 1) instead of Jacobians generated by algorithmic differentiation (slower, value is 0) + + ============= =========================== ============= + **Type:** int **Range:** :math:`\{0, 1\}` **Length:** 1 + ============= =========================== ============= diff --git a/doc/literature.bib b/doc/literature.bib index de1673281..b12e824aa 100644 --- a/doc/literature.bib +++ b/doc/literature.bib @@ -436,4 +436,27 @@ @article{Jaepel2022 url = {https://www.sciencedirect.com/science/article/pii/S0021967322005830}, author = {Ronald Colin Jäpel and Johannes Felix Buyel}, } - +@book{Kopriva2009, +address = {Dordrecht}, +author = {Kopriva, David A.}, +series = {Scientific {Computation}}, +title = {Implementing {Spectral} {Methods} for {Partial} {Differential} {Equations}: {Algorithms} for {Scientists} and {Engineers}}, +isbn = {978-90-481-2260-8 978-90-481-2261-5}, +shorttitle = {Implementing {Spectral} {Methods} for {Partial} {Differential} {Equations}}, +url = {http://link.springer.com/10.1007/978-90-481-2261-5}, +urldate = {2024-01-12}, +publisher = {Springer Netherlands}, +year = {2009}, +doi = {https://doi.org/10.1007/978-90-481-2261-5}, +} +@article{Breuer2023, +title = {Spatial discontinuous Galerkin spectral element method for a family of chromatography models in CADET}, +journal = {Computers \& Chemical Engineering}, +volume = {177}, +pages = {108340}, +year = {2023}, +issn = {0098-1354}, +doi = {https://doi.org/10.1016/j.compchemeng.2023.108340}, +url = {https://www.sciencedirect.com/science/article/pii/S0098135423002107}, +author = {Jan Michael Breuer and Samuel Leweke and Johannes Schmölder and Gregor Gassner and Eric {von Lieres}}, +} diff --git a/src/libcadet/CMakeLists.txt b/src/libcadet/CMakeLists.txt index c1c550a0b..2bd8d6422 100644 --- a/src/libcadet/CMakeLists.txt +++ b/src/libcadet/CMakeLists.txt @@ -163,6 +163,9 @@ set (LIBCADET_NONLINALG_SOURCES ${CMAKE_SOURCE_DIR}/src/libcadet/nonlin/CompositeSolver.cpp ${CMAKE_SOURCE_DIR}/src/libcadet/nonlin/Solver.cpp ) +if (ENABLE_DG) + list(APPEND LIBCADET_NONLINALG_SOURCES ${CMAKE_SOURCE_DIR}/src/libcadet/linalg/BandedEigenSparseRowIterator.hpp) +endif() if (ENABLE_GRM_2D) set(LIBCADET_NONLINALG_SPARSE_SOURCES) @@ -188,6 +191,14 @@ else() endif() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/linalg/SparseSolverInterface.hpp.in" "${CMAKE_CURRENT_BINARY_DIR}/SparseSolverInterface.hpp" @ONLY) +if (ENABLE_DG) + list (APPEND LIBCADET_SOURCES + ${CMAKE_SOURCE_DIR}/src/libcadet/model/LumpedRateModelWithPoresDG.cpp + ${CMAKE_SOURCE_DIR}/src/libcadet/model/LumpedRateModelWithoutPoresDG.cpp + ${CMAKE_SOURCE_DIR}/src/libcadet/model/GeneralRateModelDG.cpp + ) +endif() + # Preprocess binding and reaction models foreach(BM IN LISTS LIBCADET_BINDINGMODEL_SOURCES) get_filename_component(BMFILEWE ${BM} NAME_WE) @@ -239,11 +250,10 @@ if (LAPACK_FOUND) endif() endif() - # Add the build target for CADET object library add_library(libcadet_object OBJECT ${LIBCADET_SOURCES}) target_compile_definitions(libcadet_object PRIVATE libcadet_EXPORTS ${LIB_LAPACK_DEFINE}) - target_link_libraries(libcadet_object PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET}) + target_link_libraries(libcadet_object PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET} ${EIGEN_TARGET}) # --------------------------------------------------- # Build the static library @@ -251,7 +261,7 @@ if (LAPACK_FOUND) add_library(libcadet_static STATIC $) set_target_properties(libcadet_static PROPERTIES OUTPUT_NAME cadet_static) - target_link_libraries(libcadet_static PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET}) + target_link_libraries(libcadet_static PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET} ${EIGEN_TARGET}) # --------------------------------------------------- # Build the shared library @@ -259,7 +269,7 @@ if (LAPACK_FOUND) add_library(libcadet_shared SHARED $) set_target_properties(libcadet_shared PROPERTIES OUTPUT_NAME cadet) - target_link_libraries (libcadet_shared PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET}) + target_link_libraries (libcadet_shared PUBLIC CADET::CompileOptions CADET::LibOptions PRIVATE CADET::AD libcadet_nonlinalg_static SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET} ${EIGEN_TARGET}) list(APPEND LIBCADET_TARGETS libcadet_nonlinalg_static libcadet_object libcadet_static libcadet_shared) diff --git a/src/libcadet/CompileTimeConfig.hpp.in b/src/libcadet/CompileTimeConfig.hpp.in index 5be0f62ca..1bebf0b77 100644 --- a/src/libcadet/CompileTimeConfig.hpp.in +++ b/src/libcadet/CompileTimeConfig.hpp.in @@ -14,5 +14,6 @@ #define LIBCADET_COMPILETIMECONFIG_HPP_ #cmakedefine ENABLE_GRM_2D +#cmakedefine ENABLE_DG #endif // LIBCADET_COMPILETIMECONFIG_HPP_ diff --git a/src/libcadet/ModelBuilderImpl.cpp b/src/libcadet/ModelBuilderImpl.cpp index b3b087fd5..7004ee565 100644 --- a/src/libcadet/ModelBuilderImpl.cpp +++ b/src/libcadet/ModelBuilderImpl.cpp @@ -40,6 +40,11 @@ namespace cadet #ifdef ENABLE_GRM_2D void registerGeneralRateModel2D(std::unordered_map>& models); #endif +#ifdef ENABLE_DG + void registerGeneralRateModelDG(std::unordered_map>& models); + void registerLumpedRateModelWithPoresDG(std::unordered_map>& models); + void registerLumpedRateModelWithoutPoresDG(std::unordered_map>& models); +#endif namespace inlet { @@ -66,7 +71,11 @@ namespace cadet #ifdef ENABLE_GRM_2D model::registerGeneralRateModel2D(_modelCreators); #endif - +#ifdef ENABLE_DG + model::registerGeneralRateModelDG(_modelCreators); + model::registerLumpedRateModelWithPoresDG(_modelCreators); + model::registerLumpedRateModelWithoutPoresDG(_modelCreators); +#endif // Register all available inlet profiles model::inlet::registerPiecewiseCubicPoly(_inletCreators); diff --git a/src/libcadet/linalg/BandedEigenSparseRowIterator.hpp b/src/libcadet/linalg/BandedEigenSparseRowIterator.hpp new file mode 100644 index 000000000..36dfa0982 --- /dev/null +++ b/src/libcadet/linalg/BandedEigenSparseRowIterator.hpp @@ -0,0 +1,263 @@ +/** + * @file + * Defines RowIterator for Eigen lib compressed sparse matrix + */ + +#ifndef LIBCADET_BANDEDEIGENSPARSEROWITERATOR_HPP_ +#define LIBCADET_BANDEDEIGENSPARSEROWITERATOR_HPP_ + +#include +#include +#include +#include +#include + +#include "SparseSolverInterface.hpp" +#include "cadet/cadetCompilerInfo.hpp" +#include "common/CompilerSpecific.hpp" + +namespace cadet +{ + +namespace linalg +{ + + +class BandedEigenSparseRowIterator +{ +public: + /** + * @brief Creates a ConstBandedSparseRowIterator pointing nowhere + */ + BandedEigenSparseRowIterator() : _matrix(nullptr), _values(nullptr), _colIdx(nullptr), _row(-1), _numNonZero(0), _dummy(0.0) { } + + /** + * @brief Creates a BandedSparseRowIterator of the given matrix row + * @param [in] mat Matrix + * @param [in] row Index of the row + */ + BandedEigenSparseRowIterator(Eigen::SparseMatrix& mat, int row) + : _matrix(&mat), _values(valuesOfRow(mat, row)), _colIdx(columnIndicesOfRow(mat, row)), _row(row), + _numNonZero(getInnerNumberOfNonZeros(mat, row)), _dummy(0.0) + { + } + + ~BandedEigenSparseRowIterator() CADET_NOEXCEPT { } + + // Default copy and assignment semantics + BandedEigenSparseRowIterator(const BandedEigenSparseRowIterator& cpy) = default; + BandedEigenSparseRowIterator& operator=(const BandedEigenSparseRowIterator& cpy) = default; + + BandedEigenSparseRowIterator(BandedEigenSparseRowIterator&& cpy) CADET_NOEXCEPT = default; +#ifdef COMPILER_SUPPORT_NOEXCEPT_DEFAULTED_MOVE + BandedSparseRowIterator& operator=(BandedSparseRowIterator&& cpy) CADET_NOEXCEPT = default; +#else + BandedEigenSparseRowIterator& operator=(BandedEigenSparseRowIterator&& cpy) = default; +#endif + + /** + * @brief Sets all matrix elements in the row to the given value + * @param [in] val Value all matrix elements in the row are set to + */ + inline void setAll(double val) + { + std::fill(_values, _values + _numNonZero, val); + } + + /** + * @brief Copies a row of another iterator to the row of this iterator + * @details Assumes the same sparsity pattern of source and destination row. + * @param [in] it Iterator pointing to a row of a matrix + */ + template + inline void copyRowFrom(const OtherIterator_t& it) + { + cadet_assert(_numNonZero == it.numNonZeros()); + std::copy(it.data(), it.data() + _numNonZero, _values); + } + + /** + * @brief Adds the given array to the current row + * @details Performs the operation @f$ y = y + \alpha x @f$, where @f$ x @f$ may only be a + * subset of the current row the iterator points to. The start of the subset is + * given by @p startDiag. The subset has to fully fit into the matrix row. + * @param [in] row Pointer to array @f$ x @f$ that is added to the given row @f$ y @f$ + * @param [in] startDiag Index of the diagonal at which the row is added + * @param [in] length Length of the array + * @param [in] factor Factor @f$ \alpha @f$ + */ + inline void addArray(double const* row, int startDiag, int length, double factor) + { + // shift to column index to start with + int idx = startDiag - _row; + + for (int i = 0; i < length; i++) { + _values[idx + i] = factor * row[i]; + } + + } + + /** + * @brief Accesses an element in the current row where the main diagonal is centered (index @c 0) + * @details The @p diagonal determines the element in a row. A negative index indicates a lower diagonal, + * while a positive index indicates an upper diagonal. If @p diagonal is @c 0, then the main + * diagonal is retrieved. + * + * @param [in] diagonal Index of the diagonal + * + * @return Matrix element at the given position + */ + inline double& centered(int diagonal) { return native(_row + diagonal); } + inline double centered(int diagonal) const { return native(_row + diagonal); } + + /** + * @brief Accesses an element in the current row where the lowest diagonal is indexed by @c 0 + * @param [in] col Index of the column + * @return Matrix element at the given position + */ + inline double& native(int col) + { + cadet_assert((col >= 0) && (col < _matrix->rows())); + + // Try to find the element + // TODO: Use binary search + for (int i = 0; i < _numNonZero; ++i) + { + if (_colIdx[i] == col) + return _values[i]; + } + + // We don't have it + _dummy = 0.0; + return _dummy; + } + + inline double native(sparse_int_t col) const + { + cadet_assert((col >= 0) && (col < _matrix->rows())); + + // Try to find the element + // TODO: Use binary search + for (int i = 0; i < _numNonZero; ++i) + { + if (_colIdx[i] == col) + return _values[i]; + } + + // We don't have it + return 0.0; + } + + inline double& operator()(int diagonal) { return centered(diagonal); } + inline double operator()(int diagonal) const { return centered(diagonal); } + + inline double& operator[](int diagonal) { return centered(diagonal); } + inline double operator[](int diagonal) const { return centered(diagonal); } + + inline BandedEigenSparseRowIterator& operator++() CADET_NOEXCEPT + { + ++_row; + updateOnRowChange(); + return *this; + } + + inline BandedEigenSparseRowIterator& operator--() CADET_NOEXCEPT + { + --_row; + updateOnRowChange(); + return *this; + } + + inline BandedEigenSparseRowIterator& operator+=(int idx) CADET_NOEXCEPT + { + _row += idx; + updateOnRowChange(); + return *this; + } + + inline BandedEigenSparseRowIterator& operator-=(int idx) CADET_NOEXCEPT + { + _row -= idx; + updateOnRowChange(); + return *this; + } + + inline BandedEigenSparseRowIterator operator+(int op) const CADET_NOEXCEPT + { + return BandedEigenSparseRowIterator(*_matrix, _row + op); + } + + inline friend BandedEigenSparseRowIterator operator+(int op, const BandedEigenSparseRowIterator& it) CADET_NOEXCEPT + { + return BandedEigenSparseRowIterator(*it._matrix, op + it.row()); + } + + inline BandedEigenSparseRowIterator operator-(int op) const CADET_NOEXCEPT + { + return BandedEigenSparseRowIterator(*_matrix, _row - op); + } + + inline friend BandedEigenSparseRowIterator operator-(int op, const BandedEigenSparseRowIterator& it) CADET_NOEXCEPT + { + return BandedEigenSparseRowIterator(*it._matrix, op - it.row()); + } + + /** + * @brief Returns the underlying matrix this iterator is pointing into + * @return Matrix this iterator is pointing into + */ + inline const Eigen::SparseMatrix& matrix() const CADET_NOEXCEPT { return *_matrix; } + + /** + * @brief Returns the index of the current row + * @return Index of the current row + */ + inline int row() const CADET_NOEXCEPT { return _row; } + + /** + * @brief Returns the number of non-zero entries in this row + * @return Number of non-zero entries in this row + */ + inline int numNonZeros() const { return _numNonZero; } + + /** + * @brief Returns an array of the matrix entries in this row + * @return Array with matrix entries in this row + */ + inline double* data() { return _values; } + inline double const* data() const { return _values; } + +protected: + + inline void updateOnRowChange() + { + _values = valuesOfRow(*_matrix, _row); + _colIdx = columnIndicesOfRow(*_matrix, _row); + _numNonZero = getInnerNumberOfNonZeros(*_matrix, _row); + } + + inline double* valuesOfRow(Eigen::SparseMatrix& mat, int row) { + return mat.valuePtr() + mat.outerIndexPtr()[row]; + } + + inline const int* columnIndicesOfRow(Eigen::SparseMatrix& mat, int row) { + return mat.innerIndexPtr() + mat.outerIndexPtr()[row]; + } + + inline int getInnerNumberOfNonZeros(Eigen::SparseMatrix& mat, int row) { + return mat.outerIndexPtr()[row + 1] - mat.outerIndexPtr()[row]; + } + + Eigen::SparseMatrix* _matrix; + double* _values; + int const* _colIdx; + int _row; + int _numNonZero; + double _dummy; +}; + +} // namespace linalg + +} // namespace cadet + +#endif // LIBCADET_BANDEDEIGENSPARSEROWITERATOR_HPP_ \ No newline at end of file diff --git a/src/libcadet/model/BindingModel.hpp b/src/libcadet/model/BindingModel.hpp index 321509ee8..8a003c027 100644 --- a/src/libcadet/model/BindingModel.hpp +++ b/src/libcadet/model/BindingModel.hpp @@ -20,10 +20,16 @@ #include +#include "CompileTimeConfig.hpp" #include "cadet/ParameterProvider.hpp" #include "cadet/ParameterId.hpp" #include "linalg/DenseMatrix.hpp" #include "linalg/BandMatrix.hpp" + +#ifdef ENABLE_DG + #include "linalg/BandedEigenSparseRowIterator.hpp" +#endif + #include "AutoDiff.hpp" #include "SimulationTypes.hpp" #include "Memory.hpp" @@ -292,7 +298,9 @@ class IBindingModel */ virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, int offsetCp, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const = 0; virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, int offsetCp, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const = 0; - +#ifdef ENABLE_DG + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, int offsetCp, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const = 0; +#endif /** * @brief Calculates the time derivative of the quasi-stationary bound state equations * @details Calculates @f$ \frac{\partial \text{flux}_{\text{qs}}}{\partial t} @f$ for the quasi-stationary equations diff --git a/src/libcadet/model/GeneralRateModel.cpp b/src/libcadet/model/GeneralRateModel.cpp index 2ffa4f806..42a3cbbc3 100644 --- a/src/libcadet/model/GeneralRateModel.cpp +++ b/src/libcadet/model/GeneralRateModel.cpp @@ -100,7 +100,7 @@ unsigned int GeneralRateModel::numDofs() const CADET_NOEXCEPT // Column bulk DOFs: nCol * nComp // Particle DOFs: nCol * nParType particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs // in each shell; there are nParCell shells for each particle type - // Flux DOFs: nCol * nComp * nParType (as many as column bulk DOFs) + // Flux DOFs: nCol * nComp * nParType (column bulk DOFs times particle types) // Inlet DOFs: nComp return _disc.nCol * (_disc.nComp * (1 + _disc.nParType)) + _disc.parTypeOffset[_disc.nParType] + _disc.nComp; } @@ -110,7 +110,7 @@ unsigned int GeneralRateModel::numPureDofs() const CADET_NOEXCEPT // Column bulk DOFs: nCol * nComp // Particle DOFs: nCol particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs // in each shell; there are nPar shells - // Flux DOFs: nCol * nComp (as many as column bulk DOFs) + // Flux DOFs: nCol * nComp * nParType (column bulk DOFs times particle types) return _disc.nCol * (_disc.nComp * (1 + _disc.nParType)) + _disc.parTypeOffset[_disc.nParType]; } diff --git a/src/libcadet/model/GeneralRateModelDG-InitialConditions.cpp b/src/libcadet/model/GeneralRateModelDG-InitialConditions.cpp new file mode 100644 index 000000000..c96383c3b --- /dev/null +++ b/src/libcadet/model/GeneralRateModelDG-InitialConditions.cpp @@ -0,0 +1,1317 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2022: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/GeneralRateModelDG.hpp" +#include "model/BindingModel.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/Subset.hpp" +#include "ParamReaderHelper.hpp" +#include "AdUtils.hpp" +#include "model/parts/BindingCellKernel.hpp" +#include "SimulationTypes.hpp" +#include "SensParamUtil.hpp" + +#include +#include + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include "ParallelSupport.hpp" +#ifdef CADET_PARALLELIZE + #include +#endif + +namespace cadet +{ + +namespace model +{ + +int GeneralRateModelDG::multiplexInitialConditions(const cadet::ParameterId& pId, unsigned int adDirection, double adValue) +{ + if (_singleBinding) + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initCp[pId.component]); + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initCp[t * _disc.nComp + pId.component].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initQ[_disc.nBoundBeforeType[0] + _disc.boundOffset[pId.component] + pId.boundState]); + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initQ[_disc.nBoundBeforeType[t] + _disc.boundOffset[t * _disc.nComp + pId.component] + pId.boundState].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + else + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initCp[pId.particleType * _disc.nComp + pId.component]); + _initCp[pId.particleType * _disc.nComp + pId.component].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState]); + _initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + return 0; +} + +int GeneralRateModelDG::multiplexInitialConditions(const cadet::ParameterId& pId, double val, bool checkSens) +{ + if (_singleBinding) + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initCp[pId.component])) + return -1; + + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initCp[t * _disc.nComp + pId.component].setValue(val); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initQ[_disc.nBoundBeforeType[0] + _disc.boundOffset[pId.component] + pId.boundState])) + return -1; + + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initQ[_disc.nBoundBeforeType[t] + _disc.boundOffset[t * _disc.nComp + pId.component] + pId.boundState].setValue(val); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + else + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initCp[pId.particleType * _disc.nComp + pId.component])) + return -1; + + _initCp[pId.particleType * _disc.nComp + pId.component].setValue(val); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState])) + return -1; + + _initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState].setValue(val); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + return 0; +} + +void GeneralRateModelDG::applyInitialCondition(const SimulationState& simState) const +{ + Indexer idxr(_disc); + + // Check whether full state vector is available as initial condition + if (!_initState.empty()) + { + std::fill(simState.vecStateY, simState.vecStateY + idxr.offsetC(), 0.0); + std::copy(_initState.data(), _initState.data() + numPureDofs(), simState.vecStateY + idxr.offsetC()); + + if (!_initStateDot.empty()) + { + std::fill(simState.vecStateYdot, simState.vecStateYdot + idxr.offsetC(), 0.0); + std::copy(_initStateDot.data(), _initStateDot.data() + numPureDofs(), simState.vecStateYdot + idxr.offsetC()); + } + else + std::fill(simState.vecStateYdot, simState.vecStateYdot + numDofs(), 0.0); + + return; + } + + double* const stateYbulk = simState.vecStateY + idxr.offsetC(); + + // Loop over column nodes + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + // Loop over components at node + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[point * idxr.strideColNode() + comp * idxr.strideColComp()] = static_cast(_initC[comp]); + } + + // Loop over particles + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + const unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ point }); + + // Loop over particle nodes + for (unsigned int shell = 0; shell < _disc.nParPoints[type]; ++shell) + { + const unsigned int shellOffset = offset + shell * idxr.strideParNode(type); + + // Initialize c_p + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + simState.vecStateY[shellOffset + comp] = static_cast(_initCp[comp + _disc.nComp * type]); + + // Initialize q + active const* const iq = _initQ.data() + _disc.nBoundBeforeType[type]; + for (unsigned int bnd = 0; bnd < _disc.strideBound[type]; ++bnd) + simState.vecStateY[shellOffset + idxr.strideParLiquid() + bnd] = static_cast(iq[bnd]); + } + } + } +} + +void GeneralRateModelDG::readInitialCondition(IParameterProvider& paramProvider) +{ + _initState.clear(); + _initStateDot.clear(); + + // Check if INIT_STATE is present + if (paramProvider.exists("INIT_STATE")) + { + const std::vector initState = paramProvider.getDoubleArray("INIT_STATE"); + _initState = std::vector(initState.begin(), initState.begin() + numPureDofs()); + + // Check if INIT_STATE contains the full state and its time derivative + if (initState.size() >= 2 * numPureDofs()) + _initStateDot = std::vector(initState.begin() + numPureDofs(), initState.begin() + 2 * numPureDofs()); + return; + } + + const std::vector initC = paramProvider.getDoubleArray("INIT_C"); + + if (initC.size() < _disc.nComp) + throw InvalidParameterException("INIT_C does not contain enough values for all components"); + + ad::copyToAd(initC.data(), _initC.data(), _disc.nComp); + + // Check if INIT_CP is present + if (paramProvider.exists("INIT_CP")) + { + const std::vector initCp = paramProvider.getDoubleArray("INIT_CP"); + + if (((initCp.size() < _disc.nComp) && _singleBinding) || ((initCp.size() < _disc.nComp * _disc.nParType) && !_singleBinding)) + throw InvalidParameterException("INIT_CP does not contain enough values for all components"); + + if (!_singleBinding) + ad::copyToAd(initCp.data(), _initCp.data(), _disc.nComp * _disc.nParType); + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initCp.data(), _initCp.data() + t * _disc.nComp, _disc.nComp); + } + } + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initC.data(), _initCp.data() + t * _disc.nComp, _disc.nComp); + } + + std::vector initQ; + if (paramProvider.exists("INIT_Q")) + initQ = paramProvider.getDoubleArray("INIT_Q"); + + if (initQ.empty() || (_disc.strideBound[_disc.nParType] == 0)) + return; + + if ((_disc.strideBound[_disc.nParType] > 0) && (((initQ.size() < _disc.strideBound[_disc.nParType]) && !_singleBinding) || ((initQ.size() < _disc.strideBound[0]) && _singleBinding))) + throw InvalidParameterException("INIT_Q does not contain enough values for all bound states"); + + if (!_singleBinding) + ad::copyToAd(initQ.data(), _initQ.data(), _disc.strideBound[_disc.nParType]); + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initQ.data(), _initQ.data() + _disc.nBoundBeforeType[t], _disc.strideBound[t]); + } +} + +/** + * @brief Computes consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function performs step 1. See consistentInitialTimeDerivative() for step 2. + * + * This function is to be used with consistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ +void GeneralRateModelDG::consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // initialization for inexact integration DG discretization of particle mass balance not supported. This would require the consideration of the additional algebraic constraints, + // but the general performance (stiffness due to the additional algebraic constraints) of this scheme does not justify the effort here. + for (unsigned int type = 0; type < _disc.nParType; type++) { + if (_disc.parExactInt[type] == false) { + LOG(Error) << "No consistent initialization for inexact integration DG discretization in particles (cf. par_exact_integration). If consistent initialization is required, change to exact integration."; + return; + } + } + + // Step 1: Solve algebraic equations + + // Step 1a: Compute quasi-stationary binding model state + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_binding[type]->hasQuasiStationaryReactions()) + continue; + + // Copy quasi-stationary binding mask to a local array that also includes the mobile phase + std::vector qsMask(_disc.nComp + _disc.strideBound[type], false); + int const* const qsMaskSrc = _binding[type]->reactionQuasiStationarity(); + std::copy_n(qsMaskSrc, _disc.strideBound[type], qsMask.data() + _disc.nComp); + + // Activate mobile phase components that have at least one active bound state + unsigned int bndStartIdx = 0; + unsigned int numActiveComp = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd) + { + if (qsMaskSrc[bndStartIdx + bnd]) + { + ++numActiveComp; + qsMask[comp] = true; + break; + } + } + + bndStartIdx += _disc.nBound[_disc.nComp * type + comp]; + } + + const linalg::ConstMaskArray mask{ qsMask.data(), static_cast(_disc.nComp + _disc.strideBound[type]) }; + const int probSize = linalg::numMaskActive(mask); + +//#ifdef CADET_PARALLELIZE +// BENCH_SCOPE(_timerConsistentInitPar); +// tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints), [&](std::size_t pblk) +//#else + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) +//#endif + { + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + // Reuse memory of sparse matrix for dense matrix + linalg::DenseMatrixView fullJacobianMatrix(_globalJacDisc.valuePtr() + _globalJacDisc.outerIndexPtr()[idxr.offsetCp(ParticleTypeIndex{ type }) + pblk], nullptr, mask.len, mask.len); + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(pblk / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[pblk % _disc.nNodes])) / _disc.colLength; + + // Get workspace memory + BufferedArray nonlinMemBuffer = tlmAlloc.array(_nonlinearSolver->workspaceSize(probSize)); + double* const nonlinMem = static_cast(nonlinMemBuffer); + + BufferedArray solutionBuffer = tlmAlloc.array(probSize); + double* const solution = static_cast(solutionBuffer); + + BufferedArray fullResidualBuffer = tlmAlloc.array(mask.len); + double* const fullResidual = static_cast(fullResidualBuffer); + + BufferedArray fullXBuffer = tlmAlloc.array(mask.len); + double* const fullX = static_cast(fullXBuffer); + + BufferedArray jacobianMemBuffer = tlmAlloc.array(probSize * probSize); + double* const jacobianMem = static_cast(jacobianMemBuffer); + + BufferedArray conservedQuantsBuffer = tlmAlloc.array(numActiveComp); + double* const conservedQuants = static_cast(conservedQuantsBuffer); + + linalg::DenseMatrixView jacobianMatrix(jacobianMem, _globalJacDisc.outerIndexPtr(), probSize, probSize); + const parts::cell::CellParameters cellResParams = makeCellResidualParams(type, mask.mask + _disc.nComp); + + // This loop cannot be run in parallel without creating a Jacobian matrix for each thread which would increase memory usage + const int localOffsetToParticle = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); + for (unsigned int node = 0; node < _disc.nParPoints[type]; ++node) + { + const int localOffsetInParticle = static_cast(node) * idxr.strideParNode(type); + + // Get pointer to q variables in a shell of particle pblk + double* const qShell = vecStateY + localOffsetToParticle + localOffsetInParticle + idxr.strideParLiquid(); + active* const localAdRes = adJac.adRes ? adJac.adRes + localOffsetToParticle + localOffsetInParticle : nullptr; + active* const localAdY = adJac.adY ? adJac.adY + localOffsetToParticle + localOffsetInParticle : nullptr; + + // r (particle) coordinate of current node + const double r = _disc.deltaR[type] * std::floor(node / _disc.nParNode[type]) + + 0.5 * _disc.deltaR[type] * (1 + _disc.parNodes[type][node % _disc.nParNode[type]]); + const ColumnPosition colPos{ z, 0.0, r }; + + // Determine whether nonlinear solver is required + if (!_binding[type]->preConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideParLiquid(), tlmAlloc)) + continue; + + // Extract initial values from current state + linalg::selectVectorSubset(qShell - _disc.nComp, mask, solution); + + // Save values of conserved moieties + const double epsQ = 1.0 - static_cast(_parPorosity[type]); + linalg::conservedMoietiesFromPartitionedMask(mask, _disc.nBound + type * _disc.nComp, _disc.nComp, qShell - _disc.nComp, conservedQuants, static_cast(_parPorosity[type]), epsQ); + + std::function jacFunc; + + // @todo AD for DG +// if (localAdY && localAdRes) +// { +// jacFunc = [&](double const* const x, linalg::detail::DenseMatrixBase& mat) +// { +// // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) +// // and initialize residuals with zero (also resetting directional values) +// ad::copyToAd(qShell - _disc.nComp, localAdY, mask.len); +// // @todo Check if this is necessary +// ad::resetAd(localAdRes, mask.len); +// +// // Prepare input vector by overwriting masked items +// linalg::applyVectorSubset(x, mask, localAdY); +// +// // Call residual function +// parts::cell::residualKernel( +// simTime.t, simTime.secIdx, colPos, localAdY, nullptr, localAdRes, fullJacobianMatrix.row(0), cellResParams, tlmAlloc +// ); +// +//#ifdef CADET_CHECK_ANALYTIC_JACOBIAN +// std::copy_n(qShell - _disc.nComp, mask.len, fullX); +// linalg::applyVectorSubset(x, mask, fullX); +// +// // Compute analytic Jacobian +// parts::cell::residualKernel( +// simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc +// ); +// +// // Compare +// const double diff = ad::compareDenseJacobianWithBandedAd( +// localAdRes - localOffsetInParticle, localOffsetInParticle, adJac.adDirOffset, _jacP[type * _disc.nPoints].lowerBandwidth(), +// _jacP[type * _disc.nPoints].lowerBandwidth(), _jacP[type * _disc.nPoints].upperBandwidth(), fullJacobianMatrix +// ); +// LOG(Debug) << "MaxDiff: " << diff; +//#endif +// +// // Extract Jacobian from AD +// ad::extractDenseJacobianFromBandedAd( +// localAdRes - localOffsetInParticle, localOffsetInParticle, adJac.adDirOffset, _jacP[type * _disc.nPoints].lowerBandwidth(), +// _jacP[type * _disc.nPoints].lowerBandwidth(), _jacP[type * _disc.nPoints].upperBandwidth(), fullJacobianMatrix +// ); +// +// // Extract Jacobian from full Jacobian +// mat.setAll(0.0); +// linalg::copyMatrixSubset(fullJacobianMatrix, mask, mask, mat); +// +// // Replace upper part with conservation relations +// mat.submatrixSetAll(0.0, 0, 0, numActiveComp, probSize); +// +// unsigned int bndIdx = 0; +// unsigned int rIdx = 0; +// unsigned int bIdx = 0; +// for (unsigned int comp = 0; comp < _disc.nComp; ++comp) +// { +// if (!mask.mask[comp]) +// { +// bndIdx += _disc.nBound[_disc.nComp * type + comp]; +// continue; +// } +// +// mat.native(rIdx, rIdx) = static_cast(_parPorosity[type]); +// +// for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) +// { +// if (mask.mask[bndIdx]) +// { +// mat.native(rIdx, bIdx + numActiveComp) = epsQ; +// ++bIdx; +// } +// } +// +// ++rIdx; +// } +// +// return true; +// }; +// } +// else +// { + jacFunc = [&](double const* const x, linalg::detail::DenseMatrixBase& mat) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract Jacobian from full Jacobian + mat.setAll(0.0); + linalg::copyMatrixSubset(fullJacobianMatrix, mask, mask, mat); + + // Replace upper part with conservation relations + mat.submatrixSetAll(0.0, 0, 0, numActiveComp, probSize); + + unsigned int bndIdx = 0; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[_disc.nComp * type + comp]; + continue; + } + + mat.native(rIdx, rIdx) = static_cast(_parPorosity[type]); + + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + mat.native(rIdx, bIdx + numActiveComp) = epsQ; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }; + //} // @todo AD + + // Apply nonlinear solver + _nonlinearSolver->solve( + [&](double const* const x, double* const r) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract values from residual + linalg::selectVectorSubset(fullResidual, mask, r); + + // Calculate residual of conserved moieties + std::fill_n(r, numActiveComp, 0.0); + unsigned int bndIdx = _disc.nComp; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[_disc.nComp * type + comp]; + continue; + } + + r[rIdx] = static_cast(_parPorosity[type]) * x[rIdx] - conservedQuants[rIdx]; + + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + r[rIdx] += epsQ * x[bIdx + numActiveComp]; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }, + jacFunc, errorTol, solution, nonlinMem, jacobianMatrix, probSize); + + // Apply solution + linalg::applyVectorSubset(solution, mask, qShell - idxr.strideParLiquid()); + + // Refine / correct solution + _binding[type]->postConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideParLiquid(), tlmAlloc); + } + + // reset jacobian pattern + setJacobianPattern_GRM(_globalJacDisc, _disc.curSection, _dynReactionBulk); + + } //CADET_PARFOR_END; + } + +} + +/** + * @brief Computes consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function performs step 2. See consistentInitialState() for step 1. + * + * This function is to be used with consistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] vecStateY Consistently initialized state vector + * @param [in,out] vecStateYdot On entry, residual without taking time derivatives into account. On exit, consistent state time derivatives. + */ +void GeneralRateModelDG::consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // Step 2: Compute the correct time derivative of the state vector + + // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system + + double* entries = _globalJacDisc.valuePtr(); + for (unsigned int entry = 0; entry < _globalJacDisc.nonZeros(); entry++) + entries[entry] = 0.0; + + Eigen::Map yDot(vecStateYdot, numDofs()); + + // Note that the residual has not been negated, yet. We will do that now. + for (unsigned int i = 0; i < numDofs(); ++i) + vecStateYdot[i] = -vecStateYdot[i]; + + // bulk column block: dc/dt = rhs = residual(with dc/dt=nullptr) + linalg::BandedEigenSparseRowIterator jacBlk(_globalJacDisc, idxr.offsetC()); + for (unsigned int blk = 0; blk < _disc.nPoints * _disc.nComp; blk++, ++jacBlk) + jacBlk[0] = 1.0; + + // Process the particle blocks +#ifdef CADET_PARALLELIZE + BENCH_START(_timerConsistentInitPar); + tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints * _disc.nParType), [&](std::size_t pblk) +#else + for (unsigned int pblk = 0; pblk < _disc.nPoints * _disc.nParType; ++pblk) +#endif + { + unsigned int type = pblk / _disc.nPoints; + unsigned int par = pblk % _disc.nPoints; + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(par / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[par % _disc.nNodes])) / _disc.colLength; + + // Assemble + linalg::BandedEigenSparseRowIterator jacPar(_globalJacDisc, idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par })); + + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + double* const dFluxDt = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }); + + for (unsigned int j = 0; j < _disc.nParPoints[type]; ++j) + { + addTimeDerivativeToJacobianParticleShell(jacPar, idxr, 1.0, type); // Iterator jacPar advances to next node + + if (!_binding[type]->hasQuasiStationaryReactions()) + continue; + + // Get iterators to beginning of solid phase + linalg::BandedEigenSparseRowIterator jacSolidOrig(_globalJac, idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }) + j * static_cast(idxr.strideParNode(type)) + static_cast(idxr.strideParLiquid())); + linalg::BandedEigenSparseRowIterator jacSolid = jacPar - idxr.strideParBound(type); + + int const* const mask = _binding[type]->reactionQuasiStationarity(); + double* const qShellDot = vecStateYdot + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }) + static_cast(j) * idxr.strideParNode(type) + idxr.strideParLiquid(); + + // Obtain derivative of fluxes wrt. time + std::fill_n(dFluxDt, _disc.strideBound[type], 0.0); + if (_binding[type]->dependsOnTime()) + { + // r (particle) coordinate of current node (particle radius normed to 1) - needed in externally dependent adsorption kinetic + const double r = (_disc.deltaR[type] * std::floor(j / _disc.nParNode[type]) + + 0.5 * _disc.deltaR[type] * (1 + _disc.parNodes[type][j % _disc.nParNode[type]])) + / (static_cast(_parRadius[type]) - static_cast(_parCoreRadius[type])); + _binding[type]->timeDerivativeQuasiStationaryFluxes(simTime.t, simTime.secIdx, + ColumnPosition{ z, 0.0, r }, + qShellDot - _disc.nComp, qShellDot, dFluxDt, tlmAlloc); + } + + // Copy row from original Jacobian (without time derivatives) and set right hand side + for (int i = 0; i < idxr.strideParBound(type); ++i, ++jacSolid, ++jacSolidOrig) + { + if (!mask[i]) + continue; + + jacSolid.copyRowFrom(jacSolidOrig); + qShellDot[i] = -dFluxDt[i]; + } + } + } CADET_PARFOR_END; + +#ifdef CADET_PARALLELIZE + BENCH_STOP(_timerConsistentInitPar); +#endif + + // todo ? Precondition + //double* const scaleFactors = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }); + //fbm.rowScaleFactors(scaleFactors); + //fbm.scaleRows(scaleFactors); + + // Factorize + _globalSolver.factorize(_globalJacDisc.block(idxr.offsetC(), idxr.offsetC(), numPureDofs(), numPureDofs())); + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Factorize() failed"; + } + + // Solve + yDot.segment(idxr.offsetC(), numPureDofs()) = _globalSolver.solve(yDot.segment(idxr.offsetC(), numPureDofs())); + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Solve() failed"; + } + +} + + + +/** + * @brief Computes approximately / partially consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialState(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 in the column + * bulk and flux blocks. The resulting equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for the bulk block and 0 for the flux block. + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function performs step 1. See leanConsistentInitialTimeDerivative() for step 2. + * + * This function is to be used with leanConsistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + */ +void GeneralRateModelDG::leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) +{ + // @todo + if (isSectionDependent(_parDiffusionMode) || isSectionDependent(_parSurfDiffusionMode)) + LOG(Warning) << "Lean consistent initialization is not appropriate for section-dependent pore and surface diffusion"; + + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // Step 1: Solve algebraic equations + + // Step 1a: Compute quasi-stationary binding model state + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (_binding[type]->hasQuasiStationaryReactions()) + { +#ifdef CADET_PARALLELIZE + BENCH_SCOPE(_timerConsistentInitPar); + tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints), [&](std::size_t pblk) +#else + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) +#endif + { + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(pblk / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[pblk % _disc.nNodes])) / _disc.colLength; + + const int localOffsetToParticle = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); + for (std::size_t shell = 0; shell < static_cast(_disc.nParPoints[type]); ++shell) + { + // Get pointer to q variables in a shell of particle pblk + const int localOffsetInParticle = static_cast(shell) * idxr.strideParNode(type) + idxr.strideParLiquid(); + double* const qShell = vecStateY + localOffsetToParticle + localOffsetInParticle; + // r (particle) coordinate of current node + const double r = _disc.deltaR[type] * std::floor(shell / _disc.nParNode[type]) + + 0.5 * _disc.deltaR[type] * (1 + _disc.parNodes[type][shell % _disc.nParNode[type]]); + const ColumnPosition colPos{ z, 0.0, r}; + + // Perform consistent initialization that does not require a full fledged nonlinear solver (that may fail or damage the current state vector) + if (!_binding[type]->preConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideParLiquid(), tlmAlloc)) + continue; + } + } CADET_PARFOR_END; + } + } + +} + +/** + * @brief Computes approximately / partially consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialTimeDerivative(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 in the column + * bulk and flux blocks. The resulting equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for the bulk block and 0 for the flux block. + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function performs step 2. See leanConsistentInitialState() for step 1. + * + * This function is to be used with leanConsistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] t Current time point + * @param [in] vecStateY (Lean) consistently initialized state vector + * @param [in,out] vecStateYdot On entry, inconsistent state time derivatives. On exit, partially consistent state time derivatives. + * @param [in] res On entry, residual without taking time derivatives into account. The data is overwritten during execution of the function. + */ +void GeneralRateModelDG::leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem) +{ + // @TODO? + //if (isSectionDependent(_parDiffusionMode) || isSectionDependent(_parSurfDiffusionMode)) + // LOG(Warning) << "Lean consistent initialization is not appropriate for section-dependent pore and surface diffusion"; + + //BENCH_SCOPE(_timerConsistentInit); + + //Indexer idxr(_disc); + + //// Step 2: Compute the correct time derivative of the state vector + + //// Step 2a: Assemble, factorize, and solve column bulk block of linear system + + //// Note that the residual is not negated as required at this point. We will fix that later. + + //double* const resSlice = res + idxr.offsetC(); + + //// Handle bulk block + //_convDispOp.solveTimeDerivativeSystem(SimulationTime{ t, 0u }, resSlice); + + //// Note that we have solved with the *positive* residual as right hand side + //// instead of the *negative* one. Fortunately, we are dealing with linear systems, + //// which means that we can just negate the solution. + //double* const yDotSlice = vecStateYdot + idxr.offsetC(); + //for (unsigned int i = 0; i < _disc.nPoints * _disc.nComp; ++i) + // yDotSlice[i] = -resSlice[i]; + + //// Step 2b: Solve for fluxes j_f by backward substitution + + //// Reset \dot{j}_f to 0.0 + //double* const jfDot = vecStateYdot + idxr.offsetJf(); + //std::fill(jfDot, jfDot + _disc.nComp * _disc.nPoints * _disc.nParType, 0.0); + + //solveForFluxes(vecStateYdot, idxr); +} + +void GeneralRateModelDG::initializeSensitivityStates(const std::vector& vecSensY) const +{ + Indexer idxr(_disc); + for (std::size_t param = 0; param < vecSensY.size(); ++param) + { + double* const stateYbulk = vecSensY[param] + idxr.offsetC(); + + // Loop over column cells + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + // Loop over components in cell + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[point * idxr.strideColNode() + comp * idxr.strideColComp()] = _initC[comp].getADValue(param); + } + + // Loop over particles + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + const unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ point }); + + // Loop over particle cells + for (unsigned int shell = 0; shell < _disc.nParPoints[type]; ++shell) + { + const unsigned int shellOffset = offset + shell * idxr.strideParNode(type); + double* const stateYparticle = vecSensY[param] + shellOffset; + double* const stateYparticleSolid = stateYparticle + idxr.strideParLiquid(); + + // Initialize c_p + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + stateYparticle[comp] = _initCp[comp + type * _disc.nComp].getADValue(param); + + // Initialize q + for (unsigned int bnd = 0; bnd < _disc.strideBound[type]; ++bnd) + stateYparticleSolid[bnd] = _initQ[bnd + _disc.nBoundBeforeType[type]].getADValue(param); + } + } + } + } +} + +/** + * @brief Computes consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of consistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$. Let @f$ \mathcal{I}_a @f$ be the index set of algebraic equations, then, at this point, we have + * \f[ \left( \frac{\partial F}{\partial y}(t, y_0, \dot{y}_0) s + \frac{\partial F}{\partial p}(t, y_0, \dot{y}_0) \right)_{\mathcal{I}_a} = 0. \f]
  2. + *
  3. Compute the time derivatives of the sensitivity @f$ \dot{s} @f$ such that the differential equations hold. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{s}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the sensitivity vector @f$ s @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * Let @f$ \mathcal{I}_d @f$ denote the index set of differential equations. + * The right hand side of the linear system is given by @f[ -\frac{\partial F}{\partial y}(t, y, \dot{y}) s - \frac{\partial F}{\partial p}(t, y, \dot{y}), @f] + * which is 0 for algebraic equations (@f$ -\frac{\partial^2 F}{\partial t \partial p}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ +void GeneralRateModelDG::consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) +{ + // @TODO? +// BENCH_SCOPE(_timerConsistentInit); +// +// Indexer idxr(_disc); +// +// for (std::size_t param = 0; param < vecSensY.size(); ++param) +// { +// double* const sensY = vecSensY[param]; +// double* const sensYdot = vecSensYdot[param]; +// +// // Copy parameter derivative dF / dp from AD and negate it +// for (unsigned int i = _disc.nComp; i < numDofs(); ++i) +// sensYdot[i] = -adRes[i].getADValue(param); +// +// // Step 1: Solve algebraic equations +// +// // Step 1a: Compute quasi-stationary binding model state +// for (unsigned int type = 0; type < _disc.nParType; ++type) +// { +// if (!_binding[type]->hasQuasiStationaryReactions()) +// continue; +// +// int const* const qsMask = _binding[type]->reactionQuasiStationarity(); +// const linalg::ConstMaskArray mask{ qsMask, static_cast(_disc.strideBound[type]) }; +// const int probSize = linalg::numMaskActive(mask); +// +//#ifdef CADET_PARALLELIZE +// BENCH_SCOPE(_timerConsistentInitPar); +// tbb::parallel_for(std::size_t(0), static_cast(_disc.nCol), [&](std::size_t pblk) +//#else +// for (unsigned int pblk = 0; pblk < _disc.nCol; ++pblk) +//#endif +// { +// // Reuse memory of band matrix for dense matrix +// linalg::DenseMatrixView jacobianMatrix(_jacPdisc[type * _disc.nCol + pblk].data(), _jacPdisc[type * _disc.nCol + pblk].pivot(), probSize, probSize); +// +// // Get workspace memory +// LinearBufferAllocator tlmAlloc = threadLocalMem.get(); +// +// BufferedArray rhsBuffer = tlmAlloc.array(probSize); +// double* const rhs = static_cast(rhsBuffer); +// +// BufferedArray rhsUnmaskedBuffer = tlmAlloc.array(idxr.strideParBound(type)); +// double* const rhsUnmasked = static_cast(rhsUnmaskedBuffer); +// +// double* const maskedMultiplier = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); +// double* const scaleFactors = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); +// +// for (unsigned int shell = 0; shell < _disc.nParCell[type]; ++shell) +// { +// const int jacRowOffset = static_cast(shell) * idxr.strideParShell(type) + _disc.nComp; +// const int localQOffset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }) + static_cast(shell) * idxr.strideParShell(type) + idxr.strideParLiquid(); +// +// // Extract subproblem Jacobian from full Jacobian +// jacobianMatrix.setAll(0.0); +// linalg::copyMatrixSubset(_jacP[type * _disc.nCol + pblk], mask, mask, jacRowOffset, 0, jacobianMatrix); +// +// // Construct right hand side +// linalg::selectVectorSubset(sensYdot + localQOffset, mask, rhs); +// +// // Zero out masked elements +// std::copy_n(sensY + localQOffset - idxr.strideParLiquid(), idxr.strideParShell(type), maskedMultiplier); +// linalg::fillVectorSubset(maskedMultiplier + _disc.nComp, mask, 0.0); +// +// // Assemble right hand side +// _jacP[type * _disc.nCol + pblk].submatrixMultiplyVector(maskedMultiplier, jacRowOffset, -static_cast(_disc.nComp), _disc.strideBound[type], idxr.strideParShell(type), rhsUnmasked); +// linalg::vectorSubsetAdd(rhsUnmasked, mask, -1.0, 1.0, rhs); +// +// // Precondition +// jacobianMatrix.rowScaleFactors(scaleFactors); +// jacobianMatrix.scaleRows(scaleFactors); +// +// // Solve +// jacobianMatrix.factorize(); +// jacobianMatrix.solve(scaleFactors, rhs); +// +// // Write back +// linalg::applyVectorSubset(rhs, mask, sensY + localQOffset); +// } +// } CADET_PARFOR_END; +// } +// +// // Step 1b: Compute fluxes j_f, right hand side is -dF / dp +// std::copy(sensYdot + idxr.offsetJf(), sensYdot + numDofs(), sensY + idxr.offsetJf()); +// +// solveForFluxes(sensY, idxr); +// +// // Step 2: Compute the correct time derivative of the state vector +// +// // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system +// +// // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in sensYdot +// multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, sensYdot); +// +// // Note that we have correctly negated the right hand side +// +// // Handle bulk block +// _convDispOp.solveTimeDerivativeSystem(simTime, sensYdot + idxr.offsetC()); +// +// // Process the particle blocks +//#ifdef CADET_PARALLELIZE +// BENCH_START(_timerConsistentInitPar); +// tbb::parallel_for(std::size_t(0), static_cast(_disc.nCol * _disc.nParType), [&](std::size_t pblk) +//#else +// for (unsigned int pblk = 0; pblk < _disc.nCol * _disc.nParType; ++pblk) +//#endif +// { +// const unsigned int type = pblk / _disc.nCol; +// const unsigned int par = pblk % _disc.nCol; +// +// // Assemble +// linalg::FactorizableBandMatrix& fbm = _jacPdisc[pblk]; +// fbm.setAll(0.0); +// +// linalg::FactorizableBandMatrix::RowIterator jac = fbm.row(0); +// for (unsigned int j = 0; j < _disc.nParCell[type]; ++j) +// { +// // Populate matrix with time derivative Jacobian first +// addTimeDerivativeToJacobianParticleShell(jac, idxr, 1.0, type); +// // Iterator jac has already been advanced to next shell +// +// // Overwrite rows corresponding to algebraic equations with the Jacobian and set right hand side to 0 +// if (_binding[type]->hasQuasiStationaryReactions()) +// { +// // Get iterators to beginning of solid phase +// linalg::BandMatrix::RowIterator jacSolidOrig = _jacP[pblk].row(j * static_cast(idxr.strideParShell(type)) + static_cast(idxr.strideParLiquid())); +// linalg::FactorizableBandMatrix::RowIterator jacSolid = jac - idxr.strideParBound(type); +// +// int const* const mask = _binding[type]->reactionQuasiStationarity(); +// double* const qShellDot = sensYdot + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }) + static_cast(j) * idxr.strideParShell(type) + idxr.strideParLiquid(); +// +// // Copy row from original Jacobian and set right hand side +// for (int i = 0; i < idxr.strideParBound(type); ++i, ++jacSolid, ++jacSolidOrig) +// { +// if (!mask[i]) +// continue; +// +// jacSolid.copyRowFrom(jacSolidOrig); +// +// // Right hand side is -\frac{\partial^2 res(t, y, \dot{y})}{\partial p \partial t} +// // If the residual is not explicitly depending on time, this expression is 0 +// // @todo This is wrong if external functions are used. Take that into account! +// qShellDot[i] = 0.0; +// } +// } +// } +// +// // Precondition +// double* const scaleFactors = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par }); +// fbm.rowScaleFactors(scaleFactors); +// fbm.scaleRows(scaleFactors); +// +// // Factorize +// const bool result = fbm.factorize(); +// if (!result) +// { +// LOG(Error) << "Factorize() failed for par block " << pblk << " (type " << type << " col " << par << ")"; +// } +// +// // Solve +// const bool result2 = fbm.solve(scaleFactors, sensYdot + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ par })); +// if (!result2) +// { +// LOG(Error) << "Solve() failed for par block " << pblk << " (type " << type << " col " << par << ")"; +// } +// } CADET_PARFOR_END; +// +//#ifdef CADET_PARALLELIZE +// BENCH_STOP(_timerConsistentInitPar); +//#endif +// +// // TODO: Right hand side for fluxes should be -d^2res/(dp dy) * \dot{y} +// // If parameters depend on time, then it should be +// // -d^2res/(dp dy) * \dot{y} - d^2res/(dt dy) * s - d^2res/(dp dt) +// +// // Step 2b: Solve for fluxes j_f by backward substitution +// solveForFluxes(sensYdot, idxr); +// } +} + +/** + * @brief Computes approximately / partially consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of leanConsistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the sensitivity @f$ \dot{s} @f$ such that the differential equations hold. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{s}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the sensitivity vector @f$ s @f$ is fixed). The resulting + * equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * Let @f$ \mathcal{I}_d @f$ denote the index set of differential equations. + * The right hand side of the linear system is given by @f[ -\frac{\partial F}{\partial y}(t, y, \dot{y}) s - \frac{\partial F}{\partial p}(t, y, \dot{y}), @f] + * which is 0 for algebraic equations (@f$ -\frac{\partial^2 F}{\partial t \partial p}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ +void GeneralRateModelDG::leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) +{ + // @TODO? + + //if (isSectionDependent(_parDiffusionMode) || isSectionDependent(_parSurfDiffusionMode)) + // LOG(Warning) << "Lean consistent initialization is not appropriate for section-dependent pore and surface diffusion"; + + //BENCH_SCOPE(_timerConsistentInit); + + //Indexer idxr(_disc); + + //for (std::size_t param = 0; param < vecSensY.size(); ++param) + //{ + // double* const sensY = vecSensY[param]; + // double* const sensYdot = vecSensYdot[param]; + + // // Copy parameter derivative from AD to tempState and negate it + // // We need to use _tempState in order to keep sensYdot unchanged at this point + // for (int i = 0; i < idxr.offsetCp(); ++i) + // _tempState[i] = -adRes[i].getADValue(param); + + // std::fill(_tempState + idxr.offsetCp(), _tempState + idxr.offsetJf(), 0.0); + + // for (unsigned int i = idxr.offsetJf(); i < numDofs(); ++i) + // _tempState[i] = -adRes[i].getADValue(param); + + // // Step 1: Compute fluxes j_f, right hand side is -dF / dp + // std::copy(_tempState + idxr.offsetJf(), _tempState + numDofs(), sensY + idxr.offsetJf()); + + // solveForFluxes(sensY, idxr); + + // // Step 2: Compute the correct time derivative of the state vector + + // // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system + + // // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in _tempState + // multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, _tempState); + + // // Copy relevant parts to sensYdot for use as right hand sides + // std::copy(_tempState + idxr.offsetC(), _tempState + idxr.offsetCp(), sensYdot + idxr.offsetC()); + // std::copy(_tempState + idxr.offsetJf(), _tempState + numDofs(), sensYdot); + + // // Handle bulk block + // _convDispOp.solveTimeDerivativeSystem(simTime, sensYdot + idxr.offsetC()); + + // // Step 2b: Solve for fluxes j_f by backward substitution + // solveForFluxes(sensYdot, idxr); + //} +} + +} // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/GeneralRateModelDG-LinearSolver.cpp b/src/libcadet/model/GeneralRateModelDG-LinearSolver.cpp new file mode 100644 index 000000000..2d6d1f870 --- /dev/null +++ b/src/libcadet/model/GeneralRateModelDG-LinearSolver.cpp @@ -0,0 +1,257 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2022: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/GeneralRateModelDG.hpp" +#include "model/BindingModel.hpp" +#include "model/parts/BindingCellKernel.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "AdUtils.hpp" + +#include +#include + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include "ParallelSupport.hpp" + +#ifdef CADET_PARALLELIZE + #include + #include + + typedef tbb::flow::continue_node< tbb::flow::continue_msg > node_t; + typedef const tbb::flow::continue_msg & msg_t; +#endif + +namespace cadet +{ + +namespace model +{ + +/** + * @brief Computes the solution of the linear system involving the system Jacobian + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The right hand side \f$ b \f$ is given by @p rhs, the Jacobians are evaluated at the + * point \f$(y, \dot{y})\f$ given by @p y and @p yDot. The residual @p res at this point, \f$ F(t, y, \dot{y}) \f$, + * may help with this. Error weights (see IDAS guide) are given in @p weight. The solution is returned in @p rhs. + * + * The full Jacobian @f$ J = \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) @f$ is given by + * @f[ \begin{align} + J = + \left[\begin{array}{c|ccc|c} + J_0 & & & & J_{0,f} \\ + \hline + & J_1 & & & J_{1,f} \\ + & & \ddots & & \vdots \\ + & & & J_{N_z} & J_{N_z,f} \\ + \hline + J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & J_f + \end{array}\right]. + \end{align} @f] + * By decomposing the Jacobian @f$ J @f$ into @f$ J = LU @f$, we get + * @f[ \begin{align} + L &= \left[\begin{array}{c|ccc|c} + J_0 & & & & \\ + \hline + & J_1 & & & \\ + & & \ddots & & \\ + & & & J_{N_z} & \\ + \hline + J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + \end{array}\right], \\ + U &= \left[\begin{array}{c|ccc|c} + I & & & & J_0^{-1} \, J_{0,f} \\ + \hline + & I & & & J_1^{-1} \, J_{1,f} \\ + & & \ddots & & \vdots \\ + & & & I & J_{N_z}^{-1} \, J_{N_z,f} \\ + \hline + & & & & S + \end{array}\right]. + \end{align} @f] + * Here, the Schur-complement @f$ S @f$ is given by + * @f[ \begin{align} + S = J_f - J_{f,0} \, J_0^{-1} \, J_{0,f} - \sum_{p=1}^{N_z}{J_{f,p} \, J_p^{-1} \, J_{p,f}}. + \end{align} @f] + * Note that @f$ J_f = I @f$ is the identity matrix and that the off-diagonal blocks @f$ J_{i,f} @f$ + * and @f$ J_{f,i} @f$ for @f$ i = 0, \dots, N_{z} @f$ are sparse. + * + * Exploiting the decomposition, the solution procedure @f$ x = J^{-1}b = \left( LU \right)^{-1}b = U^{-1} L^{-1} b @f$ + * works as follows: + * -# Factorize the diagonal blocks @f$ J_0, \dots, J_{N_z} @f$ + * -# Solve @f$ y = L^{-1} b @f$ by forward substitution. This is accomplished by first solving the diagonal + * blocks independently, that is, + * @f[ y_i = J_{i}^{-1} b_i. @f] + * Then, calculate the flux-part @f$ y_f @f$ by substituting in the already calculated solutions @f$ y_i @f$: + * @f[ y_f = b_f - \sum_{i=0}^{N_z} J_{f,i} y_i. @f] + * -# Solve the Schur-complement @f$ S x_f = y_f @f$ using an iterative method that only requires + * matrix-vector products. The already inverted diagonal blocks @f$ J_i^{-1} @f$ come in handy here. + * -# Solve the rest of the @f$ U x = y @f$ system by backward substitution. To be more precise, compute + * @f[ x_i = y_i - J_i^{-1} J_{i,f} y_f. @f] + * + * + * @param [in] t Current time point + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] outerTol Error tolerance for the solution of the linear system from outer Newton iteration + * @param [in,out] rhs On entry the right hand side of the linear equation system, on exit the solution + * @param [in] weight Vector with error weights + * @param [in] simState State of the simulation (state vector and its time derivatives) at which the Jacobian is evaluated + * @return @c 0 on success, @c -1 on non-recoverable error, and @c +1 on recoverable error + */ +int GeneralRateModelDG::linearSolve(double t, double alpha, double outerTol, double* const rhs, double const* const weight, + const ConstSimulationState& simState) +{ + BENCH_SCOPE(_timerLinearSolve); + + Indexer idxr(_disc); + + Eigen::Map r(rhs, numDofs()); // map rhs to Eigen object + + // ==== Step 1: Factorize diagonal Jacobian blocks + + // Factorize partial Jacobians only if required + + if (_factorizeJacobian) + { + + // Assemble and factorize discretized bulk Jacobian + assembleDiscretizedGlobalJacobian(alpha, idxr); + + _globalSolver.factorize(_globalJacDisc.block(idxr.offsetC(), idxr.offsetC(), numPureDofs(), numPureDofs())); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Factorize() failed"; + } + + // Do not factorize again at next call without changed Jacobians + _factorizeJacobian = false; + } + + // ==== Step 1.5: Solve J c_uo = b_uo - A * c_in = b_uo - A*b_in + + // Handle inlet DOFs: + // Inlet at z = 0 for forward flow, at z = L for backward flow. + unsigned int offInlet = (_disc.velocity >= 0.0) ? 0 : (_disc.nCol - 1u) * idxr.strideColCell(); + + for (int comp = 0; comp < _disc.nComp; comp++) { + for (int node = 0; node < (_disc.exactInt ? _disc.nNodes : 1); node++) { + r[idxr.offsetC() + offInlet + comp * idxr.strideColComp() + node * idxr.strideColNode()] += _jacInlet(node, 0) * r[comp]; + } + } + + // ==== Step 2: Solve system of pure DOFs + // The result is stored in rhs (in-place solution) + + r.segment(idxr.offsetC(), numPureDofs()) = _globalSolver.solve(r.segment(idxr.offsetC(), numPureDofs())); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Solve() failed"; + } + + // The full solution is now stored in rhs + return 0; +} + +/** + * @brief Assembles bulk Jacobian @f$ J_i @f$ (@f$ i > 0 @f$) of the time-discretized equations + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The system Jacobian of the original equations, + * \f[ \frac{\partial F}{\partial y}, \f] + * is already computed (by AD or manually in residualImpl() with @c wantJac = true). This function is responsible + * for adding + * \f[ \alpha \frac{\partial F}{\partial \dot{y}} \f] + * to the system Jacobian, which yields the Jacobian of the time-discretized equations + * \f[ F\left(t, y_0, \sum_{k=0}^N \alpha_k y_k \right) = 0 \f] + * when a BDF method is used. The time integrator needs to solve this equation for @f$ y_0 @f$, which requires + * the solution of the linear system mentioned above (@f$ \alpha_0 = \alpha @f$ given in @p alpha). + * + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + */ +void GeneralRateModelDG::assembleDiscretizedGlobalJacobian(double alpha, Indexer idxr) { + + /* add static (per section) jacobian without inlet */ + _globalJacDisc = _globalJac; + + // Add time derivatives to particle shells + for (unsigned int parType = 0; parType < _disc.nParType; parType++) { + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + + linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode })); + + // If special case for inexact integration DG scheme: + // Do not add time derivative to particle mass balance equation at inner particle boundary for mass balance(s) + if (!_disc.parExactInt[parType] && _parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab && _parCoreRadius[parType] == 0.0) { + // we still need to add the derivative for the binding + // move iterator to solid phase + jac += idxr.strideParLiquid(); + // Solid phase + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) { + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; ++bnd, ++jac) + { + // Add derivative with respect to dynamic states to Jacobian + if (_binding[parType]->reactionQuasiStationarity()[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) + continue; + + // surface diffusion + kinetic binding -> additional DG-discretized mass balance equation for solid, for which the (inexact integration) discretization special case also holds + else if (_hasSurfaceDiffusion[parType] + && static_cast(getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], _disc.curSection)[_disc.nBoundBeforeType[parType] + getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) + continue; + + // Add derivative with respect to dq / dt to Jacobian + jac[0] += alpha; + } + } + } + else { // else, treat boundary node "normally" + addTimeDerivativeToJacobianParticleShell(jac, idxr, alpha, parType); + } + + // compute time derivative of remaining points + // Iterator jac has already been advanced to next shell + for (unsigned int j = 1; j < _disc.nParPoints[parType]; ++j) + { + addTimeDerivativeToJacobianParticleShell(jac, idxr, alpha, parType); + // Iterator jac has already been advanced to next shell + } + } + } + + // add the remaining bulk time derivatives to global jacobian. + linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, idxr.offsetC()); + for (; jac.row() < idxr.offsetCp(); ++jac) { + jac[0] += alpha; // main diagonal + } +} + +/** + * @brief Adds Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ to bead rows of system Jacobian + * @details Actually adds @f$ \alpha \frac{\partial F}{\partial \dot{y}} @f$, which is useful + * for constructing the linear system in BDF time discretization. + * @param [in,out] jac On entry, RowIterator of the particle block pointing to the beginning of a bead shell; + * on exit, the iterator points to the end of the bead shell + * @param [in] idxr Indexer + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] parType Index of the particle type + */ +void GeneralRateModelDG::addTimeDerivativeToJacobianParticleShell(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, unsigned int parType) +{ + parts::cell::addTimeDerivativeToJacobianParticleShell(jac, alpha, static_cast(_parPorosity[parType]), _disc.nComp, _disc.nBound + _disc.nComp * parType, + _poreAccessFactor.data() + _disc.nComp * parType, _disc.strideBound[parType], _disc.boundOffset + _disc.nComp * parType, _binding[parType]->reactionQuasiStationarity()); +} + +} // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/GeneralRateModelDG.cpp b/src/libcadet/model/GeneralRateModelDG.cpp new file mode 100644 index 000000000..36a37722f --- /dev/null +++ b/src/libcadet/model/GeneralRateModelDG.cpp @@ -0,0 +1,2366 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/GeneralRateModelDG.hpp" +#include "BindingModelFactory.hpp" +#include "ReactionModelFactory.hpp" +#include "ParamReaderHelper.hpp" +#include "ParamReaderScopes.hpp" +#include "cadet/Exceptions.hpp" +#include "cadet/ExternalFunction.hpp" +#include "cadet/SolutionRecorder.hpp" +#include "ConfigurationHelper.hpp" +#include "model/BindingModel.hpp" +#include "model/ReactionModel.hpp" +#include "model/ParameterDependence.hpp" +#include "model/parts/BindingCellKernel.hpp" +#include "SimulationTypes.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/Norms.hpp" +#include "linalg/Subset.hpp" + +#include "Stencil.hpp" +#include "Weno.hpp" +#include "AdUtils.hpp" +#include "SensParamUtil.hpp" + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include +#include +#include +#include + +#include "ParallelSupport.hpp" +#ifdef CADET_PARALLELIZE + #include +#endif + +namespace cadet +{ + +namespace model +{ + +constexpr double SurfVolRatioSphere = 3.0; +constexpr double SurfVolRatioCylinder = 2.0; +constexpr double SurfVolRatioSlab = 1.0; + + +GeneralRateModelDG::GeneralRateModelDG(UnitOpIdx unitOpIdx) : UnitOperationBase(unitOpIdx), + _hasSurfaceDiffusion(0, false), _dynReactionBulk(nullptr), + _globalJac(), _globalJacDisc(), _jacInlet(), _hasParDepSurfDiffusion(false), + _analyticJac(true), _jacobianAdDirs(0), _factorizeJacobian(false), _tempState(nullptr), + _initC(0), _initCp(0), _initQ(0), _initState(0), _initStateDot(0) +{ +} + +GeneralRateModelDG::~GeneralRateModelDG() CADET_NOEXCEPT +{ + delete[] _tempState; + + //_globalJac.~SparseMatrix(); // TODO: Eigen deconstructor doesnt work somehow + //_globalJacDisc.~SparseMatrix(); + + delete _dynReactionBulk; + + clearParDepSurfDiffusion(); + + delete[] _disc.nParCell; + delete[] _disc.parTypeOffset; + delete[] _disc.nParPointsBeforeType; + delete[] _disc.nBound; + delete[] _disc.boundOffset; + delete[] _disc.strideBound; + delete[] _disc.nBoundBeforeType; +} + +unsigned int GeneralRateModelDG::numDofs() const CADET_NOEXCEPT +{ + // Column bulk DOFs: nPoints * nComp + // Particle DOFs: nPoints * nParType particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs + // in each shell; there are nParCell shells for each particle type + // Inlet DOFs: nComp + return _disc.nPoints * _disc.nComp + _disc.parTypeOffset[_disc.nParType] + _disc.nComp; +} + +unsigned int GeneralRateModelDG::numPureDofs() const CADET_NOEXCEPT +{ + // Column bulk DOFs: nPoints * nComp + // Particle DOFs: nPoints particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs + // in each shell; there are nPar shells + return _disc.nPoints * _disc.nComp + _disc.parTypeOffset[_disc.nParType]; +} + + +bool GeneralRateModelDG::usesAD() const CADET_NOEXCEPT +{ +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + // We always need AD if we want to check the analytical Jacobian + return true; +#else + // We only need AD if we are not computing the Jacobian analytically + return !_analyticJac; +#endif +} + +void GeneralRateModelDG::clearParDepSurfDiffusion() +{ + if (_singleParDepSurfDiffusion) + { + if (!_parDepSurfDiffusion.empty()) + delete _parDepSurfDiffusion[0]; + } + else + { + for (IParameterDependence* pd : _parDepSurfDiffusion) + delete pd; + } + + _parDepSurfDiffusion.clear(); +} + +bool GeneralRateModelDG::configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper) +{ + // ==== Read discretization + _disc.nComp = paramProvider.getInt("NCOMP"); + + paramProvider.pushScope("discretization"); + + _disc.nCol = paramProvider.getInt("NCOL"); + if (_disc.nCol < 1) + throw InvalidParameterException("Number of column cells must be at least 1!"); + + if (paramProvider.getInt("POLYDEG") < 1) + throw InvalidParameterException("Polynomial degree must be at least 1!"); + else + _disc.polyDeg = paramProvider.getInt("POLYDEG"); + + if (_disc.polyDeg < 3) + LOG(Warning) << "Polynomial degree > 2 in bulk discretization (cf. POLYDEG) is always recommended for performance reasons."; + + _disc.exactInt = paramProvider.getBool("EXACT_INTEGRATION"); + + const std::vector nParCell = paramProvider.getIntArray("NPARCELL"); + const std::vector parPolyDeg = paramProvider.getIntArray("PARPOLYDEG"); + const std::vector parExactInt = paramProvider.getBoolArray("PAR_EXACT_INTEGRATION"); + + const std::vector nBound = paramProvider.getIntArray("NBOUND"); + if (nBound.size() < _disc.nComp) + throw InvalidParameterException("Field NBOUND contains too few elements (NCOMP = " + std::to_string(_disc.nComp) + " required)"); + + if (paramProvider.exists("NPARTYPE")) + _disc.nParType = paramProvider.getInt("NPARTYPE"); + else + { + // Infer number of particle types + _disc.nParType = std::max({ nBound.size() / _disc.nComp, nParCell.size(), parPolyDeg.size(), parExactInt.size() }); + } + + if ((parExactInt.size() > 1) && (parExactInt.size() < _disc.nParType)) + throw InvalidParameterException("Field PAR_EXACT_INTEGRATION must have 1 or NPARTYPE (" + std::to_string(_disc.nParType) + ") entries"); + if ((nParCell.size() > 1) && (nParCell.size() < _disc.nParType)) + throw InvalidParameterException("Field NPARCELL must have 1 or NPARTYPE (" + std::to_string(_disc.nParType) + ") entries"); + if ((parPolyDeg.size() > 1) && (parPolyDeg.size() < _disc.nParType)) + throw InvalidParameterException("Field PARPOLYDEG must have 1 or NPARTYPE (" + std::to_string(_disc.nParType) + ") entries"); + + _disc.nParCell = new unsigned int[_disc.nParType]; + if (nParCell.size() < _disc.nParType) + { + // Multiplex number of particle cells to all particle types + for (unsigned int i = 0; i < _disc.nParType; ++i) + std::fill(_disc.nParCell, _disc.nParCell + _disc.nParType, nParCell[0]); + } + else + std::copy_n(nParCell.begin(), _disc.nParType, _disc.nParCell); + + _disc.parPolyDeg = new unsigned int[_disc.nParType]; + if (parPolyDeg.size() < _disc.nParType) + { + if (parPolyDeg[0] < 1) { + throw InvalidParameterException("Particle polynomial degree must be at least 1!"); + } + else { + // Multiplex polynomial degree of particle elements to all particle types + for (unsigned int i = 0; i < _disc.nParType; ++i) + std::fill(_disc.parPolyDeg, _disc.parPolyDeg + _disc.nParType, parPolyDeg[0]); + + } + } + else { + for (unsigned int parType = 0; parType < _disc.nParType; parType++) + { + if (parPolyDeg[parType] < 1) + throw InvalidParameterException("Particle polynomial degree(s) must be at least 1!"); + if (_disc.nParCell[parType] < 1) + throw InvalidParameterException("Number of particle cell(s) must be at least 1!"); + + _disc.parPolyDeg[parType] = parPolyDeg[parType]; + } + } + + _disc.parExactInt = new bool[_disc.nParType]; + if (parExactInt.size() < _disc.nParType) + { + // Multiplex exact/inexact integration of particle elements to all particle types + for (unsigned int i = 0; i < _disc.nParType; ++i) { + std::fill(_disc.parExactInt, _disc.parExactInt + _disc.nParType, parExactInt[0]); + if (!_disc.parExactInt[i]) + LOG(Warning) << "Inexact integration method (cf. PAR_EXACT_INTEGRATION) in particles might add severe! stiffness to the system and disables consistent initialization!"; + } + } + else + std::copy_n(parExactInt.begin(), _disc.nParType, _disc.parExactInt); + + if ((nBound.size() > _disc.nComp) && (nBound.size() < _disc.nComp * _disc.nParType)) + throw InvalidParameterException("Field NBOUND must have NCOMP (" + std::to_string(_disc.nComp) + ") or NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ") entries"); + + // Compute discretization operators and initialize containers + _disc.initializeDG(); + + _disc.nBound = new unsigned int[_disc.nComp * _disc.nParType]; + if (nBound.size() < _disc.nComp * _disc.nParType) + { + // Multiplex number of bound states to all particle types + for (unsigned int i = 0; i < _disc.nParType; ++i) + std::copy_n(nBound.begin(), _disc.nComp, _disc.nBound + i * _disc.nComp); + } + else + std::copy_n(nBound.begin(), _disc.nComp * _disc.nParType, _disc.nBound); + + const unsigned int nTotalBound = std::accumulate(_disc.nBound, _disc.nBound + _disc.nComp * _disc.nParType, 0u); + + // Precompute offsets and total number of bound states (DOFs in solid phase) + _disc.boundOffset = new unsigned int[_disc.nComp * _disc.nParType]; + _disc.strideBound = new unsigned int[_disc.nParType + 1]; + _disc.nBoundBeforeType = new unsigned int[_disc.nParType]; + _disc.strideBound[_disc.nParType] = nTotalBound; + _disc.nBoundBeforeType[0] = 0; + for (unsigned int j = 0; j < _disc.nParType; ++j) + { + unsigned int* const ptrOffset = _disc.boundOffset + j * _disc.nComp; + unsigned int* const ptrBound = _disc.nBound + j * _disc.nComp; + + ptrOffset[0] = 0; + for (unsigned int i = 1; i < _disc.nComp; ++i) + { + ptrOffset[i] = ptrOffset[i - 1] + ptrBound[i - 1]; + } + _disc.strideBound[j] = ptrOffset[_disc.nComp - 1] + ptrBound[_disc.nComp - 1]; + + if (j != _disc.nParType - 1) + _disc.nBoundBeforeType[j + 1] = _disc.nBoundBeforeType[j] + _disc.strideBound[j]; + } + + // Precompute offsets of particle type DOFs + _disc.parTypeOffset = new unsigned int[_disc.nParType + 1]; + _disc.nParPointsBeforeType = new unsigned int[_disc.nParType + 1]; + _disc.parTypeOffset[0] = 0; + _disc.nParPointsBeforeType[0] = 0; + unsigned int nTotalParPoints = 0; + for (unsigned int j = 1; j < _disc.nParType + 1; ++j) + { + _disc.parTypeOffset[j] = _disc.parTypeOffset[j-1] + (_disc.nComp + _disc.strideBound[j-1]) * _disc.nParPoints[j-1] * _disc.nPoints; + _disc.nParPointsBeforeType[j] = _disc.nParPointsBeforeType[j-1] + _disc.nParPoints[j-1]; + nTotalParPoints += _disc.nParPoints[j-1]; + } + _disc.nParPointsBeforeType[_disc.nParType] = nTotalParPoints; + + // Configure particle discretization + _parCellSize.resize(_disc.offsetMetric[_disc.nParType]); + _parCenterRadius.resize(_disc.offsetMetric[_disc.nParType]); + _parOuterSurfAreaPerVolume.resize(_disc.offsetMetric[_disc.nParType]); + _parInnerSurfAreaPerVolume.resize(_disc.offsetMetric[_disc.nParType]); + + // Read particle discretization mode and default to "EQUIDISTANT_PAR" + _parDiscType = std::vector(_disc.nParType, ParticleDiscretizationMode::Equidistant); + std::vector pdt = paramProvider.getStringArray("PAR_DISC_TYPE"); + if ((pdt.size() == 1) && (_disc.nParType > 1)) + { + // Multiplex using first value + pdt.resize(_disc.nParType, pdt[0]); + } + else if (pdt.size() < _disc.nParType) + throw InvalidParameterException("Field PAR_DISC_TYPE contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (pdt[i] == "EQUIVOLUME_PAR") + _parDiscType[i] = ParticleDiscretizationMode::Equivolume; + else if (pdt[i] == "USER_DEFINED_PAR") + _parDiscType[i] = ParticleDiscretizationMode::UserDefined; + } + + // Read particle geometry and default to "SPHERICAL" + _parGeomSurfToVol = std::vector(_disc.nParType, SurfVolRatioSphere); + if (paramProvider.exists("PAR_GEOM")) + { + std::vector pg = paramProvider.getStringArray("PAR_GEOM"); + if ((pg.size() == 1) && (_disc.nParType > 1)) + { + // Multiplex using first value + pg.resize(_disc.nParType, pg[0]); + } + else if (pg.size() < _disc.nParType) + throw InvalidParameterException("Field PAR_GEOM contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (pg[i] == "SPHERE") + _parGeomSurfToVol[i] = SurfVolRatioSphere; + else if (pg[i] == "CYLINDER") + _parGeomSurfToVol[i] = SurfVolRatioCylinder; + else if (pg[i] == "SLAB") + _parGeomSurfToVol[i] = SurfVolRatioSlab; + else + throw InvalidParameterException("Unknown particle geometry type \"" + pg[i] + "\" at index " + std::to_string(i) + " of field PAR_GEOM"); + } + } + + if (paramProvider.exists("PAR_DISC_VECTOR")) + { + _parDiscVector = paramProvider.getDoubleArray("PAR_DISC_VECTOR"); + if (_parDiscVector.size() < nTotalParPoints + _disc.nParType) + throw InvalidParameterException("Field PAR_DISC_VECTOR contains too few elements (Sum [NPAR + 1] = " + std::to_string(nTotalParPoints + _disc.nParType) + " required)"); + } + + // Determine whether analytic Jacobian should be used but don't set it right now. + // We need to setup Jacobian matrices first. +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + const bool analyticJac = paramProvider.getBool("USE_ANALYTIC_JACOBIAN"); +#else + const bool analyticJac = false; +#endif + + // Allocate space for initial conditions + _initC.resize(_disc.nComp); + _initCp.resize(_disc.nComp * _disc.nParType); + _initQ.resize(nTotalBound); + + // Determine whether surface diffusion optimization is applied (decreases Jacobian size) //@TODO? + const bool optimizeParticleJacobianBandwidth = paramProvider.exists("OPTIMIZE_PAR_BANDWIDTH") ? paramProvider.getBool("OPTIMIZE_PAR_BANDWIDTH") : true; + + // Create nonlinear solver for consistent initialization + configureNonlinearSolver(paramProvider); + + paramProvider.popScope(); + + // ==== Construct and configure parameter dependencies + clearParDepSurfDiffusion(); + bool parSurfDiffDepConfSuccess = true; + if (paramProvider.exists("PAR_SURFDIFFUSION_DEP")) + { + const std::vector psdDepNames = paramProvider.getStringArray("PAR_SURFDIFFUSION_DEP"); + if ((psdDepNames.size() == 1) || (_disc.nParType == 1)) + _singleParDepSurfDiffusion = true; + + if (!_singleParDepSurfDiffusion && (psdDepNames.size() < _disc.nParType)) + throw InvalidParameterException("Field PAR_SURFDIFFUSION_DEP contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + else if (_singleParDepSurfDiffusion && (psdDepNames.size() != 1)) + throw InvalidParameterException("Field PAR_SURFDIFFUSION_DEP requires (only) 1 element"); + + if (_singleParDepSurfDiffusion) + { + if ((psdDepNames[0] == "") || (psdDepNames[0] == "NONE") || (psdDepNames[0] == "DUMMY")) + { + _hasParDepSurfDiffusion = false; + _singleParDepSurfDiffusion = true; + _parDepSurfDiffusion = std::vector(_disc.nParType, nullptr); + } + else + { + IParameterDependence* const pd = helper.createParameterDependence(psdDepNames[0]); + if (!pd) + throw InvalidParameterException("Unknown parameter dependence " + psdDepNames[0]); + + _parDepSurfDiffusion = std::vector(_disc.nParType, pd); + parSurfDiffDepConfSuccess = pd->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound, _disc.boundOffset); + _hasParDepSurfDiffusion = true; + } + } + else + { + _parDepSurfDiffusion = std::vector(_disc.nParType, nullptr); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if ((psdDepNames[0] == "") || (psdDepNames[0] == "NONE") || (psdDepNames[0] == "DUMMY")) + continue; + + _parDepSurfDiffusion[i] = helper.createParameterDependence(psdDepNames[i]); + if (!_parDepSurfDiffusion[i]) + throw InvalidParameterException("Unknown parameter dependence " + psdDepNames[i]); + + parSurfDiffDepConfSuccess = _parDepSurfDiffusion[i]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound + i * _disc.nComp, _disc.boundOffset + i * _disc.nComp) && parSurfDiffDepConfSuccess; + } + + _hasParDepSurfDiffusion = std::any_of(_parDepSurfDiffusion.cbegin(), _parDepSurfDiffusion.cend(), [](IParameterDependence const* pd) -> bool { return pd; }); + } + } + else + { + _hasParDepSurfDiffusion = false; + _singleParDepSurfDiffusion = true; + _parDepSurfDiffusion = std::vector(_disc.nParType, nullptr); + } + + if (optimizeParticleJacobianBandwidth) + { + // Check whether surface diffusion is present + _hasSurfaceDiffusion = std::vector(_disc.nParType, false); + if (paramProvider.exists("PAR_SURFDIFFUSION")) + { + const std::vector surfDiff = paramProvider.getDoubleArray("PAR_SURFDIFFUSION"); + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + // Assume particle surface diffusion if a parameter dependence is present + if (_parDepSurfDiffusion[i]) + { + _hasSurfaceDiffusion[i] = true; + continue; + } + + double const* const lsd = surfDiff.data() + _disc.nBoundBeforeType[i]; + + // Check surface diffusion coefficients of each particle type + for (unsigned int j = 0; j < _disc.strideBound[i]; ++j) + { + if (lsd[j] != 0.0) + { + _hasSurfaceDiffusion[i] = true; + break; + } + } + } + } + } + else + { + // Assume that surface diffusion is present + _hasSurfaceDiffusion = std::vector(_disc.nParType, true); + } + + const bool transportSuccess = _convDispOpB.configureModelDiscretization(paramProvider, _disc.nComp, _disc.nPoints, 0); // strideCell not needed for DG, so just set to zero + + _disc.dispersion = Eigen::VectorXd::Zero(_disc.nComp); // fill later on with convDispOpB (section and component dependent) + + _disc.velocity = static_cast(_convDispOpB.currentVelocity()); // updated later on (section dependent) + _disc.curSection = -1; + + _disc.colLength = paramProvider.getDouble("COL_LENGTH"); + _disc.deltaZ = _disc.colLength / _disc.nCol; + + // ==== Construct and configure binding model + clearBindingModels(); + _binding = std::vector(_disc.nParType, nullptr); + + std::vector bindModelNames = { "NONE" }; + if (paramProvider.exists("ADSORPTION_MODEL")) + bindModelNames = paramProvider.getStringArray("ADSORPTION_MODEL"); + + if (paramProvider.exists("ADSORPTION_MODEL_MULTIPLEX")) + _singleBinding = (paramProvider.getInt("ADSORPTION_MODEL_MULTIPLEX") == 1); + else + { + // Infer multiplex mode + _singleBinding = (bindModelNames.size() == 1); + } + + if (!_singleBinding && (bindModelNames.size() < _disc.nParType)) + throw InvalidParameterException("Field ADSORPTION_MODEL contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + else if (_singleBinding && (bindModelNames.size() != 1)) + throw InvalidParameterException("Field ADSORPTION_MODEL requires (only) 1 element"); + + bool bindingConfSuccess = true; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_singleBinding && (i > 0)) + { + // Reuse first binding model + _binding[i] = _binding[0]; + } + else + { + _binding[i] = helper.createBindingModel(bindModelNames[i]); + if (!_binding[i]) + throw InvalidParameterException("Unknown binding model " + bindModelNames[i]); + + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", _singleBinding, i, _disc.nParType == 1, _binding[i]->usesParamProviderInDiscretizationConfig()); + bindingConfSuccess = _binding[i]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound + i * _disc.nComp, _disc.boundOffset + i * _disc.nComp) && bindingConfSuccess; + } + } + + // ==== Construct and configure dynamic reaction model + bool reactionConfSuccess = true; + + _dynReactionBulk = nullptr; + if (paramProvider.exists("REACTION_MODEL")) + { + const std::string dynReactName = paramProvider.getString("REACTION_MODEL"); + _dynReactionBulk = helper.createDynamicReactionModel(dynReactName); + if (!_dynReactionBulk) + throw InvalidParameterException("Unknown dynamic reaction model " + dynReactName); + + if (_dynReactionBulk->usesParamProviderInDiscretizationConfig()) + paramProvider.pushScope("reaction_bulk"); + + reactionConfSuccess = _dynReactionBulk->configureModelDiscretization(paramProvider, _disc.nComp, nullptr, nullptr); + + if (_dynReactionBulk->usesParamProviderInDiscretizationConfig()) + paramProvider.popScope(); + } + + clearDynamicReactionModels(); + _dynReaction = std::vector(_disc.nParType, nullptr); + + if (paramProvider.exists("REACTION_MODEL_PARTICLES")) + { + const std::vector dynReactModelNames = paramProvider.getStringArray("REACTION_MODEL_PARTICLES"); + + if (paramProvider.exists("REACTION_MODEL_PARTICLES_MULTIPLEX")) + _singleDynReaction = (paramProvider.getInt("REACTION_MODEL_PARTICLES_MULTIPLEX") == 1); + else + { + // Infer multiplex mode + _singleDynReaction = (dynReactModelNames.size() == 1); + } + + if (!_singleDynReaction && (dynReactModelNames.size() < _disc.nParType)) + throw InvalidParameterException("Field REACTION_MODEL_PARTICLES contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + else if (_singleDynReaction && (dynReactModelNames.size() != 1)) + throw InvalidParameterException("Field REACTION_MODEL_PARTICLES requires (only) 1 element"); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_singleDynReaction && (i > 0)) + { + // Reuse first binding model + _dynReaction[i] = _dynReaction[0]; + } + else + { + _dynReaction[i] = helper.createDynamicReactionModel(dynReactModelNames[i]); + if (!_dynReaction[i]) + throw InvalidParameterException("Unknown dynamic reaction model " + dynReactModelNames[i]); + + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", _singleDynReaction, i, _disc.nParType == 1, _dynReaction[i]->usesParamProviderInDiscretizationConfig()); + reactionConfSuccess = _dynReaction[i]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound + i * _disc.nComp, _disc.boundOffset + i * _disc.nComp) && reactionConfSuccess; + } + } + } + + // Allocate memory + _tempState = new double[numDofs()]; + + if (_disc.exactInt) + _jacInlet.resize(_disc.nNodes, 1); // first cell depends on inlet concentration (same for every component) + else + _jacInlet.resize(1, 1); // first node depends on inlet concentration (same for every component) + + // set jacobian pattern + _globalJacDisc.resize(numDofs(), numDofs()); + _globalJac.resize(numDofs(), numDofs()); + // pattern is set in configure(), after surface diffusion is read + //FDJac = MatrixXd::Zero(numDofs(), numDofs()); // todo delete + + // Set whether analytic Jacobian is used + useAnalyticJacobian(analyticJac); + + return transportSuccess && parSurfDiffDepConfSuccess && bindingConfSuccess && reactionConfSuccess; +} + +bool GeneralRateModelDG::configure(IParameterProvider& paramProvider) +{ + _parameters.clear(); + + const bool transportSuccess = _convDispOpB.configure(_unitOpIdx, paramProvider, _parameters); + + // Read geometry parameters + _colPorosity = paramProvider.getDouble("COL_POROSITY"); + _singleParRadius = readAndRegisterMultiplexTypeParam(paramProvider, _parameters, _parRadius, "PAR_RADIUS", _disc.nParType, _unitOpIdx); + _singleParPorosity = readAndRegisterMultiplexTypeParam(paramProvider, _parameters, _parPorosity, "PAR_POROSITY", _disc.nParType, _unitOpIdx); + + // Let PAR_CORERADIUS default to 0.0 for backwards compatibility + if (paramProvider.exists("PAR_CORERADIUS")) + _singleParCoreRadius = readAndRegisterMultiplexTypeParam(paramProvider, _parameters, _parCoreRadius, "PAR_CORERADIUS", _disc.nParType, _unitOpIdx); + else + { + _singleParCoreRadius = true; + _parCoreRadius = std::vector(_disc.nParType, 0.0); + } + + // Check whether PAR_TYPE_VOLFRAC is required or not + if ((_disc.nParType > 1) && !paramProvider.exists("PAR_TYPE_VOLFRAC")) + throw InvalidParameterException("The required parameter \"PAR_TYPE_VOLFRAC\" was not found"); + + // Let PAR_TYPE_VOLFRAC default to 1.0 for backwards compatibility + if (paramProvider.exists("PAR_TYPE_VOLFRAC")) + { + readScalarParameterOrArray(_parTypeVolFrac, paramProvider, "PAR_TYPE_VOLFRAC", 1); + if (_parTypeVolFrac.size() == _disc.nParType) + { + _axiallyConstantParTypeVolFrac = true; + + // Expand to all axial cells + _parTypeVolFrac.resize(_disc.nPoints * _disc.nParType, 1.0); + for (unsigned int i = 1; i < _disc.nPoints; ++i) + std::copy(_parTypeVolFrac.begin(), _parTypeVolFrac.begin() + _disc.nParType, _parTypeVolFrac.begin() + _disc.nParType * i); + } + else + _axiallyConstantParTypeVolFrac = false; + } + else + { + _parTypeVolFrac.resize(_disc.nPoints, 1.0); + _axiallyConstantParTypeVolFrac = false; + } + + // Check whether all sizes are matched + if (_disc.nParType != _parRadius.size()) + throw InvalidParameterException("Number of elements in field PAR_RADIUS does not match number of particle types"); + if (_disc.nParType * _disc.nPoints != _parTypeVolFrac.size()) + throw InvalidParameterException("Number of elements in field PAR_TYPE_VOLFRAC does not match number of particle types times number of axial cells"); + if (_disc.nParType != _parPorosity.size()) + throw InvalidParameterException("Number of elements in field PAR_POROSITY does not match number of particle types"); + if (_disc.nParType != _parCoreRadius.size()) + throw InvalidParameterException("Number of elements in field PAR_CORERADIUS does not match number of particle types"); + + // Check that particle volume fractions sum to 1.0 + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + const double volFracSum = std::accumulate(_parTypeVolFrac.begin() + i * _disc.nParType, _parTypeVolFrac.begin() + (i+1) * _disc.nParType, 0.0, + [](double a, const active& b) -> double { return a + static_cast(b); }); + if (std::abs(1.0 - volFracSum) > 1e-10) + throw InvalidParameterException("Sum of field PAR_TYPE_VOLFRAC differs from 1.0 (is " + std::to_string(volFracSum) + ") in axial cell " + std::to_string(i)); + } + + // Read vectorial parameters (which may also be section dependent; transport) + _filmDiffusionMode = readAndRegisterMultiplexCompTypeSecParam(paramProvider, _parameters, _filmDiffusion, "FILM_DIFFUSION", _disc.nParType, _disc.nComp, _unitOpIdx); + _parDiffusionMode = readAndRegisterMultiplexCompTypeSecParam(paramProvider, _parameters, _parDiffusion, "PAR_DIFFUSION", _disc.nParType, _disc.nComp, _unitOpIdx); + + _disc.offsetSurfDiff = new unsigned int[_disc.strideBound[_disc.nParType]]; + if (paramProvider.exists("PAR_SURFDIFFUSION")) + _parSurfDiffusionMode = readAndRegisterMultiplexBndCompTypeSecParam(paramProvider, _parameters, _parSurfDiffusion, "PAR_SURFDIFFUSION", _disc.nParType, _disc.nComp, _disc.strideBound, _disc.nBound, _unitOpIdx); + else + { + _parSurfDiffusionMode = MultiplexMode::Component; + _parSurfDiffusion.resize(_disc.strideBound[_disc.nParType], 0.0); + } + + bool parSurfDiffDepConfSuccess = true; + if (_hasParDepSurfDiffusion) + { + if (_singleParDepSurfDiffusion && _parDepSurfDiffusion[0]) + { + parSurfDiffDepConfSuccess = _parDepSurfDiffusion[0]->configure(paramProvider, _unitOpIdx, ParTypeIndep, "PAR_SURFDIFFUSION"); + } + else if (!_singleParDepSurfDiffusion) + { + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (!_parDepSurfDiffusion[i]) + continue; + + parSurfDiffDepConfSuccess = _parDepSurfDiffusion[i]->configure(paramProvider, _unitOpIdx, i, "PAR_SURFDIFFUSION") && parSurfDiffDepConfSuccess; + } + } + } + + if ((_filmDiffusion.size() < _disc.nComp * _disc.nParType) || (_filmDiffusion.size() % (_disc.nComp * _disc.nParType) != 0)) + throw InvalidParameterException("Number of elements in field FILM_DIFFUSION is not a positive multiple of NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ")"); + if ((_parDiffusion.size() < _disc.nComp * _disc.nParType) || (_parDiffusion.size() % (_disc.nComp * _disc.nParType) != 0)) + throw InvalidParameterException("Number of elements in field PAR_DIFFUSION is not a positive multiple of NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ")"); + if ((_parSurfDiffusion.size() < _disc.strideBound[_disc.nParType]) || ((_disc.strideBound[_disc.nParType] > 0) && (_parSurfDiffusion.size() % _disc.strideBound[_disc.nParType] != 0))) + throw InvalidParameterException("Number of elements in field PAR_SURFDIFFUSION is not a positive multiple of NTOTALBND (" + std::to_string(_disc.strideBound[_disc.nParType]) + ")"); + + if (paramProvider.exists("PORE_ACCESSIBILITY")) + _poreAccessFactorMode = readAndRegisterMultiplexCompTypeSecParam(paramProvider, _parameters, _poreAccessFactor, "PORE_ACCESSIBILITY", _disc.nParType, _disc.nComp, _unitOpIdx); + else + { + _poreAccessFactorMode = MultiplexMode::ComponentType; + _poreAccessFactor = std::vector(_disc.nComp * _disc.nParType, 1.0); + } + + if (_disc.nComp * _disc.nParType != _poreAccessFactor.size()) + throw InvalidParameterException("Number of elements in field PORE_ACCESSIBILITY differs from NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ")"); + + // Add parameters to map + _parameters[makeParamId(hashString("COL_POROSITY"), _unitOpIdx, CompIndep, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = &_colPorosity; + + if (_axiallyConstantParTypeVolFrac) + { + // Register only the first nParType items + for (unsigned int i = 0; i < _disc.nParType; ++i) + _parameters[makeParamId(hashString("PAR_TYPE_VOLFRAC"), _unitOpIdx, CompIndep, i, BoundStateIndep, ReactionIndep, SectionIndep)] = &_parTypeVolFrac[i]; + } + else + registerParam2DArray(_parameters, _parTypeVolFrac, [=](bool multi, unsigned cell, unsigned int type) { return makeParamId(hashString("PAR_TYPE_VOLFRAC"), _unitOpIdx, CompIndep, type, BoundStateIndep, ReactionIndep, cell); }, _disc.nParType); + + // Calculate the particle radial discretization variables (_parCellSize, _parCenterRadius, etc.) + updateRadialDisc(); + + // Register initial conditions parameters + registerParam1DArray(_parameters, _initC, [=](bool multi, unsigned int comp) { return makeParamId(hashString("INIT_C"), _unitOpIdx, comp, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep); }); + + if (_singleBinding) + { + for (unsigned int c = 0; c < _disc.nComp; ++c) + _parameters[makeParamId(hashString("INIT_CP"), _unitOpIdx, c, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = &_initCp[c]; + } + else + registerParam2DArray(_parameters, _initCp, [=](bool multi, unsigned int type, unsigned int comp) { return makeParamId(hashString("INIT_CP"), _unitOpIdx, comp, type, BoundStateIndep, ReactionIndep, SectionIndep); }, _disc.nComp); + + + if (!_binding.empty()) + { + const unsigned int maxBoundStates = *std::max_element(_disc.strideBound, _disc.strideBound + _disc.nParType); + std::vector initParams(maxBoundStates); + + if (_singleBinding) + { + _binding[0]->fillBoundPhaseInitialParameters(initParams.data(), _unitOpIdx, ParTypeIndep); + + active* const iq = _initQ.data() + _disc.nBoundBeforeType[0]; + for (unsigned int i = 0; i < _disc.strideBound[0]; ++i) + _parameters[initParams[i]] = iq + i; + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + _binding[type]->fillBoundPhaseInitialParameters(initParams.data(), _unitOpIdx, type); + + active* const iq = _initQ.data() + _disc.nBoundBeforeType[type]; + for (unsigned int i = 0; i < _disc.strideBound[type]; ++i) + _parameters[initParams[i]] = iq + i; + } + } + } + + // Reconfigure binding model + bool bindingConfSuccess = true; + if (!_binding.empty()) + { + if (_singleBinding) + { + if (_binding[0] && _binding[0]->requiresConfiguration()) + { + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", true); + bindingConfSuccess = _binding[0]->configure(paramProvider, _unitOpIdx, ParTypeIndep); + } + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_binding[type] || !_binding[type]->requiresConfiguration()) + continue; + + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", type, _disc.nParType == 1, true); + bindingConfSuccess = _binding[type]->configure(paramProvider, _unitOpIdx, type) && bindingConfSuccess; + } + } + } + + // Reconfigure reaction model + bool dynReactionConfSuccess = true; + if (_dynReactionBulk && _dynReactionBulk->requiresConfiguration()) + { + paramProvider.pushScope("reaction_bulk"); + dynReactionConfSuccess = _dynReactionBulk->configure(paramProvider, _unitOpIdx, ParTypeIndep); + paramProvider.popScope(); + } + + if (_singleDynReaction) + { + if (_dynReaction[0] && _dynReaction[0]->requiresConfiguration()) + { + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", true); + dynReactionConfSuccess = _dynReaction[0]->configure(paramProvider, _unitOpIdx, ParTypeIndep) && dynReactionConfSuccess; + } + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_dynReaction[type] || !_dynReaction[type]->requiresConfiguration()) + continue; + + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", type, _disc.nParType == 1, true); + dynReactionConfSuccess = _dynReaction[type]->configure(paramProvider, _unitOpIdx, type) && dynReactionConfSuccess; + } + } + + // jaobian pattern set after binding and particle surface diffusion are configured + setJacobianPattern_GRM(_globalJac, 0, _dynReactionBulk); + _globalJacDisc = _globalJac; + // the solver repetitively solves the linear system with a static pattern of the jacobian (set above). + // The goal of analyzePattern() is to reorder the nonzero elements of the matrix, such that the factorization step creates less fill-in + _globalSolver.analyzePattern(_globalJacDisc.block(_disc.nComp, _disc.nComp, numPureDofs(), numPureDofs())); + + return transportSuccess && parSurfDiffDepConfSuccess && bindingConfSuccess && dynReactionConfSuccess; +} + +unsigned int GeneralRateModelDG::threadLocalMemorySize() const CADET_NOEXCEPT +{ + LinearMemorySizer lms; + + // Memory for residualImpl() + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_binding[i] && _binding[i]->requiresWorkspace()) + lms.fitBlock(_binding[i]->workspaceSize(_disc.nComp, _disc.strideBound[i], _disc.nBound + i * _disc.nComp)); + + if (_dynReaction[i] && _dynReaction[i]->requiresWorkspace()) + lms.fitBlock(_dynReaction[i]->workspaceSize(_disc.nComp, _disc.strideBound[i], _disc.nBound + i * _disc.nComp)); + } + + if (_dynReactionBulk && _dynReactionBulk->requiresWorkspace()) + lms.fitBlock(_dynReactionBulk->workspaceSize(_disc.nComp, 0, nullptr)); + + const unsigned int maxStrideBound = *std::max_element(_disc.strideBound, _disc.strideBound + _disc.nParType); + lms.add(_disc.nComp + maxStrideBound); + lms.add((maxStrideBound + _disc.nComp) * (maxStrideBound + _disc.nComp)); + + lms.commit(); + const std::size_t resImplSize = lms.bufferSize(); + + // Memory for consistentInitialState() + lms.add(_nonlinearSolver->workspaceSize(_disc.nComp + maxStrideBound) * sizeof(double)); + lms.add(_disc.nComp + maxStrideBound); + lms.add(_disc.nComp + maxStrideBound); + lms.add(_disc.nComp + maxStrideBound); + lms.add((_disc.nComp + maxStrideBound) * (_disc.nComp + maxStrideBound)); + lms.add(_disc.nComp); + + lms.addBlock(resImplSize); + lms.commit(); + + // Memory for consistentInitialSensitivity + lms.add(_disc.nComp + maxStrideBound); + lms.add(maxStrideBound); + lms.commit(); + + return lms.bufferSize(); +} +//@TODO: AD +unsigned int GeneralRateModelDG::numAdDirsForJacobian() const CADET_NOEXCEPT +{ + // We need as many directions as the highest bandwidth of the diagonal blocks: + // The bandwidth of the column block depends on the size of the WENO stencil, whereas + // the bandwidth of the particle blocks are given by the number of components and bound states. + + // Get maximum stride of particle type blocks + //int maxStride = 0; + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // maxStride = std::max(maxStride, _jacP[type * _disc.nPoints].stride()); + //} + + return 1;// std::max(_convDispOp.requiredADdirs(), maxStride); +} + +void GeneralRateModelDG::useAnalyticJacobian(const bool analyticJac) +{ +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + _analyticJac = analyticJac; + if (!_analyticJac) + _jacobianAdDirs = numAdDirsForJacobian(); + else + _jacobianAdDirs = 0; +#else + // If CADET_CHECK_ANALYTIC_JACOBIAN is active, we always enable AD for comparison and use it in simulation + _analyticJac = false; + _jacobianAdDirs = numAdDirsForJacobian(); +#endif +} + +void GeneralRateModelDG::notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac) +{ + // calculate offsets between surface diffusion storage and state vector order + orderSurfDiff(); + + Indexer idxr(_disc); + + // todo: only reset jacobian pattern if it changes, i.e. once in configuration and then only for changes in SurfDiff+kinetic binding. + setJacobianPattern_GRM(_globalJac, 0, _dynReactionBulk); + _globalJacDisc = _globalJac; + + // ConvectionDispersionOperator tells us whether flow direction has changed + if (!_convDispOpB.notifyDiscontinuousSectionTransition(t, secIdx)) { + // (re)compute DG Jaconian blocks (can only be done after notify) + updateSection(secIdx); + _disc.initializeDGjac(_parGeomSurfToVol); + return; + } + else { + // (re)compute DG Jacobian blocks + updateSection(secIdx); + _disc.initializeDGjac(_parGeomSurfToVol); + } + + // @TODO: backwards flow + //// Setup the matrix connecting inlet DOFs to first column cells + //_jacInlet.clear(); + //const double h = static_cast(_convDispOpB.columnLength()) / static_cast(_disc.nPoints); + //const double u = static_cast(_convDispOpB.currentVelocity()); + + //if (u >= 0.0) + //{ + // // Forwards flow + + // // Place entries for inlet DOF to first column cell conversion + // for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + // _jacInlet.addElement(comp * idxr.strideColComp(), comp, -u / h); + //} + //else + //{ + // // Backwards flow + + // // Place entries for inlet DOF to last column cell conversion + // const unsigned int offset = (_disc.nPoints - 1) * idxr.strideColNode(); + // for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + // _jacInlet.addElement(offset + comp * idxr.strideColComp(), comp, u / h); + //} +} + +void GeneralRateModelDG::setFlowRates(active const* in, active const* out) CADET_NOEXCEPT +{ + _convDispOpB.setFlowRates(in[0], out[0], _colPorosity); +} + +void GeneralRateModelDG::reportSolution(ISolutionRecorder& recorder, double const* const solution) const +{ + Exporter expr(_disc, *this, solution); + recorder.beginUnitOperation(_unitOpIdx, *this, expr); + recorder.endUnitOperation(); +} + +void GeneralRateModelDG::reportSolutionStructure(ISolutionRecorder& recorder) const +{ + Exporter expr(_disc, *this, nullptr); + recorder.unitOperationStructure(_unitOpIdx, *this, expr); +} +// @TODO: AD +unsigned int GeneralRateModelDG::requiredADdirs() const CADET_NOEXCEPT +{ +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + return _jacobianAdDirs; +#else + // If CADET_CHECK_ANALYTIC_JACOBIAN is active, we always need the AD directions for the Jacobian + return numAdDirsForJacobian(); +#endif +} +// @TODO: AD +void GeneralRateModelDG::prepareADvectors(const AdJacobianParams& adJac) const +{ + //// Early out if AD is disabled + //if (!adJac.adY) + // return; + + //Indexer idxr(_disc); + + //// Column block + //_convDispOp.prepareADvectors(adJac); + + //// Particle blocks + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // const unsigned int lowerParBandwidth = _jacP[type * _disc.nPoints].lowerBandwidth(); + // const unsigned int upperParBandwidth = _jacP[type * _disc.nPoints].upperBandwidth(); + + // for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) + // { + // ad::prepareAdVectorSeedsForBandMatrix(adJac.adY + idxr.offsetCp(ParticleTypeIndex{type}, ParticleIndex{pblk}), adJac.adDirOffset, idxr.strideParBlock(type), lowerParBandwidth, upperParBandwidth, lowerParBandwidth); + // } + //} +} +//@TODO: enable AD +/** + * @brief Extracts the system Jacobian from band compressed AD seed vectors + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ +void GeneralRateModelDG::extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset) +{ + //Indexer idxr(_disc); + + //// Column + //_convDispOp.extractJacobianFromAD(adRes, adDirOffset); + + //// Particles + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) + // { + // linalg::BandMatrix& jacMat = _jacP[_disc.nPoints * type + pblk]; + // ad::extractBandedJacobianFromAd(adRes + idxr.offsetCp(ParticleTypeIndex{type}, ParticleIndex{pblk}), adDirOffset, jacMat.lowerBandwidth(), jacMat); + // } + //} +} + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + +//@TODO: enable AD +/** + * @brief Compares the analytical Jacobian with a Jacobian derived by AD + * @details The analytical Jacobian is assumed to be stored in the corresponding band matrices. + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ +void GeneralRateModelDG::checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const +{ + Indexer idxr(_disc); + + LOG(Debug) << "AD dir offset: " << adDirOffset << " DiagDirCol: " << _convDispOp.jacobian().lowerBandwidth() << " DiagDirPar: " << _jacP[0].lowerBandwidth(); + + // Column + const double maxDiffCol = _convDispOp.checkAnalyticJacobianAgainstAd(adRes, adDirOffset); + + // Particles + double maxDiffPar = 0.0; + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) + { + linalg::BandMatrix& jacMat = _jacP[_disc.nPoints * type + pblk]; + const double localDiff = ad::compareBandedJacobianWithAd(adRes + idxr.offsetCp(ParticleTypeIndex{type}, ParticleIndex{pblk}), adDirOffset, jacMat.lowerBandwidth(), jacMat); + LOG(Debug) << "-> Par type " << type << " block " << pblk << " diff: " << localDiff; + maxDiffPar = std::max(maxDiffPar, localDiff); + } + } +} + +#endif + +int GeneralRateModelDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidual); + + // Evaluate residual do not compute Jacobian or parameter sensitivities + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); +} + +int GeneralRateModelDG::residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidual); + + //FDJac = calcFDJacobian(static_cast(simState.vecStateY), static_cast(simState.vecStateYdot), simTime, threadLocalMem, 2.0); // todo delete + + // Evaluate residual, use AD for Jacobian if required but do not evaluate parameter derivatives + return residual(simTime, simState, res, adJac, threadLocalMem, true, false); +} + +int GeneralRateModelDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, + const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity) +{ + if (updateJacobian) + { + _factorizeJacobian = true; + +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + if (_analyticJac) + { + if (paramSensitivity) // TODO: sensitivities + { + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } + else + { + // Compute Jacobian via AD // TODO: AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) // TODO: sensitivities + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; + } +#else + // Compute Jacobian via AD // TODO: AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Only do comparison if we have a residuals vector (which is not always the case) + if (res) + { + // Evaluate with analytical Jacobian which is stored in the band matrices + retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + + // Compare AD with anaytic Jacobian + checkAnalyticJacobianAgainstAd(adJac.adRes, adJac.adDirOffset); + } + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; +#endif + } + else + { + if (paramSensitivity) // TODO: sensitivities + { + // initialize residuals with zero + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } +} + +template +int GeneralRateModelDG::residualImpl(double t, unsigned int secIdx, StateType const* const y, double const* const yDot, ResidualType* const res, util::ThreadLocalStorage& threadLocalMem) +{ + + // determine wether we have a section switch. If so, set velocity, dispersion, newStaticJac + updateSection(secIdx); + + double* const resPtr = reinterpret_cast(res); + Eigen::Map resi(resPtr, numDofs()); + resi.setZero(); + + + if (wantJac && _disc.newStaticJac) { + + // estimate new static (per section) jacobian + bool success = calcStaticAnaJacobian_GRM(secIdx); + + _disc.newStaticJac = false; + + if (cadet_unlikely(!success)) { + LOG(Error) << "Jacobian pattern did not fit the Jacobian estimation"; + } + } + + residualBulk(t, secIdx, y, yDot, res, threadLocalMem); + + BENCH_START(_timerResidualPar); + + for (unsigned int pblk = 0; pblk < _disc.nPoints * _disc.nParType; ++pblk) + { + const unsigned int parType = pblk / _disc.nPoints; + const unsigned int par = pblk % _disc.nPoints; + residualParticle(t, parType, par, secIdx, y, yDot, res, threadLocalMem); + } + + // we need to add the DG discretized solid entries of the jacobian that get overwritten by the binding kernel. + // These entries only exist for the GRM with surface diffusion + if (wantJac) { + for (unsigned int parType = 0; parType < _disc.nParType; parType++) { + if (_binding[parType]->hasDynamicReactions() && _hasSurfaceDiffusion[parType]) { + active const* const _parSurfDiff = getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], secIdx) + _disc.nBoundBeforeType[parType]; + addSolidDGentries(parType, _parSurfDiff); + } + } + } + + BENCH_STOP(_timerResidualPar); + + residualFlux(t, secIdx, y, yDot, res); + + // Handle inlet DOFs, which are simply copied to the residual + for (unsigned int i = 0; i < _disc.nComp; ++i) + { + res[i] = y[i]; + } + + return 0; +} + +template +int GeneralRateModelDG::residualBulk(double t, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase, util::ThreadLocalStorage& threadLocalMem) +{ + Indexer idxr(_disc); + + // Eigen access to data pointers + const double* yPtr = reinterpret_cast(yBase); + const double* const ypPtr = reinterpret_cast(yDotBase); + double* const resPtr = reinterpret_cast(resBase); + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + + // extract current component mobile phase, mobile phase residual, mobile phase derivative (discontinous memory blocks) + Eigen::Map> cl_comp(yPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> clRes_comp(resPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> clDot_comp(ypPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + + /* convection dispersion RHS */ + + _disc.boundary[0] = yPtr[comp]; // copy inlet DOFs to ghost node + ConvDisp_DG(cl_comp, clRes_comp, t, comp); + + /* residual */ + + if (ypPtr) // NULLpointer for consistent initialization + clRes_comp = clDot_comp - clRes_comp; + } + + if (!_dynReactionBulk || (_dynReactionBulk->numReactionsLiquid() == 0)) + return 0; + + // Dynamic reactions + if (_dynReactionBulk) { + // Get offsets + StateType const* y = yBase + idxr.offsetC(); + ResidualType* res = resBase + idxr.offsetC(); + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + for (unsigned int col = 0; col < _disc.nPoints; ++col, y += idxr.strideColNode(), res += idxr.strideColNode()) + { + const ColumnPosition colPos{ (0.5 + static_cast(col)) / static_cast(_disc.nPoints), 0.0, 0.0 }; + _dynReactionBulk->residualLiquidAdd(t, secIdx, colPos, y, res, -1.0, tlmAlloc); + + if (wantJac) + { + linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, col * idxr.strideColNode()); + // static_cast should be sufficient here, but this statement is also analyzed when wantJac = false + _dynReactionBulk->analyticJacobianLiquidAdd(t, secIdx, colPos, reinterpret_cast(y), -1.0, jac, tlmAlloc); + } + } + } + + return 0; +} + +template +int GeneralRateModelDG::residualParticle(double t, unsigned int parType, unsigned int colNode, unsigned int secIdx, StateType const* yBase, + double const* yDotBase, ResidualType* resBase, util::ThreadLocalStorage& threadLocalMem) +{ + Indexer idxr(_disc); + + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + // special case: individual treatment of time derivatives in particle mass balance at inner particle boundary node + bool specialCase = !_disc.parExactInt[parType] && (_parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab && _parCoreRadius[parType] == 0.0); + + // Prepare parameters + active const* const parDiff = getSectionDependentSlice(_parDiffusion, _disc.nComp * _disc.nParType, secIdx) + parType * _disc.nComp; + + // Ordering of particle surface diffusion: + // bnd0comp0, bnd0comp1, bnd0comp2, bnd1comp0, bnd1comp1, bnd1comp2 + active const* const _parSurfDiff = getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], secIdx) + _disc.nBoundBeforeType[parType]; + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(colNode / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[colNode % _disc.nNodes])) / _disc.colLength; + + // The RowIterator is always centered on the main diagonal. + // This means that jac[0] is the main diagonal, jac[-1] is the first lower diagonal, + // and jac[1] is the first upper diagonal. We can also access the rows from left to + // right beginning with the last lower diagonal moving towards the main diagonal and + // continuing to the last upper diagonal by using the native() method. + linalg::BandedEigenSparseRowIterator jac(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode })); + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + const parts::cell::CellParameters cellResParams = makeCellResidualParams(parType, qsReaction); + + // Handle time derivatives, binding, dynamic reactions: residualKernel computes discrete point wise, + // so we loop over each discrete particle point + for (unsigned int par = 0; par < _disc.nParPoints[parType]; ++par) + { + int cell = std::floor(par / _disc.nParNode[parType]); + // local Pointers to current particle node, needed in residualKernel + StateType const* local_y = yBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + par * idxr.strideParNode(parType); + double const* local_yDot = yDotBase ? yDotBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + par * idxr.strideParNode(parType) : nullptr; + ResidualType* local_res = resBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + par * idxr.strideParNode(parType); + + // r (particle) coordinate of current node (particle radius normed to 1) - needed in externally dependent adsorption kinetic + const double r = (_disc.deltaR[_disc.offsetMetric[parType] + cell] * cell + + 0.5 * _disc.deltaR[_disc.offsetMetric[parType] + cell] * (1 + _disc.parNodes[parType][par % _disc.nParNode[parType]])) + / (static_cast(_parRadius[parType]) - static_cast(_parCoreRadius[parType])); + const ColumnPosition colPos{ z, 0.0, r }; + + // Handle time derivatives, binding, dynamic reactions. + // if special case: Dont add time derivatives to inner boundary node for DG discretized mass balance equations. + // This can be achieved by setting yDot pointer to null before passing to residual kernel, and adding only the time derivative for dynamic binding + // TODO Check Treatment of reactions (do we need yDot then?) + if (cadet_unlikely(par == 0 && specialCase)) { + + parts::cell::residualKernel( + t, secIdx, colPos, local_y, nullptr, local_res, jac, cellResParams, tlmAlloc // TODO Check Treatment of reactions (do we need yDot then?) + ); + + if (cellResParams.binding->hasDynamicReactions() && local_yDot) + { + unsigned int idx = 0; + for (unsigned int comp = 0; comp < cellResParams.nComp; ++comp) + { + for (unsigned int state = 0; state < cellResParams.nBound[comp]; ++state, ++idx) + { + // Skip quasi-stationary fluxes + if (cellResParams.qsReaction[idx]) + continue; + + // for kinetic bindings and surface diffusion, we have an additional DG-discretized mass balance eq. + // -> add time derivate at inner bonudary node only without surface diffusion + else if (_hasSurfaceDiffusion[parType]) + continue; + // some bound states might still not be effected by surface diffusion + else if (_parSurfDiff[idx] != 0.0) + continue; + + // Add time derivative to solid phase + local_res[idxr.strideParLiquid() + idx] += local_yDot[idxr.strideParLiquid() + idx]; + } + } + } + } + else { + + parts::cell::residualKernel( + t, secIdx, colPos, local_y, local_yDot, local_res, jac, cellResParams, tlmAlloc + ); + + } + + // move rowiterator to next particle node + jac += idxr.strideParNode(parType); + } + + // We still need to handle transport/diffusion + + // get pointers to the particle block of the current column node, particle type + const double* c_p = reinterpret_cast(yBase) + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + double* resC_p = reinterpret_cast(resBase) + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + + // Mobile phase RHS + + // get film diffusion flux at current node to compute boundary condition + active const* const filmDiff = getSectionDependentSlice(_filmDiffusion, _disc.nComp * _disc.nParType, secIdx) + parType * _disc.nComp; + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.localFlux[comp] = static_cast(filmDiff[comp]) * (reinterpret_cast(yBase)[idxr.offsetC() + colNode * idxr.strideColNode() + comp] + - reinterpret_cast(yBase)[idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + (_disc.nParPoints[parType] - 1) * idxr.strideParNode(parType) + comp]); + } + + int nNodes = _disc.nParNode[parType]; + int nCells = _disc.nParCell[parType]; + int nPoints = _disc.nParPoints[parType]; + int nComp = _disc.nComp; + + int strideParLiquid = idxr.strideParLiquid(); + int strideParNode = idxr.strideParNode(parType); + + for (unsigned int comp = 0; comp < nComp; comp++) + { + // component dependent (through access factor) inverse Beta_P + double invBetaP = (1.0 - static_cast(_parPorosity[parType])) / (static_cast(_poreAccessFactor[_disc.nComp * parType + comp]) * static_cast(_parPorosity[parType])); + + // =====================================================================================================// + // solve auxiliary systems d_p g_p + d_s beta_p sum g_s= d (d_p c_p + d_s beta_p sum c_s) / d xi // + // =====================================================================================================// + // component-wise! strides + unsigned int strideCell = nNodes; + unsigned int strideNode = 1u; + // reset cache for auxiliary variable + _disc.g_pSum[parType].setZero(); + _disc.g_p[parType].setZero(); + + Eigen::Map> cp(c_p + comp, _disc.nParPoints[parType], InnerStride(idxr.strideParNode(parType))); + + // handle surface diffusion: Compute auxiliary variable; For kinetic bindings: add additional mass balance to residual of respective bound state + if (_hasSurfaceDiffusion[parType]) { + + for (int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + + if (static_cast(_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { // some bound states might still not be effected by surface diffusion + + // get solid phase vector + Eigen::Map> q_p(c_p + strideParLiquid + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd, + _disc.nParPoints[parType], InnerStride(strideParNode)); + // compute g_s = d c_s / d xi + solve_auxiliary_DG(parType, q_p, strideCell, strideNode, comp); + // apply invBeta_p, d_s and add to sum -> gSum += d_s * invBeta_p * (D c - M^-1 B [c - c^*]) + _disc.g_pSum[parType] += _disc.g_p[parType] * invBetaP * static_cast(_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]); + + /* For kinetic bindings with surface diffusion: add the additional DG-discretized particle mass balance equations to residual */ + + if (!qsReaction[bnd]) { + + // Eigen access to current bound state residual + Eigen::Map> resCs( + reinterpret_cast(resBase) + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd, + _disc.nParPoints[parType], InnerStride(idxr.strideParNode(parType)) + ); + + applyParInvMap(_disc.g_p[parType], parType); + _disc.g_p[parType] *= static_cast(_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]); + + // Eigen access to auxiliary variable of current bound state + Eigen::Map> _gp(&_disc.g_p[parType][0], _disc.nParPoints[parType], InnerStride(1)); + + // adds - D_r * gs to the residual, including metric part.->res = invMap^2* [ -D_r * (d_s c^s) ] + parVolumeIntegral(parType, false, _gp, resCs); + + // adds M^-1 B (gs - gs^*) to the residual -> res = invMap^2 * [ - D_r * (d_s c^s) + M^-1 B (gs - gs^*) ] + parSurfaceIntegral(parType, _gp, resCs, strideCell, strideNode, false, comp, true); + } + } + } + } + + // compute g_p = d c_p / d xi + solve_auxiliary_DG(parType, cp, strideCell, strideNode, comp); + + // add particle diffusion part to auxiliary variable sum -> gSum += d_p * (D c - M^-1 B [c - c^*]) + _disc.g_pSum[parType] += _disc.g_p[parType] * static_cast(parDiff[comp]); + + // apply squared inverse mapping to sum of bound state auxiliary variables -> gSum = - invMap^2 * (d_p * c^p + sum_mi d_s invBeta_p c^s) + applyParInvMap(_disc.g_pSum[parType], parType); + + // ====================================================================================// + // solve DG-discretized particle mass balance // + // ====================================================================================// + + /* solve DG-discretized particle mass balance equation */ + + Eigen::Map> _g_pSum(&_disc.g_pSum[parType][0], _disc.nParPoints[parType], InnerStride(1)); + + // Eigen access to particle liquid residual + Eigen::Map> resCp(resC_p + comp, _disc.nParPoints[parType], InnerStride(idxr.strideParNode(parType))); + + // adds - D_r * (g_sum) to the residual, including metric part. -> res = - D_r * (d_p * c^p + invBeta_p sum_mi d_s c^s) + parVolumeIntegral(parType, false, _g_pSum, resCp); + + // adds M^-1 B (g_sum - g_sum^*) to the residual -> res = - D_r * (d_p * c^p + invBeta_p sum_mi d_s c^s) + M^-1 B (g_sum - g_sum^*) + parSurfaceIntegral(parType, _g_pSum, resCp, strideCell, strideNode, false, comp); + + } + + return 0; +} + +template +int GeneralRateModelDG::residualFlux(double t, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase) +{ + Indexer idxr(_disc); + + const ParamType invBetaC = 1.0 / static_cast(_colPorosity) - 1.0; + + // Get offsets + ResidualType* const resCol = resBase + idxr.offsetC(); + StateType const* const yCol = yBase + idxr.offsetC(); + + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + ResidualType* const resParType = resBase + idxr.offsetCp(ParticleTypeIndex{type}); + StateType const* const yParType = yBase + idxr.offsetCp(ParticleTypeIndex{type}); + + const ParamType epsP = static_cast(_parPorosity[type]); + + // Ordering of diffusion: + // sec0type0comp0, sec0type0comp1, sec0type0comp2, sec0type1comp0, sec0type1comp1, sec0type1comp2, + // sec1type0comp0, sec1type0comp1, sec1type0comp2, sec1type1comp0, sec1type1comp1, sec1type1comp2, ... + active const* const filmDiff = getSectionDependentSlice(_filmDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + active const* const parDiff = getSectionDependentSlice(_parDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + + const ParamType surfaceToVolumeRatio = _parGeomSurfToVol[type] / static_cast(_parRadius[type]); + + const ParamType jacCF_val = invBetaC * surfaceToVolumeRatio; + const ParamType jacPF_val = -1.0 / epsP; + + // Add flux to column void / bulk volume + for (unsigned int i = 0; i < _disc.nPoints * _disc.nComp; ++i) + { + const unsigned int colNode = i / _disc.nComp; + const unsigned int comp = i - colNode * _disc.nComp; + // + 1/Beta_c * (surfaceToVolumeRatio_{p,j}) * d_j * (k_f * [c_l - c_p]) + resCol[i] += static_cast(filmDiff[comp]) * jacCF_val * static_cast(_parTypeVolFrac[type + colNode * _disc.nParType]) + * (yCol[i] - yParType[colNode * idxr.strideParBlock(type) + (_disc.nParPoints[type] - 1) * idxr.strideParNode(type) + comp]); + } + + // Bead boundary condition is computed in residualParticle(). + + } + + return 0; +} + +parts::cell::CellParameters GeneralRateModelDG::makeCellResidualParams(unsigned int parType, int const* qsReaction) const +{ + return parts::cell::CellParameters + { + _disc.nComp, + _disc.nBound + _disc.nComp * parType, + _disc.boundOffset + _disc.nComp * parType, + _disc.strideBound[parType], + qsReaction, + _parPorosity[parType], + _poreAccessFactor.data() + _disc.nComp * parType, + _binding[parType], + (_dynReaction[parType] && (_dynReaction[parType]->numReactionsCombined() > 0)) ? _dynReaction[parType] : nullptr + }; +} +// todo sensitivities +int GeneralRateModelDG::residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode and at the same time update the + // Jacobian (in one AD run, if analytic Jacobians are disabled) + return residual(simTime, simState, nullptr, adJac, threadLocalMem, true, true); +} +// todo sensitivities, AD +int GeneralRateModelDG::residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adRes, threadLocalMem); +} +// todo sensitivities +int GeneralRateModelDG::residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3) +{ +// BENCH_SCOPE(_timerResidualSens); +// +// // tmp1 stores result of (dF / dy) * s +// // tmp2 stores result of (dF / dyDot) * sDot +// +// for (std::size_t param = 0; param < yS.size(); ++param) +// { +// // Directional derivative (dF / dy) * s +// multiplyWithJacobian(SimulationTime{0.0, 0u}, ConstSimulationState{nullptr, nullptr}, yS[param], 1.0, 0.0, tmp1); +// +// // Directional derivative (dF / dyDot) * sDot +// multiplyWithDerivativeJacobian(SimulationTime{0.0, 0u}, ConstSimulationState{nullptr, nullptr}, ySdot[param], tmp2); +// +// double* const ptrResS = resS[param]; +// +// BENCH_START(_timerResidualSensPar); +// +// // Complete sens residual is the sum: +// // TODO: Chunk TBB loop +//#ifdef CADET_PARALLELIZE +// tbb::parallel_for(std::size_t(0), static_cast(numDofs()), [&](std::size_t i) +//#else +// for (unsigned int i = 0; i < numDofs(); ++i) +//#endif +// { +// ptrResS[i] = tmp1[i] + tmp2[i] + adRes[i].getADValue(param); +// } CADET_PARFOR_END; +// +// BENCH_STOP(_timerResidualSensPar); +// } + + return 0; +} +/** + * @brief Multiplies the given vector with the system Jacobian (i.e., @f$ \frac{\partial F}{\partial y}\left(t, y, \dot{y}\right) @f$) + * @details Actually, the operation @f$ z = \alpha \frac{\partial F}{\partial y} x + \beta z @f$ is performed. + * + * Note that residual() or one of its cousins has to be called with the requested point @f$ (t, y, \dot{y}) @f$ once + * before calling multiplyWithJacobian() as this implementation ignores the given @f$ (t, y, \dot{y}) @f$. + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] yS Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] alpha Factor @f$ \alpha @f$ in front of @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] beta Factor @f$ \beta @f$ in front of @f$ z @f$ + * @param [in,out] ret Vector @f$ z @f$ which stores the result of the operation + */ +void GeneralRateModelDG::multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret) +{ + // todo: required? +} + +/** + * @brief Multiplies the time derivative Jacobian @f$ \frac{\partial F}{\partial \dot{y}}\left(t, y, \dot{y}\right) @f$ with a given vector + * @details The operation @f$ z = \frac{\partial F}{\partial \dot{y}} x @f$ is performed. + * The matrix-vector multiplication is performed matrix-free (i.e., no matrix is explicitly formed). + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] sDot Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ + * @param [out] ret Vector @f$ z @f$ which stores the result of the operation + */ +void GeneralRateModelDG::multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret) +{ + // TODO: sensitivities +} + +void GeneralRateModelDG::setExternalFunctions(IExternalFunction** extFuns, unsigned int size) +{ + for (IBindingModel* bm : _binding) + { + if (bm) + bm->setExternalFunctions(extFuns, size); + } +} + +unsigned int GeneralRateModelDG::localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT +{ + // Inlets are duplicated so need to be accounted for + if (static_cast(_convDispOpB.currentVelocity()) >= 0.0) + // Forward Flow: outlet is last cell + return _disc.nComp + (_disc.nPoints - 1) * _disc.nComp; + else + // Backward flow: Outlet is first cell + return _disc.nComp; +} + +unsigned int GeneralRateModelDG::localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT +{ + // Always 0 due to dedicated inlet DOFs + return 0; +} + +unsigned int GeneralRateModelDG::localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT +{ + return 1; +} + +unsigned int GeneralRateModelDG::localInletComponentStride(unsigned int port) const CADET_NOEXCEPT +{ + return 1; +} + +void GeneralRateModelDG::expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut) +{ + // @todo Write this function +} + +void GeneralRateModelDG::setEquidistantRadialDisc(unsigned int parType) +{ + active* const ptrCenterRadius = _parCenterRadius.data() + _disc.offsetMetric[parType]; + active* const ptrOuterSurfAreaPerVolume = _parOuterSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + active* const ptrInnerSurfAreaPerVolume = _parInnerSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + + const active radius = _parRadius[parType] - _parCoreRadius[parType]; + const active dr = radius / static_cast(_disc.nParCell[parType]); + std::fill(_parCellSize.data() + _disc.offsetMetric[parType], _parCellSize.data() + _disc.offsetMetric[parType] + _disc.nParCell[parType], dr); + + if (_parGeomSurfToVol[parType] == SurfVolRatioSphere) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + const active r_out = _parRadius[parType] - static_cast(cell) * dr; + const active r_in = _parRadius[parType] - static_cast(cell + 1) * dr; + + ptrCenterRadius[cell] = _parRadius[parType] - (0.5 + static_cast(cell)) * dr; + + // Compute denominator -> corresponding to cell volume + const active vol = pow(r_out, 3.0) - pow(r_in, 3.0); + + ptrOuterSurfAreaPerVolume[cell] = 3.0 * sqr(r_out) / vol; + ptrInnerSurfAreaPerVolume[cell] = 3.0 * sqr(r_in) / vol; + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioCylinder) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + const active r_out = _parRadius[parType] - static_cast(cell) * dr; + const active r_in = _parRadius[parType] - static_cast(cell + 1) * dr; + + ptrCenterRadius[cell] = _parRadius[parType] - (0.5 + static_cast(cell)) * dr; + + // Compute denominator -> corresponding to cell volume + const active vol = sqr(r_out) - sqr(r_in); + + ptrOuterSurfAreaPerVolume[cell] = 2.0 * r_out / vol; + ptrInnerSurfAreaPerVolume[cell] = 2.0 * r_in / vol; + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioSlab) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + const active r_out = _parRadius[parType] - static_cast(cell) * dr; + const active r_in = _parRadius[parType] - static_cast(cell + 1) * dr; + + ptrCenterRadius[cell] = _parRadius[parType] - (0.5 + static_cast(cell)) * dr; + + // Compute denominator -> corresponding to cell volume + const active vol = r_out - r_in; + + ptrOuterSurfAreaPerVolume[cell] = 1.0 / vol; + ptrInnerSurfAreaPerVolume[cell] = 1.0 / vol; + } + } +} + +// todo +/** + * @brief Computes the radial nodes in the beads in such a way that all shells have the same volume + */ +void GeneralRateModelDG::setEquivolumeRadialDisc(unsigned int parType) +{ + active* const ptrCellSize = _parCellSize.data() + _disc.offsetMetric[parType]; + active* const ptrCenterRadius = _parCenterRadius.data() + _disc.offsetMetric[parType]; + active* const ptrOuterSurfAreaPerVolume = _parOuterSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + active* const ptrInnerSurfAreaPerVolume = _parInnerSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + + if (_parGeomSurfToVol[parType] == SurfVolRatioSphere) + { + active r_out = _parRadius[parType]; + active r_in = _parCoreRadius[parType]; + const active volumePerShell = (pow(_parRadius[parType], 3.0) - pow(_parCoreRadius[parType], 3.0)) / static_cast(_disc.nParCell[parType]); + + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + if (cell != (_disc.nParCell[parType] - 1)) + r_in = pow(pow(r_out, 3.0) - volumePerShell, (1.0 / 3.0)); + else + r_in = _parCoreRadius[parType]; + + ptrCellSize[cell] = r_out - r_in; + ptrCenterRadius[cell] = (r_out + r_in) * 0.5; + + ptrOuterSurfAreaPerVolume[cell] = 3.0 * sqr(r_out) / volumePerShell; + ptrInnerSurfAreaPerVolume[cell] = 3.0 * sqr(r_in) / volumePerShell; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(r_out - r_in); + + // For the next cell: r_out == r_in of the current cell + r_out = r_in; + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioCylinder) + { + active r_out = _parRadius[parType]; + active r_in = _parCoreRadius[parType]; + const active volumePerShell = (sqr(_parRadius[parType]) - sqr(_parCoreRadius[parType])) / static_cast(_disc.nParCell[parType]); + + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + if (cell != (_disc.nParCell[parType] - 1)) + r_in = sqrt(sqr(r_out) - volumePerShell); + else + r_in = _parCoreRadius[parType]; + + ptrCellSize[cell] = r_out - r_in; + ptrCenterRadius[cell] = (r_out + r_in) * 0.5; + + ptrOuterSurfAreaPerVolume[cell] = 2.0 * r_out / volumePerShell; + ptrInnerSurfAreaPerVolume[cell] = 2.0 * r_in / volumePerShell; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(r_out - r_in); + + // For the next cell: r_out == r_in of the current cell + r_out = r_in; + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioSlab) + { + active r_out = _parRadius[parType]; + active r_in = _parCoreRadius[parType]; + const active volumePerShell = (_parRadius[parType] - _parCoreRadius[parType]) / static_cast(_disc.nParCell[parType]); + + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + if (cell != (_disc.nParCell[parType] - 1)) + r_in = r_out - volumePerShell; + else + r_in = _parCoreRadius[parType]; + + ptrCellSize[cell] = r_out - r_in; + ptrCenterRadius[cell] = (r_out + r_in) * 0.5; + + ptrOuterSurfAreaPerVolume[cell] = 1.0 / volumePerShell; + ptrInnerSurfAreaPerVolume[cell] = 1.0 / volumePerShell; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(r_out - r_in); + + // For the next cell: r_out == r_in of the current cell + r_out = r_in; + } + } +} + +// todo +/** + * @brief Computes all helper quantities for radial bead discretization from given radial cell boundaries + * @details Calculates surface areas per volume for every shell and the radial shell centers. + */ +void GeneralRateModelDG::setUserdefinedRadialDisc(unsigned int parType) +{ + active* const ptrCellSize = _parCellSize.data() + _disc.offsetMetric[parType]; + active* const ptrCenterRadius = _parCenterRadius.data() + _disc.offsetMetric[parType]; + active* const ptrOuterSurfAreaPerVolume = _parOuterSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + active* const ptrInnerSurfAreaPerVolume = _parInnerSurfAreaPerVolume.data() + _disc.offsetMetric[parType]; + + // Care for the right ordering and include 0.0 / 1.0 if not already in the vector. + std::vector orderedInterfaces = std::vector(_parDiscVector.begin() + _disc.offsetMetric[parType] + parType, + _parDiscVector.begin() + _disc.offsetMetric[parType] + parType + _disc.nParCell[parType] + 1); + + // Sort in descending order + std::sort(orderedInterfaces.begin(), orderedInterfaces.end(), std::greater()); + + // Force first and last element to be 1.0 and 0.0, respectively + orderedInterfaces[0] = 1.0; + orderedInterfaces.back() = 0.0; + + // Map [0, 1] -> [core radius, particle radius] via linear interpolation + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + orderedInterfaces[cell] = static_cast(orderedInterfaces[cell]) * (_parRadius[parType] - _parCoreRadius[parType]) + _parCoreRadius[parType]; + + if (_parGeomSurfToVol[parType] == SurfVolRatioSphere) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + ptrCellSize[cell] = orderedInterfaces[cell] - orderedInterfaces[cell + 1]; + ptrCenterRadius[cell] = (orderedInterfaces[cell] + orderedInterfaces[cell + 1]) * 0.5; + + // Compute denominator -> corresponding to cell volume + const active vol = pow(orderedInterfaces[cell], 3.0) - pow(orderedInterfaces[cell + 1], 3.0); + + ptrOuterSurfAreaPerVolume[cell] = 3.0 * sqr(orderedInterfaces[cell]) / vol; + ptrInnerSurfAreaPerVolume[cell] = 3.0 * sqr(orderedInterfaces[cell + 1]) / vol; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(ptrOuterSurfAreaPerVolume[cell] - ptrInnerSurfAreaPerVolume[cell]); + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioCylinder) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + ptrCellSize[cell] = orderedInterfaces[cell] - orderedInterfaces[cell + 1]; + ptrCenterRadius[cell] = (orderedInterfaces[cell] + orderedInterfaces[cell + 1]) * 0.5; + + // Compute denominator -> corresponding to cell volume + const active vol = sqr(orderedInterfaces[cell]) - sqr(orderedInterfaces[cell + 1]); + + ptrOuterSurfAreaPerVolume[cell] = 2.0 * orderedInterfaces[cell] / vol; + ptrInnerSurfAreaPerVolume[cell] = 2.0 * orderedInterfaces[cell + 1] / vol; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(ptrOuterSurfAreaPerVolume[cell] - ptrInnerSurfAreaPerVolume[cell]); + } + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioSlab) + { + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; ++cell) + { + ptrCellSize[cell] = orderedInterfaces[cell] - orderedInterfaces[cell + 1]; + ptrCenterRadius[cell] = (orderedInterfaces[cell] + orderedInterfaces[cell + 1]) * 0.5; + + // Compute denominator -> corresponding to cell volume + const active vol = orderedInterfaces[cell] - orderedInterfaces[cell + 1]; + + ptrOuterSurfAreaPerVolume[cell] = 1.0 / vol; + ptrInnerSurfAreaPerVolume[cell] = 1.0 / vol; + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - (cell + 1)] = static_cast(ptrOuterSurfAreaPerVolume[cell] - ptrInnerSurfAreaPerVolume[cell]); + } + } +} + +void GeneralRateModelDG::updateRadialDisc() +{ + _disc.deltaR = new double[_disc.offsetMetric[_disc.nParType]]; + + for (unsigned int parType = 0; parType < _disc.nParType; ++parType) + { + if (_parDiscType[parType] == ParticleDiscretizationMode::Equidistant) { + for (int cell = 0; cell < _disc.nParCell[parType]; cell++) { + _disc.deltaR[_disc.offsetMetric[parType] + cell] = (static_cast(_parRadius[parType]) - static_cast(_parCoreRadius[parType])) / _disc.nParCell[parType]; + } + setEquidistantRadialDisc(parType); + } + else if (_parDiscType[parType] == ParticleDiscretizationMode::Equivolume) + setEquivolumeRadialDisc(parType); + else if (_parDiscType[parType] == ParticleDiscretizationMode::UserDefined) + setUserdefinedRadialDisc(parType); + } + + /* metrics */ + // estimate cell dependent D_r + + for (int parType = 0; parType < _disc.nParType; parType++) { + + for (int cell = 0; cell < _disc.nParCell[parType]; cell++) { + + _disc.Ir[_disc.offsetMetric[parType] + cell] = VectorXd::Zero(_disc.nParNode[parType]); + + for (int node = 0; node < _disc.nParNode[parType]; node++) { + _disc.Ir[_disc.offsetMetric[parType] + cell][node] = _disc.deltaR[_disc.offsetMetric[parType] + cell] / 2.0 * (_disc.parNodes[parType][node] + 1.0); + } + + _disc.Dr[_disc.offsetMetric[parType] + cell].resize(_disc.nParNode[parType], _disc.nParNode[parType]); + _disc.Dr[_disc.offsetMetric[parType] + cell].setZero(); + + double r_L = static_cast(_parCoreRadius[parType]) + cell * _disc.deltaR[_disc.offsetMetric[parType] + cell]; // left boundary of current cell + + _disc.Ir[_disc.offsetMetric[parType] + cell] = _disc.Ir[_disc.offsetMetric[parType] + cell] + VectorXd::Ones(_disc.nParNode[parType]) * r_L; + + if (_parGeomSurfToVol[parType] == SurfVolRatioSphere) + _disc.Ir[_disc.offsetMetric[parType] + cell] = _disc.Ir[_disc.offsetMetric[parType] + cell].array().square(); + else if (_parGeomSurfToVol[parType] == SurfVolRatioSlab) + _disc.Ir[_disc.offsetMetric[parType] + cell] = VectorXd::Ones(_disc.nParNode[parType]); // no metrics for slab + + // (D_r)_{i, j} = D_{i, j} * (r_j / r_i) [only needed for inexact integration] + _disc.Dr[_disc.offsetMetric[parType] + cell] = _disc.parPolyDerM[parType]; + _disc.Dr[_disc.offsetMetric[parType] + cell].array().rowwise() *= _disc.Ir[_disc.offsetMetric[parType] + cell].array().transpose(); + _disc.Dr[_disc.offsetMetric[parType] + cell].array().colwise() *= _disc.Ir[_disc.offsetMetric[parType] + cell].array().cwiseInverse(); + + // compute mass matrices for exact integration based on particle geometry, via transformation to normalized Jacobi polynomials with weight function w + if (_parGeomSurfToVol[parType] == SurfVolRatioSphere) { // w = (1 + \xi)^2 + + _disc.parInvMM[_disc.offsetMetric[parType] + cell] = _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 2.0).inverse() * pow((_disc.deltaR[_disc.offsetMetric[parType] + cell] / 2.0), 2.0); + if(cell > 0 || _parCoreRadius[parType] != 0.0) // following contributions are zero for first cell when R_c = 0 (no particle core) + _disc.parInvMM[_disc.offsetMetric[parType] + cell] += _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 1.0).inverse() * (_disc.deltaR[_disc.offsetMetric[parType] + cell] * r_L) + + _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 0.0).inverse() * pow(r_L, 2.0); + + _disc.parInvMM[_disc.offsetMetric[parType] + cell] = _disc.parInvMM[_disc.offsetMetric[parType] + cell].inverse(); + _disc.minus_InvMM_ST[_disc.offsetMetric[parType] + cell] = - _disc.parInvMM[_disc.offsetMetric[parType] + cell] * _disc.parPolyDerM[parType].transpose() * _disc.parInvMM[_disc.offsetMetric[parType] + cell].inverse(); + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioCylinder) { // w = (1 + \xi) + + _disc.parInvMM[_disc.offsetMetric[parType] + cell] = _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 1.0).inverse() * (_disc.deltaR[_disc.offsetMetric[parType] + cell] / 2.0); + if (cell > 0 || _parCoreRadius[parType] != 0.0) // following contribution is zero for first cell when R_c = 0 (no particle core) + _disc.parInvMM[_disc.offsetMetric[parType] + cell] += _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 0.0).inverse() * r_L; + + _disc.parInvMM[_disc.offsetMetric[parType] + cell] = _disc.parInvMM[_disc.offsetMetric[parType] + cell].inverse(); + _disc.minus_InvMM_ST[_disc.offsetMetric[parType] + cell] = -_disc.parInvMM[_disc.offsetMetric[parType] + cell] * _disc.parPolyDerM[parType].transpose() * _disc.parInvMM[_disc.offsetMetric[parType] + cell].inverse(); + } + else if (_parGeomSurfToVol[parType] == SurfVolRatioSlab) { // w = 1 + + _disc.parInvMM[_disc.offsetMetric[parType] + cell] = _disc.invMMatrix(_disc.nParNode[parType], _disc.parNodes[parType], 0.0, 0.0); + } + } + } + +} + +bool GeneralRateModelDG::setParameter(const ParameterId& pId, double value) +{ + if (pId.unitOperation == _unitOpIdx) + { + if (multiplexCompTypeSecParameterValue(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, value, nullptr)) + return true; + if (multiplexCompTypeSecParameterValue(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, value, nullptr)) + return true; + if (multiplexCompTypeSecParameterValue(pId, hashString("PAR_DIFFUSION"), _parDiffusionMode, _parDiffusion, _disc.nParType, _disc.nComp, value, nullptr)) + return true; + if (multiplexBndCompTypeSecParameterValue(pId, hashString("PAR_SURFDIFFUSION"), _parSurfDiffusionMode, _parSurfDiffusion, _disc.nParType, _disc.nComp, _disc.strideBound, _disc.nBound, _disc.boundOffset, value, nullptr)) + return true; + const int mpIc = multiplexInitialConditions(pId, value, false); + if (mpIc > 0) + return true; + else if (mpIc < 0) + return false; + + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return false; + if (pId.particleType >= _disc.nParType) + return false; + + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setValue(value); + + return true; + } + + if (multiplexTypeParameterValue(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, value, nullptr)) + return true; + if (multiplexTypeParameterValue(pId, hashString("PAR_CORERADIUS"), _singleParCoreRadius, _parCoreRadius, value, nullptr)) + return true; + if (multiplexTypeParameterValue(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, value, nullptr)) + return true; + + if (model::setParameter(pId, value, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + return true; + + if (_convDispOpB.setParameter(pId, value)) + return true; + } + + const bool result = UnitOperationBase::setParameter(pId, value); + + // Check whether particle radius or core radius has changed and update radial discretization if necessary + if (result && ((pId.name == hashString("PAR_RADIUS")) || (pId.name == hashString("PAR_CORERADIUS")))) + updateRadialDisc(); + + return result; +} + +bool GeneralRateModelDG::setParameter(const ParameterId& pId, int value) +{ + if ((pId.unitOperation != _unitOpIdx) && (pId.unitOperation != UnitOpIndep)) + return false; + + if (model::setParameter(pId, value, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + return true; + + return UnitOperationBase::setParameter(pId, value); +} + +bool GeneralRateModelDG::setParameter(const ParameterId& pId, bool value) +{ + if ((pId.unitOperation != _unitOpIdx) && (pId.unitOperation != UnitOpIndep)) + return false; + + if (model::setParameter(pId, value, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + return true; + + return UnitOperationBase::setParameter(pId, value); +} + +void GeneralRateModelDG::setSensitiveParameterValue(const ParameterId& pId, double value) +{ + if (pId.unitOperation == _unitOpIdx) + { + if (multiplexCompTypeSecParameterValue(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, value, &_sensParams)) + return; + if (multiplexCompTypeSecParameterValue(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, value, &_sensParams)) + return; + if (multiplexCompTypeSecParameterValue(pId, hashString("PAR_DIFFUSION"), _parDiffusionMode, _parDiffusion, _disc.nParType, _disc.nComp, value, &_sensParams)) + return; + if (multiplexBndCompTypeSecParameterValue(pId, hashString("PAR_SURFDIFFUSION"), _parSurfDiffusionMode, _parSurfDiffusion, _disc.nParType, _disc.nComp, _disc.strideBound, _disc.nBound, _disc.boundOffset, value, &_sensParams)) + return; + if (multiplexInitialConditions(pId, value, true) != 0) + return; + + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return; + if (pId.particleType >= _disc.nParType) + return; + + if (!contains(_sensParams, &_parTypeVolFrac[pId.particleType])) + return; + + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setValue(value); + + return; + } + + if (multiplexTypeParameterValue(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, value, &_sensParams)) + return; + if (multiplexTypeParameterValue(pId, hashString("PAR_CORERADIUS"), _singleParCoreRadius, _parCoreRadius, value, &_sensParams)) + return; + if (multiplexTypeParameterValue(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, value, &_sensParams)) + return; + + if (model::setSensitiveParameterValue(pId, value, _sensParams, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + return; + + if (_convDispOpB.setSensitiveParameterValue(_sensParams, pId, value)) + return; + } + + UnitOperationBase::setSensitiveParameterValue(pId, value); + + // Check whether particle radius or core radius has changed and update radial discretization if necessary + if ((pId.name == hashString("PAR_RADIUS")) || (pId.name == hashString("PAR_CORERADIUS"))) + updateRadialDisc(); +} + +bool GeneralRateModelDG::setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue) +{ + if (pId.unitOperation == _unitOpIdx) + { + if (multiplexCompTypeSecParameterAD(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexCompTypeSecParameterAD(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexCompTypeSecParameterAD(pId, hashString("PAR_DIFFUSION"), _parDiffusionMode, _parDiffusion, _disc.nParType, _disc.nComp, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexBndCompTypeSecParameterAD(pId, hashString("PAR_SURFDIFFUSION"), _parSurfDiffusionMode, _parSurfDiffusion, _disc.nParType, _disc.nComp, _disc.strideBound, _disc.nBound, _disc.boundOffset, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + const int mpIc = multiplexInitialConditions(pId, adDirection, adValue); + if (mpIc > 0) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + else if (mpIc < 0) + return false; + + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return false; + if (pId.particleType >= _disc.nParType) + return false; + + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + + // Register parameter and set AD seed / direction + _sensParams.insert(&_parTypeVolFrac[pId.particleType]); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setADValue(adDirection, adValue); + + return true; + } + + if (multiplexTypeParameterAD(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexTypeParameterAD(pId, hashString("PAR_CORERADIUS"), _singleParCoreRadius, _parCoreRadius, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexTypeParameterAD(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (model::setSensitiveParameter(pId, adDirection, adValue, _sensParams, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + { + LOG(Debug) << "Found parameter " << pId << " in surface diffusion parameter dependence: Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (_convDispOpB.setSensitiveParameter(_sensParams, pId, adDirection, adValue)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + } + + const bool result = UnitOperationBase::setSensitiveParameter(pId, adDirection, adValue); + + // Check whether particle radius or core radius has been set active and update radial discretization if necessary + // Note that we need to recompute the radial discretization variables (_parCellSize, _parCenterRadius, _parOuterSurfAreaPerVolume, _parInnerSurfAreaPerVolume) + // because their gradient has changed (although their nominal value has not changed). + if ((pId.name == hashString("PAR_RADIUS")) || (pId.name == hashString("PAR_CORERADIUS"))) + updateRadialDisc(); + + return result; +} + +std::unordered_map GeneralRateModelDG::getAllParameterValues() const +{ + std::unordered_map data = UnitOperationBase::getAllParameterValues(); + model::getAllParameterValues(data, _parDepSurfDiffusion, _singleParDepSurfDiffusion); + + return data; +} + +double GeneralRateModelDG::getParameterDouble(const ParameterId& pId) const +{ + double val = 0.0; + if (model::getParameterDouble(pId, _parDepSurfDiffusion, _singleParDepSurfDiffusion, val)) + return val; + + // Not found + return UnitOperationBase::getParameterDouble(pId); +} + +bool GeneralRateModelDG::hasParameter(const ParameterId& pId) const +{ + if (model::hasParameter(pId, _parDepSurfDiffusion, _singleParDepSurfDiffusion)) + return true; + + return UnitOperationBase::hasParameter(pId); +} + +int GeneralRateModelDG::Exporter::writeMobilePhase(double* buffer) const +{ + const int blockSize = numMobilePhaseDofs(); + std::copy_n(_idx.c(_data), blockSize, buffer); + return blockSize; +} + +int GeneralRateModelDG::Exporter::writeSolidPhase(double* buffer) const +{ + int numWritten = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + const int n = writeParticleMobilePhase(i, buffer); + buffer += n; + numWritten += n; + } + return numWritten; +} + +int GeneralRateModelDG::Exporter::writeParticleMobilePhase(double* buffer) const +{ + int numWritten = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + const int n = writeParticleMobilePhase(i, buffer); + buffer += n; + numWritten += n; + } + return numWritten; +} + +int GeneralRateModelDG::Exporter::writeSolidPhase(unsigned int parType, double* buffer) const +{ + cadet_assert(parType < _disc.nParType); + + const unsigned int stride = _disc.nComp + _disc.strideBound[parType]; + double const* ptr = _data + _idx.offsetCp(ParticleTypeIndex{ parType }) + _disc.nComp; + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + for (unsigned int j = 0; j < _disc.nParPoints[parType]; ++j) + { + std::copy_n(ptr, _disc.strideBound[parType], buffer); + buffer += _disc.strideBound[parType]; + ptr += stride; + } + } + return _disc.nPoints * _disc.nParPoints[parType] * _disc.strideBound[parType]; +} + +int GeneralRateModelDG::Exporter::writeParticleMobilePhase(unsigned int parType, double* buffer) const +{ + cadet_assert(parType < _disc.nParType); + + const unsigned int stride = _disc.nComp + _disc.strideBound[parType]; + double const* ptr = _data + _idx.offsetCp(ParticleTypeIndex{ parType }); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + for (unsigned int j = 0; j < _disc.nParPoints[parType]; ++j) + { + std::copy_n(ptr, _disc.nComp, buffer); + buffer += _disc.nComp; + ptr += stride; + } + } + return _disc.nPoints * _disc.nParPoints[parType] * _disc.nComp; +} + +int GeneralRateModelDG::Exporter::writeParticleFlux(double* buffer) const +{ + return 0; +} + +int GeneralRateModelDG::Exporter::writeParticleFlux(unsigned int parType, double* buffer) const +{ + return 0; +} + +int GeneralRateModelDG::Exporter::writeInlet(unsigned int port, double* buffer) const +{ + cadet_assert(port == 0); + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; +} + +int GeneralRateModelDG::Exporter::writeInlet(double* buffer) const +{ + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; +} + +int GeneralRateModelDG::Exporter::writeOutlet(unsigned int port, double* buffer) const +{ + cadet_assert(port == 0); + + if (_model._convDispOpB.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; +} + +int GeneralRateModelDG::Exporter::writeOutlet(double* buffer) const +{ + if (_model._convDispOpB.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; +} + +} // namespace model + +} // namespace cadet + +#include "model/GeneralRateModelDG-InitialConditions.cpp" +#include "model/GeneralRateModelDG-LinearSolver.cpp" + +namespace cadet +{ + +namespace model +{ + +void registerGeneralRateModelDG(std::unordered_map>& models) +{ + models[GeneralRateModelDG::identifier()] = [](UnitOpIdx uoId) { return new GeneralRateModelDG(uoId); }; + models["GRM_DG"] = [](UnitOpIdx uoId) { return new GeneralRateModelDG(uoId); }; +} + +} // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/GeneralRateModelDG.hpp b/src/libcadet/model/GeneralRateModelDG.hpp new file mode 100644 index 000000000..16b6ed72e --- /dev/null +++ b/src/libcadet/model/GeneralRateModelDG.hpp @@ -0,0 +1,3860 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +/** + * @file + * Defines the general rate model (GRM). + */ + +#ifndef LIBCADET_GENERALRATEMODELDG_HPP_ +#define LIBCADET_GENERALRATEMODELDG_HPP_ + +#include "model/UnitOperationBase.hpp" +#include "model/BindingModel.hpp" +#include "cadet/StrongTypes.hpp" +#include "cadet/SolutionExporter.hpp" +#include "model/parts/ConvectionDispersionOperator.hpp" +#include "AutoDiff.hpp" +#include "linalg/BandedEigenSparseRowIterator.hpp" +#include "linalg/SparseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/Gmres.hpp" +#include "Memory.hpp" +#include "model/ModelUtils.hpp" +#include "ParameterMultiplexing.hpp" +#include +#include +#include +#include + +#include "Benchmark.hpp" + +using namespace Eigen; + +namespace cadet +{ + +namespace model +{ + +namespace parts +{ + namespace cell + { + struct CellParameters; + } +} + +class IDynamicReactionModel; +class IParameterDependence; + +/** + * @brief General rate model of liquid column chromatography + * @details See @cite Guiochon2006, @cite Gu1995, @cite Felinger2004 + * + * @f[\begin{align} + \frac{\partial c_i}{\partial t} &= - u \frac{\partial c_i}{\partial z} + D_{\text{ax},i} \frac{\partial^2 c_i}{\partial z^2} - \frac{1 - \varepsilon_c}{\varepsilon_c} \frac{3}{r_p} j_{f,i} \\ + \frac{\partial c_{p,i}}{\partial t} + \frac{1 - \varepsilon_p}{\varepsilon_p} \frac{\partial q_{i}}{\partial t} &= D_{p,i} \left( \frac{\partial^2 c_{p,i}}{\partial r^2} + \frac{2}{r} \frac{\partial c_{p,i}}{\partial r} \right) + D_{s,i} \frac{1 - \varepsilon_p}{\varepsilon_p} \left( \frac{\partial^2 q_{i}}{\partial r^2} + \frac{2}{r} \frac{\partial q_{i}}{\partial r} \right) \\ + a \frac{\partial q_i}{\partial t} &= f_{\text{iso}}(c_p, q) +\end{align} @f] +@f[ \begin{align} + j_{f,i} = k_{f,i} \left( c_i - c_{p,i} \left(\cdot, \cdot, r_p\right)\right) +\end{align} @f] + * Danckwerts boundary conditions (see @cite Danckwerts1953) +@f[ \begin{align} +u c_{\text{in},i}(t) &= u c_i(t,0) - D_{\text{ax},i} \frac{\partial c_i}{\partial z}(t,0) \\ +\frac{\partial c_i}{\partial z}(t,L) &= 0 \\ +\varepsilon_p D_{p,i} \frac{\partial c_{p,i}}{\partial r}(\cdot, \cdot, r_p) + (1-\varepsilon_p) D_{s,i} \frac{\partial q_{i}}{\partial r}(\cdot, \cdot, r_p) &= j_{f,i} \\ +\frac{\partial c_{p,i}}{\partial r}(\cdot, \cdot, 0) &= 0 +\end{align} @f] + * Methods are described in @cite Breuer2023 (DGSEM discretization), @cite Puttmann2013 @cite Puttmann2016 (forward sensitivities, AD, band compression) + */ +class GeneralRateModelDG : public UnitOperationBase +{ +public: + + GeneralRateModelDG(UnitOpIdx unitOpIdx); + virtual ~GeneralRateModelDG() CADET_NOEXCEPT; + + virtual unsigned int numDofs() const CADET_NOEXCEPT; + virtual unsigned int numPureDofs() const CADET_NOEXCEPT; + virtual bool usesAD() const CADET_NOEXCEPT; + virtual unsigned int requiredADdirs() const CADET_NOEXCEPT; + + virtual UnitOpIdx unitOperationId() const CADET_NOEXCEPT { return _unitOpIdx; } + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual void setFlowRates(active const* in, active const* out) CADET_NOEXCEPT; + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual bool canAccumulate() const CADET_NOEXCEPT { return false; } + + static const char* identifier() { return "GENERAL_RATE_MODEL_DG"; } + virtual const char* unitOperationName() const CADET_NOEXCEPT { return identifier(); } + + virtual bool configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper); + virtual bool configure(IParameterProvider& paramProvider); + virtual void notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac); + + virtual void useAnalyticJacobian(const bool analyticJac); + + virtual void reportSolution(ISolutionRecorder& recorder, double const* const solution) const; + virtual void reportSolutionStructure(ISolutionRecorder& recorder) const; + + virtual int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3); + + virtual int linearSolve(double t, double alpha, double tol, double* const rhs, double const* const weight, + const ConstSimulationState& simState); + + virtual void prepareADvectors(const AdJacobianParams& adJac) const; + + virtual void applyInitialCondition(const SimulationState& simState) const; + virtual void readInitialCondition(IParameterProvider& paramProvider); + + virtual void consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem); + + virtual void initializeSensitivityStates(const std::vector& vecSensY) const; + virtual void consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual bool hasInlet() const CADET_NOEXCEPT { return true; } + virtual bool hasOutlet() const CADET_NOEXCEPT { return true; } + + virtual unsigned int localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentStride(unsigned int port) const CADET_NOEXCEPT; + + virtual void setExternalFunctions(IExternalFunction** extFuns, unsigned int size); + virtual void setSectionTimes(double const* secTimes, bool const* secContinuity, unsigned int nSections) { } + + virtual void expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut); + + virtual void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret); + virtual void multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret); + + inline void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double* ret) + { + multiplyWithJacobian(simTime, simState, yS, 1.0, 0.0, ret); + } + + virtual bool setParameter(const ParameterId& pId, double value); + virtual bool setParameter(const ParameterId& pId, int value); + virtual bool setParameter(const ParameterId& pId, bool value); + virtual bool setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue); + virtual void setSensitiveParameterValue(const ParameterId& id, double value); + + virtual std::unordered_map getAllParameterValues() const; + virtual double getParameterDouble(const ParameterId& pId) const; + virtual bool hasParameter(const ParameterId& pId) const; + + virtual unsigned int threadLocalMemorySize() const CADET_NOEXCEPT; + +#ifdef CADET_BENCHMARK_MODE + virtual std::vector benchmarkTimings() const + { + return std::vector({ + static_cast(numDofs()), + _timerResidual.totalElapsedTime(), + _timerResidualPar.totalElapsedTime(), + _timerResidualSens.totalElapsedTime(), + _timerResidualSensPar.totalElapsedTime(), + _timerJacobianPar.totalElapsedTime(), + _timerConsistentInit.totalElapsedTime(), + _timerConsistentInitPar.totalElapsedTime(), + _timerLinearSolve.totalElapsedTime(), + _timerFactorize.totalElapsedTime(), + _timerFactorizePar.totalElapsedTime(), + _timerMatVec.totalElapsedTime(), + _timerGmres.totalElapsedTime(), + static_cast(_gmres.numIterations()) + }); + } + + virtual char const* const* benchmarkDescriptions() const + { + static const char* const desc[] = { + "DOFs", + "Residual", + "ResidualPar", + "ResidualSens", + "ResidualSensPar", + "JacobianPar", + "ConsistentInit", + "ConsistentInitPar", + "LinearSolve", + "Factorize", + "FactorizePar", + "MatVec", + "Gmres", + "NumGMRESIter" + }; + return desc; + } +#endif + +protected: + + class Indexer; + + int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity); + + template + int residualImpl(double t, unsigned int secIdx, StateType const* const y, double const* const yDot, ResidualType* const res, util::ThreadLocalStorage& threadLocalMem); + + template + int residualBulk(double t, unsigned int secIdx, StateType const* y, double const* yDot, ResidualType* res, util::ThreadLocalStorage& threadLocalMem); + + template + int residualParticle(double t, unsigned int parType, unsigned int colCell, unsigned int secIdx, StateType const* y, double const* yDot, ResidualType* res, util::ThreadLocalStorage& threadLocalMem); + + template + int residualFlux(double t, unsigned int secIdx, StateType const* y, double const* yDot, ResidualType* res); + + void extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset); + + void assembleDiscretizedGlobalJacobian(double alpha, Indexer idxr); + + void setEquidistantRadialDisc(unsigned int parType); + void setEquivolumeRadialDisc(unsigned int parType); + void setUserdefinedRadialDisc(unsigned int parType); + void updateRadialDisc(); + + void addTimeDerivativeToJacobianParticleShell(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, unsigned int parType); + + unsigned int numAdDirsForJacobian() const CADET_NOEXCEPT; + + int multiplexInitialConditions(const cadet::ParameterId& pId, unsigned int adDirection, double adValue); + int multiplexInitialConditions(const cadet::ParameterId& pId, double val, bool checkSens); + + void clearParDepSurfDiffusion(); + + parts::cell::CellParameters makeCellResidualParams(unsigned int parType, int const* qsReaction) const; + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + void checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const; +#endif + + struct Discretization + { + unsigned int nComp; //!< Number of components + unsigned int nCol; //!< Number of column cells + unsigned int polyDeg; //!< polynomial degree of column elements + unsigned int nNodes; //!< Number of nodes per column cell + unsigned int nPoints; //!< Number of discrete column Points + bool exactInt; //!< 1 for exact integration, 0 for inexact LGL quadrature + unsigned int nParType; //!< Number of particle types + unsigned int* nParCell; //!< Array with number of radial cells in each particle type + unsigned int* nParPointsBeforeType; //!< Array with total number of radial points before a particle type (cumulative sum of nParPoints), additional last element contains total number of particle shells + unsigned int* parPolyDeg; //!< polynomial degree of particle elements + unsigned int* nParNode; //!< Array with number of radial nodes per cell in each particle type + unsigned int* nParPoints; //!< Array with number of radial nodes per cell in each particle type + bool* parExactInt; //!< 1 for exact integration, 0 for inexact LGL quadrature for each particle type + unsigned int* parTypeOffset; //!< Array with offsets (in particle block) to particle type, additional last element contains total number of particle DOFs + unsigned int* nBound; //!< Array with number of bound states for each component and particle type (particle type major ordering) + unsigned int* boundOffset; //!< Array with offset to the first bound state of each component in the solid phase (particle type major ordering) + unsigned int* strideBound; //!< Total number of bound states for each particle type, additional last element contains total number of bound states for all types + unsigned int* nBoundBeforeType; //!< Array with number of bound states before a particle type (cumulative sum of strideBound) + + bool newStaticJac; //!< determines wether static analytical jacobian is to be computed + + // parameter + Eigen::VectorXd dispersion; //!< Column dispersion (may be section and component dependent) + unsigned int* offsetSurfDiff; //!< particle surface diffusion (may be section and component dependent) + bool _dispersionCompIndep; //!< Determines whether dispersion is component independent + double velocity; //!< Interstitial velocity (may be section dependent) \f$ u \f$ + double crossSection; //!< Cross section area + int curSection; //!< current time section index + + double colLength; //!< column length + double porosity; //!< column porosity + std::vector isKinetic; + + const double SurfVolRatioSlab = 1.0; //!< Surface to volume ratio for a slab-shaped particle + const double SurfVolRatioCylinder = 2.0; //!< Surface to volume ratio for a cylindrical particle + const double SurfVolRatioSphere = 3.0; //!< Surface to volume ratio for a spherical particle + + /* DG specifics */ + + double deltaZ; //!< equidistant column spacing + double* deltaR; //!< equidistant particle element spacing for each particle type + Eigen::VectorXd nodes; //!< Array with positions of nodes in reference element + Eigen::MatrixXd polyDerM; //!< Array with polynomial derivative Matrix + Eigen::VectorXd invWeights; //!< Array with weights for numerical quadrature of size nNodes + Eigen::MatrixXd invMM; //!< dense inverse mass matrix for exact integration + Eigen::VectorXd* parNodes; //!< Array with positions of nodes in radial reference element for each particle + Eigen::MatrixXd* parPolyDerM; //!< Array with polynomial derivative Matrix for each particle + Eigen::MatrixXd* minus_InvMM_ST; //!< equals minus inverse mass matrix times transposed stiffness matrix. Required solely for exact integration DG discretization of particle equation + Eigen::VectorXd* parInvWeights; //!< Array with weights for LGL quadrature of size nNodes for each particle + Eigen::MatrixXd* parInvMM; //!< dense inverse mass matrix for exact integration of integrals with metrics, for each particle + Eigen::MatrixXd* parInvMM_Leg; //!< dense inverse mass matrix (Legendre) for exact integration of integral without metric, for each particle + Eigen::VectorXd* Ir; //!< metric part for each particle type and cell, particle type major ordering + Eigen::MatrixXd* Dr; //!< derivative matrices including metrics for each particle type and cell, particle type major ordering + Eigen::VectorXi offsetMetric; //!< offset required to access metric dependent DG operator storage of Ir, Dr -> summed up nCells of all previous parTypes + + Eigen::MatrixXd* DGjacAxDispBlocks; //!< axial dispersion blocks of DG jacobian (unique blocks only) + Eigen::MatrixXd DGjacAxConvBlock; //!< axial convection block of DG jacobian + Eigen::MatrixXd* DGjacParDispBlocks; //!< particle dispersion blocks of DG jacobian + + Eigen::VectorXd g; //!< auxiliary variable g = dc / dx + Eigen::VectorXd* g_p; //!< auxiliary variable g = dc_p / dr + Eigen::VectorXd* g_pSum; //!< auxiliary variable g = sum_{k \in p, s_i} dc_k / dr + Eigen::VectorXd h; //!< auxiliary variable h = vc - D_ax g + Eigen::VectorXd surfaceFlux; //!< stores the surface flux values of the bulk phase + Eigen::VectorXd* surfaceFluxParticle; //!< stores the surface flux values for each particle + Eigen::Vector4d boundary; //!< stores the boundary values from Danckwert boundary conditions of the bulk phase + double* localFlux; //!< stores the local (at respective particle) film diffusion flux + + /** + * @brief allocates memory for DG operators and computes those that are metric independent. Also allocates required containers needed for the DG discretization. + */ + void initializeDG() { + + nNodes = polyDeg + 1; + nPoints = nNodes * nCol; + + /* Allocate space for DG operators and containers */ + + // bulk + nodes.resize(nNodes); + nodes.setZero(); + invWeights.resize(nNodes); + invWeights.setZero(); + polyDerM.resize(nNodes, nNodes); + polyDerM.setZero(); + invMM.resize(nNodes, nNodes); + invMM.setZero(); + g.resize(nPoints); + g.setZero(); + h.resize(nPoints); + h.setZero(); + boundary.setZero(); + surfaceFlux.resize(nCol + 1); + surfaceFlux.setZero(); + newStaticJac = true; + + // particles + nParNode = new unsigned int [nParType]; + nParPoints = new unsigned int [nParType]; + g_p = new VectorXd [nParType]; + g_pSum = new VectorXd [nParType]; + surfaceFluxParticle = new VectorXd [nParType]; + parNodes = new VectorXd [nParType]; + parInvWeights = new VectorXd [nParType]; + parInvMM_Leg = new MatrixXd [nParType]; + parPolyDerM = new MatrixXd[nParType]; + localFlux = new double[nComp]; + + for (int parType = 0; parType < nParType; parType++) + { + nParNode[parType] = parPolyDeg[parType] + 1u; + nParPoints[parType] = nParNode[parType] * nParCell[parType]; + g_p[parType].resize(nParPoints[parType]); + g_p[parType].setZero(); + g_pSum[parType].resize(nParPoints[parType]); + g_pSum[parType].setZero(); + surfaceFluxParticle[parType].resize(nParCell[parType] + 1); + surfaceFluxParticle[parType].setZero(); + parNodes[parType].resize(nParNode[parType]); + parNodes[parType].setZero(); + parInvWeights[parType].resize(nParNode[parType]); + parInvWeights[parType].setZero(); + parPolyDerM[parType].resize(nParNode[parType], nParNode[parType]); + parPolyDerM[parType].setZero(); + parInvMM_Leg[parType].resize(nParNode[parType], nParNode[parType]); + parInvMM_Leg[parType].setZero(); + } + + offsetMetric = VectorXi::Zero(nParType + 1); + for (int parType = 1; parType <= nParType; parType++) { + offsetMetric[parType] += nParCell[parType - 1]; + } + Dr = new MatrixXd[offsetMetric[nParType]]; + Ir = new VectorXd[offsetMetric[nParType]]; + minus_InvMM_ST = new MatrixXd[offsetMetric[nParType]]; + parInvMM = new MatrixXd[offsetMetric[nParType]]; + + /* compute metric independent DG operators for bulk and particles. Note that metric dependent DG operators are computet in updateRadialDisc(). */ + + lglNodesWeights(polyDeg, nodes, invWeights); + invMM = invMMatrix(nNodes, nodes, 0.0, 0.0); + derivativeMatrix(polyDeg, polyDerM, nodes); + + for (int parType = 0; parType < nParType; parType++) + { + lglNodesWeights(parPolyDeg[parType], parNodes[parType], parInvWeights[parType]); + derivativeMatrix(parPolyDeg[parType], parPolyDerM[parType], parNodes[parType]); + parInvMM_Leg[parType] = invMMatrix(nParNode[parType], parNodes[parType], 0.0, 0.0); + } + } + + void initializeDGjac(std::vector parGeomSurfToVol) { + + // unique bulk jacobian blocks + DGjacAxDispBlocks = new MatrixXd[(exactInt ? std::min(nCol, 5u) : std::min(nCol, 3u))]; + // we only need unique dispersion blocks, which are given by cells 1, 2, nCol for inexact integration DG and by cells 1, 2, 3, nCol-1, nCol for eaxct integration DG + DGjacAxDispBlocks[0] = DGjacobianAxDispBlock(1); + if (nCol > 1) + DGjacAxDispBlocks[1] = DGjacobianAxDispBlock(2); + if (nCol > 2 && exactInt) + DGjacAxDispBlocks[2] = DGjacobianAxDispBlock(3); + else if (nCol > 2 && !exactInt) + DGjacAxDispBlocks[2] = DGjacobianAxDispBlock(nCol); + if (exactInt && nCol > 3) + DGjacAxDispBlocks[3] = DGjacobianAxDispBlock(std::max(4u, nCol - 1u)); + if (exactInt && nCol > 4) + DGjacAxDispBlocks[4] = DGjacobianAxDispBlock(nCol); + + DGjacAxConvBlock = DGjacobianAxConvBlock(); + + // particle jacobian blocks (each is unique) + DGjacParDispBlocks = new MatrixXd[std::accumulate(nParCell, nParCell + nParType, 0)]; + + for (unsigned int type = 0; type < nParType; type++) { + for (unsigned int block = 0; block < nParCell[type]; block++) { + DGjacParDispBlocks[offsetMetric[type] + block] = DGjacobianParDispBlock(block + 1u, type, parGeomSurfToVol[type]); + } + } + } + + private: + + /* =================================================================================== + * Polynomial Basis operators and auxiliary functions + * =================================================================================== */ + + /** + * @brief computes the Legendre polynomial L_N and q = L_N+1 - L_N-2 and q' at point x + * @param [in] polyDeg polynomial degree of spatial Discretization + * @param [in] x evaluation point + * @param [in] L <- L(x) + * @param [in] q <- q(x) = L_N+1 (x) - L_N-2(x) + * @param [in] qder <- q'(x) = [L_N+1 (x) - L_N-2(x)]' + */ + void qAndL(const int _polyDeg, const double x, double& L, double& q, double& qder) { + + double L_2 = 1.0; + double L_1 = x; + double Lder_2 = 0.0; + double Lder_1 = 1.0; + double Lder = 0.0; + + for (double k = 2; k <= _polyDeg; k++) { // note that this function is only called for polyDeg >= 2. + L = ((2 * k - 1) * x * L_1 - (k - 1) * L_2) / k; + Lder = Lder_2 + (2 * k - 1) * L_1; + L_2 = L_1; + L_1 = L; + Lder_2 = Lder_1; + Lder_1 = Lder; + } + q = ((2.0 * _polyDeg + 1) * x * L - _polyDeg * L_2) / (_polyDeg + 1.0) - L_2; + qder = Lder_1 + (2.0 * _polyDeg + 1) * L_1 - Lder_2; + } + + /** + * @brief computes the Legendre-Gauss-Lobatto nodes and (inverse) quadrature weights + * @detail inexact LGL-quadrature leads to a diagonal mass matrix (mass lumping), defined by the quadrature weights + */ + void lglNodesWeights(const int _polyDeg, Eigen::VectorXd& _nodes, Eigen::VectorXd& _invWeights) { + + const double pi = 3.1415926535897932384626434; + + // tolerance and max #iterations for Newton iteration + int nIterations = 10; + double tolerance = 1e-15; + + // Legendre polynomial and derivative + double L = 0; + double q = 0; + double qder = 0; + + switch (_polyDeg) { + case 0: + throw std::invalid_argument("Polynomial degree must be at least 1 !"); + break; + case 1: + _nodes[0] = -1; + _invWeights[0] = 1; + _nodes[1] = 1; + _invWeights[1] = 1; + break; + default: + _nodes[0] = -1; + _nodes[_polyDeg] = 1; + _invWeights[0] = 2.0 / (_polyDeg * (_polyDeg + 1.0)); + _invWeights[_polyDeg] = _invWeights[0]; + + // use symmetrie, only compute half of points and weights + for (unsigned int j = 1; j <= floor((_polyDeg + 1) / 2) - 1; j++) { + // first guess for Newton iteration + _nodes[j] = -cos(pi * (j + 0.25) / _polyDeg - 3 / (8.0 * _polyDeg * pi * (j + 0.25))); + // Newton iteration to find roots of Legendre Polynomial + for (unsigned int k = 0; k <= nIterations; k++) { + qAndL(_polyDeg, _nodes[j], L, q, qder); + _nodes[j] = _nodes[j] - q / qder; + if (abs(q / qder) <= tolerance * abs(_nodes[j])) { + break; + } + } + // calculate weights + qAndL(_polyDeg, _nodes[j], L, q, qder); + _invWeights[j] = 2.0 / (_polyDeg * (_polyDeg + 1.0) * pow(L, 2.0)); + _nodes[_polyDeg - j] = -_nodes[j]; // copy to second half of points and weights + _invWeights[_polyDeg - j] = _invWeights[j]; + } + } + + if (_polyDeg % 2 == 0) { // for even polyDeg we have an odd number of points which include 0.0 + qAndL(_polyDeg, 0.0, L, q, qder); + _nodes[_polyDeg / 2] = 0; + _invWeights[_polyDeg / 2] = 2.0 / (_polyDeg * (_polyDeg + 1.0) * pow(L, 2.0)); + } + + _invWeights = _invWeights.cwiseInverse(); // we need the inverse of the weights + } + + /** + * @brief computation of barycentric weights for fast polynomial evaluation + * @param [in] baryWeights vector to store barycentric weights. Must already be initialized with ones! + */ + void barycentricWeights(Eigen::VectorXd& baryWeights, const Eigen::VectorXd& _nodes, const int _polyDeg) { + + for (unsigned int j = 1; j <= _polyDeg; j++) { + for (unsigned int k = 0; k <= j - 1; k++) { + baryWeights[k] = baryWeights[k] * (_nodes[k] - _nodes[j]) * 1.0; + baryWeights[j] = baryWeights[j] * (_nodes[j] - _nodes[k]) * 1.0; + } + } + + for (unsigned int j = 0; j <= _polyDeg; j++) { + baryWeights[j] = 1 / baryWeights[j]; + } + } + + /** + * @brief computation of nodal (lagrange) polynomial derivative matrix + */ + void derivativeMatrix(const int _polyDeg, Eigen::MatrixXd& _polyDerM, const Eigen::VectorXd& _nodes) { + + Eigen::VectorXd baryWeights = Eigen::VectorXd::Ones(_polyDeg + 1u); + barycentricWeights(baryWeights, _nodes, _polyDeg); + + for (unsigned int i = 0; i <= _polyDeg; i++) { + for (unsigned int j = 0; j <= _polyDeg; j++) { + if (i != j) { + _polyDerM(i, j) = baryWeights[j] / (baryWeights[i] * (_nodes[i] - _nodes[j])); + _polyDerM(i, i) += -_polyDerM(i, j); + } + } + } + } + + /** + * @brief factor to normalize legendre polynomials + */ + double orthonFactor(const int _polyDeg, double a, double b) { + + double n = static_cast (_polyDeg); + return std::sqrt(((2.0 * n + a + b + 1.0) * std::tgamma(n + 1.0) * std::tgamma(n + a + b + 1.0)) + / (std::pow(2.0, a + b + 1.0) * std::tgamma(n + a + 1.0) * std::tgamma(n + b + 1.0))); + } + /** + * @brief calculates the Vandermonde matrix of the normalized legendre polynomials + */ + Eigen::MatrixXd getVandermonde_JACOBI(const int _nNodes, const Eigen::VectorXd _nodes, double a, double b) { + + Eigen::MatrixXd V(_nNodes, _nNodes); + + // degree 0 + V.block(0, 0, _nNodes, 1) = VectorXd::Ones(_nNodes) * orthonFactor(0, a, b); + // degree 1 + for (int node = 0; node < static_cast(_nNodes); node++) { + V(node, 1) = ((_nodes[node] - 1.0) / 2.0 * (a + b + 2.0) + (a + 1.0)) * orthonFactor(1, a, b); + } + + for (int deg = 2; deg <= static_cast(_nNodes - 1); deg++) { + + for (int node = 0; node < static_cast(_nNodes); node++) { + + double orthn_1 = orthonFactor(deg, a, b) / orthonFactor(deg - 1, a, b); + double orthn_2 = orthonFactor(deg, a, b) / orthonFactor(deg - 2, a, b); + + // recurrence relation + V(node, deg) = orthn_1 * ((2.0 * deg + a + b - 1.0) * ((2.0 * deg + a + b) * (2.0 * deg + a + b - 2.0) * _nodes[node] + a * a - b * b) * V(node, deg - 1)); + V(node, deg) -= orthn_2 * (2.0 * (deg + a - 1.0) * (deg + b - 1.0) * (2.0 * deg + a + b) * V(node, deg - 2)); + V(node, deg) /= 2.0 * deg * (deg + a + b) * (2.0 * deg + a + b - 2.0); + } + } + + return V; + } + /** + * @brief calculates the convection part of the DG jacobian + */ + MatrixXd DGjacobianAxConvBlock() { + + // Convection block [ d RHS_conv / d c ], additionally depends on upwind flux part from corresponding neighbour cell + MatrixXd convBlock = MatrixXd::Zero(nNodes, nNodes + 1); + + if (velocity >= 0.0) { // forward flow -> Convection block additionally depends on last entry of previous cell + convBlock.block(0, 1, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, 0, nNodes, 1) += invMM.block(0, 0, nNodes, 1); + convBlock.block(0, 1, nNodes, 1) -= invMM.block(0, 0, nNodes, 1); + } + else { + convBlock(0, 0) += invWeights[0]; + convBlock(0, 1) -= invWeights[0]; + } + } + else { // backward flow -> Convection block additionally depends on first entry of subsequent cell + convBlock.block(0, 0, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, nNodes - 1, nNodes, 1) += invMM.block(0, nNodes - 1, nNodes, 1); + convBlock.block(0, nNodes, nNodes, 1) -= invMM.block(0, nNodes - 1, nNodes, 1); + } + else { + convBlock(nNodes - 1, nNodes - 1) += invWeights[nNodes - 1]; + convBlock(nNodes - 1, nNodes) -= invWeights[nNodes - 1]; + } + } + convBlock *= 2 / deltaZ; + + return -convBlock; // *-1 for residual + } + /** + * @brief calculates the DG Jacobian auxiliary block + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd getGBlock(unsigned int cellIdx) { + + // Auxiliary Block [ d g(c) / d c ], additionally depends on boundary entries of neighbouring cells + MatrixXd gBlock = MatrixXd::Zero(nNodes, nNodes + 2); + gBlock.block(0, 1, nNodes, nNodes) = polyDerM; + if (exactInt) { + if (cellIdx != 1 && cellIdx != nCol) { // inner cells + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == 1) { // left boundary cell + if (cellIdx == nCol) + return gBlock * 2 / deltaZ; + ; + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == nCol) { // right boundary cell + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + } + else if (cellIdx == 0 || cellIdx == nCol + 1) { // cellIdx out of bounds + gBlock.setZero(); + } + gBlock *= 2 / deltaZ; + } + else { + if (cellIdx == 0 || cellIdx == nCol + 1) + return MatrixXd::Zero(nNodes, nNodes + 2); + + gBlock(0, 0) -= 0.5 * invWeights[0]; + gBlock(0, 1) += 0.5 * invWeights[0]; + gBlock(nNodes - 1, nNodes) -= 0.5 * invWeights[nNodes - 1]; + gBlock(nNodes - 1, nNodes + 1) += 0.5 * invWeights[nNodes - 1]; + gBlock *= 2 / deltaZ; + + if (cellIdx == 1) { + // adjust auxiliary Block [ d g(c) / d c ] for left boundary cell + gBlock(0, 1) -= 0.5 * invWeights[0] * 2 / deltaZ; + if (cellIdx == nCol) { // adjust for special case one cell + gBlock(0, 0) += 0.5 * invWeights[0] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes + 1) -= 0.5 * invWeights[nNodes - 1] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + else if (cellIdx == nCol) { + // adjust auxiliary Block [ d g(c) / d c ] for right boundary cell + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + + return gBlock; + } + /** + * @brief calculates the num. flux part of a dispersion DG Jacobian block + * @param [in] cellIdx cell index + * @param [in] leftG left neighbour auxiliary block + * @param [in] middleG neighbour auxiliary block + * @param [in] rightG neighbour auxiliary block + */ + Eigen::MatrixXd auxBlockGstar(unsigned int cellIdx, MatrixXd leftG, MatrixXd middleG, MatrixXd rightG) { + + // auxiliary block [ d g^* / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + MatrixXd gStarDC = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + // NOTE: N = polyDeg + // indices gStarDC : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),... , -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + // auxiliary block [d g^* / d c] + if (cellIdx != 1) { + gStarDC.block(0, nNodes, 1, nNodes + 2) += middleG.block(0, 0, 1, nNodes + 2); + gStarDC.block(0, 0, 1, nNodes + 2) += leftG.block(nNodes - 1, 0, 1, nNodes + 2); + } + if (cellIdx != nCol) { + gStarDC.block(nNodes - 1, nNodes, 1, nNodes + 2) += middleG.block(nNodes - 1, 0, 1, nNodes + 2); + gStarDC.block(nNodes - 1, 2 * nNodes, 1, nNodes + 2) += rightG.block(0, 0, 1, nNodes + 2); + } + gStarDC *= 0.5; + + return gStarDC; + } + + Eigen::MatrixXd getBMatrix() { + + MatrixXd B = MatrixXd::Zero(nNodes, nNodes); + B(0, 0) = -1.0; + B(nNodes - 1, nNodes - 1) = 1.0; + + return B; + } + /** + * @brief calculates the dispersion part of the DG jacobian + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd DGjacobianAxDispBlock(unsigned int cellIdx) { + + int offC = 0; // inlet DOFs not included in Jacobian + + MatrixXd dispBlock; + + if (exactInt) { + + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + + MatrixXd B = getBMatrix(); // "Lifting" matrix + MatrixXd gBlock = getGBlock(cellIdx); // current cell auxiliary block matrix + MatrixXd gStarDC = auxBlockGstar(cellIdx, getGBlock(cellIdx - 1), gBlock, getGBlock(cellIdx + 1)); // Numerical flux block + + // indices dispBlock : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),..., -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + dispBlock.block(0, nNodes, nNodes, nNodes + 2) += polyDerM * gBlock - invMM * B * gBlock; + dispBlock += invMM * B * gStarDC; + dispBlock *= 2 / deltaZ; + } + else { // inexact integration collocation DGSEM + + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes); + MatrixXd GBlockLeft = getGBlock(cellIdx - 1); + MatrixXd GBlock = getGBlock(cellIdx); + MatrixXd GBlockRight = getGBlock(cellIdx + 1); + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + // NOTE: N = polyDeg + // cell indices : 0 , ..., nNodes - 1; nNodes, ..., 2 * nNodes - 1; 2 * nNodes, ..., 3 * nNodes - 1 + // j : -N-1, ..., -1 ; 0 , ..., N ; N + 1, ..., 2N + 1 + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) = polyDerM * GBlock; + + if (cellIdx > 1) { + dispBlock(0, nNodes - 1) += -invWeights[0] * (-0.5 * GBlock(0, 0) + 0.5 * GBlockLeft(nNodes - 1, nNodes)); // G_N,N i=0, j=-1 + dispBlock(0, nNodes) += -invWeights[0] * (-0.5 * GBlock(0, 1) + 0.5 * GBlockLeft(nNodes - 1, nNodes + 1)); // G_N,N+1 i=0, j=0 + dispBlock.block(0, nNodes + 1, 1, nNodes) += -invWeights[0] * (-0.5 * GBlock.block(0, 2, 1, nNodes)); // G_i,j i=0, j=1,...,N+1 + dispBlock.block(0, 0, 1, nNodes - 1) += -invWeights[0] * (0.5 * GBlockLeft.block(nNodes - 1, 1, 1, nNodes - 1)); // G_N,j+N+1 i=0, j=-N-1,...,-2 + } + else if (cellIdx == 1) { // left boundary cell + dispBlock.block(0, nNodes - 1, 1, nNodes + 2) += -invWeights[0] * (-GBlock.block(0, 0, 1, nNodes + 2)); // G_N,N i=0, j=-1,...,N+1 + } + if (cellIdx < nCol) { + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock.block(nNodes - 1, 0, 1, nNodes)); // G_i,j+N+1 i=N, j=-1,...,N-1 + dispBlock(nNodes - 1, 2 * nNodes - 1) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes) + 0.5 * GBlockRight(0, 0)); // G_i,j i=N, j=N + dispBlock(nNodes - 1, 2 * nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes + 1) + 0.5 * GBlockRight(0, 1)); // G_i,j i=N, j=N+1 + dispBlock.block(nNodes - 1, 2 * nNodes + 1, 1, nNodes - 1) += invWeights[nNodes - 1] * (0.5 * GBlockRight.block(0, 2, 1, nNodes - 1)); // G_0,j-N-1 i=N, j=N+2,...,2N+1 + } + else if (cellIdx == nCol) { // right boundary cell + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes + 2) += invWeights[nNodes - 1] * (-GBlock.block(nNodes - 1, 0, 1, nNodes + 2)); // G_i,j+N+1 i=N, j=--1,...,N+1 + } + + dispBlock *= 2 / deltaZ; + } + + return -dispBlock; // *-1 for residual + } + /** + * @brief calculates the DG Jacobian auxiliary block + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd getParGBlock(unsigned int cellIdx, unsigned int parType) { + + // Auxiliary Block [ d g(c) / d c ], additionally depends on boundary entries of neighbouring cells + MatrixXd gBlock = MatrixXd::Zero(nParNode[parType], nParNode[parType] + 2); + gBlock.block(0, 1, nParNode[parType], nParNode[parType]) = parPolyDerM[parType]; + if (parExactInt[parType]) { + if (cellIdx == 0 || cellIdx == nParCell[parType] + 1) { // cellIdx out of bounds + return MatrixXd::Zero(nParNode[parType], nParNode[parType] + 2); + } + if (cellIdx != 1 && cellIdx != nParCell[parType]) { // inner cell + gBlock.block(0, 0, nParNode[parType], 1) -= 0.5 * parInvMM_Leg[parType].block(0, 0, nParNode[parType], 1); + gBlock.block(0, 1, nParNode[parType], 1) += 0.5 * parInvMM_Leg[parType].block(0, 0, nParNode[parType], 1); + gBlock.block(0, nParNode[parType], nParNode[parType], 1) -= 0.5 * parInvMM_Leg[parType].block(0, nParNode[parType] - 1, nParNode[parType], 1); + gBlock.block(0, nParNode[parType] + 1, nParNode[parType], 1) += 0.5 * parInvMM_Leg[parType].block(0, nParNode[parType] - 1, nParNode[parType], 1); + } + else if (cellIdx == 1u) { // left boundary cell + if (cellIdx == nParCell[parType]) // special case one cell + return gBlock * 2.0 / deltaR[offsetMetric[parType] + (cellIdx - 1)]; + gBlock.block(0, nParNode[parType], nParNode[parType], 1) -= 0.5 * parInvMM_Leg[parType].block(0, nParNode[parType] - 1, nParNode[parType], 1); + gBlock.block(0, nParNode[parType] + 1, nParNode[parType], 1) += 0.5 * parInvMM_Leg[parType].block(0, nParNode[parType] - 1, nParNode[parType], 1); + } + else if (cellIdx == nParCell[parType]) { // right boundary cell + gBlock.block(0, 0, nParNode[parType], 1) -= 0.5 * parInvMM_Leg[parType].block(0, 0, nParNode[parType], 1); + gBlock.block(0, 1, nParNode[parType], 1) += 0.5 * parInvMM_Leg[parType].block(0, 0, nParNode[parType], 1); + } + gBlock *= 2.0 / deltaR[offsetMetric[parType] + (cellIdx - 1)]; + } + else { + + } + + return gBlock; + } + /** + * @brief calculates the num. flux part of a dispersion DG Jacobian block + * @param [in] cellIdx cell index + * @param [in] leftG left neighbour auxiliary block + * @param [in] middleG neighbour auxiliary block + * @param [in] rightG neighbour auxiliary block + */ + Eigen::MatrixXd parAuxBlockGstar(unsigned int cellIdx, unsigned int parType, MatrixXd leftG, MatrixXd middleG, MatrixXd rightG) { + + // auxiliary block [ d g^* / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + MatrixXd gStarDC = MatrixXd::Zero(nParNode[parType], 3 * nParNode[parType] + 2); + // NOTE: N = polyDeg + // indices gStarDC : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),... , -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + // auxiliary block [d g^* / d c] + if (cellIdx != 1) { + gStarDC.block(0, nParNode[parType], 1, nParNode[parType] + 2) += middleG.block(0, 0, 1, nParNode[parType] + 2); + gStarDC.block(0, 0, 1, nParNode[parType] + 2) += leftG.block(nParNode[parType] - 1, 0, 1, nParNode[parType] + 2); + } + if (cellIdx != nParCell[parType]) { + gStarDC.block(nParNode[parType] - 1, nParNode[parType], 1, nParNode[parType] + 2) += middleG.block(nParNode[parType] - 1, 0, 1, nParNode[parType] + 2); + gStarDC.block(nParNode[parType] - 1, 2 * nParNode[parType], 1, nParNode[parType] + 2) += rightG.block(0, 0, 1, nParNode[parType] + 2); + } + gStarDC *= 0.5; + + return gStarDC; + } + + Eigen::MatrixXd getParBMatrix(int parType, int cell, double parGeomSurfToVol) { + // also known as "lifting" matrix and includes metric dependent terms for particle discretization + MatrixXd B = MatrixXd::Zero(nParNode[parType], nParNode[parType]); + if (parGeomSurfToVol == SurfVolRatioSlab) { + B(0, 0) = -1.0; + B(nParNode[parType] - 1, nParNode[parType] - 1) = 1.0; + } + else { + B(0, 0) = -Ir[offsetMetric[parType] + (cell - 1)][0]; + B(nParNode[parType] - 1, nParNode[parType] - 1) = Ir[offsetMetric[parType] + (cell - 1)][nParNode[parType] - 1]; + } + + return B; + } + /** + * @brief calculates the dispersion part of the DG jacobian + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + Eigen::MatrixXd DGjacobianParDispBlock(unsigned int cellIdx, unsigned int parType, double parGeomSurfToVol) { + + MatrixXd dispBlock; + + if (parExactInt[parType]) { + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + dispBlock = MatrixXd::Zero(nParNode[parType], 3 * nParNode[parType] + 2); + MatrixXd B = getParBMatrix(parType, cellIdx, parGeomSurfToVol); // "Lifting" matrix + MatrixXd gBlock = getParGBlock(cellIdx, parType); // current cell auxiliary block matrix + MatrixXd gStarDC = parAuxBlockGstar(cellIdx, parType, getParGBlock(cellIdx - 1, parType), gBlock, getParGBlock(cellIdx + 1, parType)); // Numerical flux block + + if (parGeomSurfToVol != SurfVolRatioSlab) { + dispBlock.block(0, nParNode[parType], nParNode[parType], nParNode[parType] + 2) = minus_InvMM_ST[offsetMetric[parType] + (cellIdx - 1)] * gBlock; + dispBlock += parInvMM[offsetMetric[parType] + (cellIdx - 1)] * B * gStarDC; + } + else { + dispBlock.block(0, nParNode[parType], nParNode[parType], nParNode[parType] + 2) = (parPolyDerM[parType] - parInvMM[parType] * B) * gBlock; + dispBlock += parInvMM[parType] * B * gStarDC; + } + dispBlock *= 2.0 / deltaR[offsetMetric[parType] + (cellIdx - 1)]; + } + else { + // inexact integration collocation DGSEM deprecated here + } + + return -dispBlock; // *-1 for residual + } + public: + /** + * @brief calculates the inverse mass matrix for exact integration + * @detail calculation via transformation of Lagrange to Jacobi polynomial coefficients + */ + MatrixXd invMMatrix(const int _nnodes, const Eigen::VectorXd _nodes, double alpha, double beta) { + return (getVandermonde_JACOBI(_nnodes, _nodes, alpha, beta) * (getVandermonde_JACOBI(_nnodes, _nodes, alpha, beta).transpose())); + } + + }; + + enum class ParticleDiscretizationMode : int + { + /** + * Equidistant distribution of shell edges + */ + Equidistant, + + /** + * Volumes of shells are uniform + */ + Equivolume, + + /** + * Shell edges specified by user + */ + UserDefined + }; + + Discretization _disc; //!< Discretization info + std::vector _hasSurfaceDiffusion; //!< Determines whether surface diffusion is present in each particle type +// IExternalFunction* _extFun; //!< External function (owned by library user) + + parts::ConvectionDispersionOperatorBase _convDispOpB; //!< Convection dispersion operator base for interstitial volume transport + IDynamicReactionModel* _dynReactionBulk; //!< Dynamic reactions in the bulk volume + + Eigen::SparseLU> _globalSolver; //!< linear solver + //Eigen::BiCGSTAB, Eigen::DiagonalPreconditioner> _globalSolver; + + Eigen::SparseMatrix _globalJac; //!< static part of global Jacobian + Eigen::SparseMatrix _globalJacDisc; //!< global Jacobian with time derivative from BDF method + //MatrixXd FDJac; // test purpose FD Jacobian + + Eigen::MatrixXd _jacInlet; //!< Jacobian inlet DOF block matrix connects inlet DOFs to first bulk cells + + active _colPorosity; //!< Column porosity (external porosity) \f$ \varepsilon_c \f$ + std::vector _parRadius; //!< Particle radius \f$ r_p \f$ + bool _singleParRadius; + std::vector _parCoreRadius; //!< Particle core radius \f$ r_c \f$ + bool _singleParCoreRadius; + std::vector _parPorosity; //!< Particle porosity (internal porosity) \f$ \varepsilon_p \f$ + bool _singleParPorosity; + std::vector _parTypeVolFrac; //!< Volume fraction of each particle type + std::vector _parDiscType; //!< Particle discretization mode + std::vector _parDiscVector; //!< Particle discretization shell edges + std::vector _parGeomSurfToVol; //!< Particle surface to volume ratio factor (i.e., 3.0 for spherical, 2.0 for cylindrical, 1.0 for hexahedral) + + // Vectorial parameters + std::vector _filmDiffusion; //!< Film diffusion coefficient \f$ k_f \f$ + MultiplexMode _filmDiffusionMode; + std::vector _parDiffusion; //!< Particle diffusion coefficient \f$ D_p \f$ + MultiplexMode _parDiffusionMode; + std::vector _parSurfDiffusion; //!< Particle surface diffusion coefficient \f$ D_s \f$ + MultiplexMode _parSurfDiffusionMode; + std::vector _poreAccessFactor; //!< Pore accessibility factor \f$ F_{\text{acc}} \f$ + MultiplexMode _poreAccessFactorMode; + std::vector _parDepSurfDiffusion; //!< Parameter dependencies for particle surface diffusion + bool _singleParDepSurfDiffusion; //!< Determines whether a single parameter dependence for particle surface diffusion is used + bool _hasParDepSurfDiffusion; //!< Determines whether particle surface diffusion parameter dependencies are present + + bool _axiallyConstantParTypeVolFrac; //!< Determines whether particle type volume fraction is homogeneous across axial coordinate + bool _analyticJac; //!< Determines whether AD or analytic Jacobians are used + unsigned int _jacobianAdDirs; //!< Number of AD seed vectors required for Jacobian computation + + std::vector _parCellSize; //!< Particle shell size + std::vector _parCenterRadius; //!< Particle node-centered position for each particle node + std::vector _parOuterSurfAreaPerVolume; //!< Particle shell outer sphere surface to volume ratio + std::vector _parInnerSurfAreaPerVolume; //!< Particle shell inner sphere surface to volume ratio + + bool _factorizeJacobian; //!< Determines whether the Jacobian needs to be factorized + double* _tempState; //!< Temporary storage with the size of the state vector or larger if binding models require it + + std::vector _initC; //!< Liquid bulk phase initial conditions + std::vector _initCp; //!< Liquid particle phase initial conditions + std::vector _initQ; //!< Solid phase initial conditions + std::vector _initState; //!< Initial conditions for state vector if given + std::vector _initStateDot; //!< Initial conditions for time derivative + + BENCH_TIMER(_timerResidual) + BENCH_TIMER(_timerResidualPar) + BENCH_TIMER(_timerResidualSens) + BENCH_TIMER(_timerResidualSensPar) + BENCH_TIMER(_timerJacobianPar) + BENCH_TIMER(_timerConsistentInit) + BENCH_TIMER(_timerConsistentInitPar) + BENCH_TIMER(_timerLinearSolve) + BENCH_TIMER(_timerFactorize) + BENCH_TIMER(_timerFactorizePar) + BENCH_TIMER(_timerMatVec) + BENCH_TIMER(_timerGmres) + + class Indexer + { + public: + Indexer(const Discretization& disc) : _disc(disc) { } + + // Strides + inline int strideColNode() const CADET_NOEXCEPT { return static_cast(_disc.nComp); } + inline int strideColCell() const CADET_NOEXCEPT { return static_cast(_disc.nNodes * strideColNode()); } + inline int strideColComp() const CADET_NOEXCEPT { return 1; } + + inline int strideParComp() const CADET_NOEXCEPT { return 1; } + inline int strideParLiquid() const CADET_NOEXCEPT { return static_cast(_disc.nComp); } + inline int strideParBound(int parType) const CADET_NOEXCEPT { return static_cast(_disc.strideBound[parType]); } + inline int strideParNode(int parType) const CADET_NOEXCEPT { return strideParLiquid() + strideParBound(parType); } + inline int strideParShell(int parType) const CADET_NOEXCEPT { return strideParNode(parType) * _disc.nParNode[parType]; } + inline int strideParBlock(int parType) const CADET_NOEXCEPT { return static_cast(_disc.nParPoints[parType]) * strideParNode(parType); } + + // Offsets + inline int offsetC() const CADET_NOEXCEPT { return _disc.nComp; } + inline int offsetCp() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints + offsetC(); } + inline int offsetCp(ParticleTypeIndex pti) const CADET_NOEXCEPT { return offsetCp() + _disc.parTypeOffset[pti.value]; } + inline int offsetCp(ParticleTypeIndex pti, ParticleIndex pi) const CADET_NOEXCEPT { return offsetCp() + _disc.parTypeOffset[pti.value] + strideParBlock(pti.value) * pi.value; } + inline int offsetBoundComp(ParticleTypeIndex pti, ComponentIndex comp) const CADET_NOEXCEPT { return _disc.boundOffset[pti.value * _disc.nComp + comp.value]; } + + // Return pointer to first element of state variable in state vector + template inline real_t* c(real_t* const data) const { return data + offsetC(); } + template inline real_t const* c(real_t const* const data) const { return data + offsetC(); } + + template inline real_t* cp(real_t* const data) const { return data + offsetCp(); } + template inline real_t const* cp(real_t const* const data) const { return data + offsetCp(); } + + template inline real_t* q(real_t* const data) const { return data + offsetCp() + strideParLiquid(); } + template inline real_t const* q(real_t const* const data) const { return data + offsetCp() + strideParLiquid(); } + + // Return specific variable in state vector + template inline real_t& c(real_t* const data, unsigned int point, unsigned int comp) const { return data[offsetC() + comp + point * strideColNode()]; } + template inline const real_t& c(real_t const* const data, unsigned int point, unsigned int comp) const { return data[offsetC() + comp + point * strideColNode()]; } + + protected: + const Discretization& _disc; + }; + + class Exporter : public ISolutionExporter + { + public: + + Exporter(const Discretization& disc, const GeneralRateModelDG& model, double const* data) : _disc(disc), _idx(disc), _model(model), _data(data) { } + Exporter(const Discretization&& disc, const GeneralRateModelDG& model, double const* data) = delete; + + virtual bool hasParticleFlux() const CADET_NOEXCEPT { return false; } + virtual bool hasParticleMobilePhase() const CADET_NOEXCEPT { return true; } + virtual bool hasSolidPhase() const CADET_NOEXCEPT { return _disc.strideBound[_disc.nParType] > 0; } + virtual bool hasVolume() const CADET_NOEXCEPT { return false; } + virtual bool isParticleLumped() const CADET_NOEXCEPT { return false; } + virtual bool hasPrimaryExtent() const CADET_NOEXCEPT { return true; } + + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual unsigned int numPrimaryCoordinates() const CADET_NOEXCEPT { return _disc.nPoints; } + virtual unsigned int numSecondaryCoordinates() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numParticleTypes() const CADET_NOEXCEPT { return _disc.nParType; } + virtual unsigned int numParticleShells(unsigned int parType) const CADET_NOEXCEPT { return _disc.nParPoints[parType]; } + virtual unsigned int numBoundStates(unsigned int parType) const CADET_NOEXCEPT { return _disc.strideBound[parType]; } + virtual unsigned int numMobilePhaseDofs() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints; } + virtual unsigned int numParticleMobilePhaseDofs() const CADET_NOEXCEPT + { + unsigned int nDofPerParType = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + nDofPerParType += _disc.nParPoints[i]; + return _disc.nPoints * nDofPerParType * _disc.nComp; + } + virtual unsigned int numParticleMobilePhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return _disc.nPoints * _disc.nParPoints[parType] * _disc.nComp; } + virtual unsigned int numSolidPhaseDofs() const CADET_NOEXCEPT + { + unsigned int nDofPerParType = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + nDofPerParType += _disc.nParPoints[i] * _disc.strideBound[i]; + return _disc.nPoints * nDofPerParType; + } + virtual unsigned int numSolidPhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return _disc.nPoints * _disc.nParPoints[parType] * _disc.strideBound[parType]; } + virtual unsigned int numParticleFluxDofs() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numVolumeDofs() const CADET_NOEXCEPT { return 0; } + + virtual int writeMobilePhase(double* buffer) const; + virtual int writeSolidPhase(double* buffer) const; + virtual int writeParticleMobilePhase(double* buffer) const; + virtual int writeSolidPhase(unsigned int parType, double* buffer) const; + virtual int writeParticleMobilePhase(unsigned int parType, double* buffer) const; + virtual int writeParticleFlux(double* buffer) const; + virtual int writeParticleFlux(unsigned int parType, double* buffer) const; + virtual int writeVolume(double* buffer) const { return 0; } + virtual int writeInlet(unsigned int port, double* buffer) const; + virtual int writeInlet(double* buffer) const; + virtual int writeOutlet(unsigned int port, double* buffer) const; + virtual int writeOutlet(double* buffer) const; + /** + * @brief calculates, writes the physical axial/column coordinates of the DG discretization with double! interface nodes + */ + virtual int writePrimaryCoordinates(double* coords) const { + Eigen::VectorXd x_l = Eigen::VectorXd::LinSpaced(static_cast(_disc.nCol + 1), 0.0, _disc.colLength); + for (unsigned int i = 0; i < _disc.nCol; i++) { + for (unsigned int j = 0; j < _disc.nNodes; j++) { + // mapping + coords[i * _disc.nNodes + j] = x_l[i] + 0.5 * (_disc.colLength / static_cast(_disc.nCol)) * (1.0 + _disc.nodes[j]); + } + } + + return _disc.nPoints; + } + virtual int writeSecondaryCoordinates(double* coords) const { return 0; } + /** + * @brief calculates, writes the physical radial/particle coordinates of the DG discretization with double! interface nodes + */ + virtual int writeParticleCoordinates(unsigned int parType, double* coords) const + { + active const* const pcr = _model._parCenterRadius.data() + _disc.offsetMetric[parType]; + + // Note that the DG particle shells are oppositely ordered compared to the FV particle shells + for (unsigned int par = 0; par < _disc.nParPoints[parType]; par++) { + + unsigned int cell = std::floor(par / _disc.nParNode[parType]); + + double r_L = static_cast(pcr[cell]) - 0.5 * _disc.deltaR[_disc.offsetMetric[parType] + cell]; + coords[par] = r_L + 0.5 * _disc.deltaR[_disc.offsetMetric[parType] + cell] * (1.0 + _disc.parNodes[parType][par % _disc.nParNode[parType]]); + } + + return _disc.nParPoints[parType]; + } + + protected: + const Discretization& _disc; + const Indexer _idx; + const GeneralRateModelDG& _model; + double const* const _data; + }; + + /** + * @brief sets the current section index and loads section dependend velocity, dispersion + */ + void updateSection(int secIdx) { + + if (cadet_unlikely(_disc.curSection != secIdx)) { + + _disc.curSection = secIdx; + _disc.newStaticJac = true; + + // update velocity and dispersion + _disc.velocity = static_cast(_convDispOpB.currentVelocity()); + + if (_convDispOpB.dispersionCompIndep()) + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOpB.currentDispersion(secIdx)[0]); + } + else { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOpB.currentDispersion(secIdx)[comp]); + } + } + } + } + +// =========================================================================================================================================================== // +// ======================================== DG functions to compute the discretization ====================================================== // +// =========================================================================================================================================================== // + + void applyParInvMap(VectorXd& state, unsigned int parType) { + for (int cell = 0; cell < _disc.nParCell[parType]; cell++) { + state.segment(cell * _disc.nParNode[parType], _disc.nParNode[parType]) *= 2.0 / _disc.deltaR[_disc.offsetMetric[parType] + cell] * 2.0 / _disc.deltaR[_disc.offsetMetric[parType] + cell]; + } + } + + /** + * @brief calculates the volume Integral of the auxiliary equation + * @detail estimates the state derivative = - D * state + * @param [in] current state vector + * @param [in] stateDer vector to be changed + * @param [in] aux true if auxiliary, else main equation + */ + void volumeIntegral(Eigen::Map>& state, Eigen::Map>& stateDer) { + // comp-cell-node state vector: use of Eigen lib performance + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + stateDer.segment(Cell * _disc.nNodes, _disc.nNodes) + -= _disc.polyDerM * state.segment(Cell * _disc.nNodes, _disc.nNodes); + } + } + + void parVolumeIntegral(const int parType, const bool aux, Eigen::Map>& state, Eigen::Map>& stateDer) { + + int nNodes = _disc.nParNode[parType]; + + /* no additional metric term for auxiliary equation or particle equation with exact integration scheme + -> res = - D * (d_p * c^p + invBeta_p sum_mi d_s c^s) */ + if (aux || (_disc.parExactInt[parType] && _parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab)) { + // comp-cell-node state vector: use of Eigen lib performance + for (unsigned int Cell = 0; Cell < _disc.nParCell[parType]; Cell++) { + stateDer.segment(Cell * nNodes, nNodes) + -= _disc.parPolyDerM[parType] * state.segment(Cell * nNodes, nNodes); + } + } + else if (_disc.parExactInt[parType] && _parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab) { + // comp-cell-node state vector: use of Eigen lib performance + for (unsigned int Cell = 0; Cell < _disc.nParCell[parType]; Cell++) { + stateDer.segment(Cell * nNodes, nNodes) + -= _disc.minus_InvMM_ST[_disc.offsetMetric[parType] + Cell] * state.segment(Cell * nNodes, nNodes); + } + } + /* include metrics for main particle equation -> res = - D * (d_p * c^p + invBeta_p sum_mi d_s c^s) */ + else { // inexact integration, main equation + + int Cell0 = 0; // auxiliary variable to distinguish special case + + // special case for non slab-shaped particles without core => r(xi_0) = 0 + if (_parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab && _parCoreRadius[parType] == 0.0) { + Cell0 = 1; + + // estimate volume integral except for boundary node + stateDer.segment(1, nNodes - 1) -= _disc.Dr[_disc.offsetMetric[parType]].block(1, 1, nNodes - 1, nNodes - 1) * state.segment(1, nNodes - 1); + // estimate volume integral for boundary node: sum_{j=1}^N state_j * w_j * D_{j,0} * r_j + stateDer[0] += (state.segment(1, nNodes - 1).array() + * _disc.parInvWeights[parType].segment(1, nNodes - 1).array().cwiseInverse() + * _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, 1).array() + * _disc.Ir[_disc.offsetMetric[parType]].segment(1, nNodes - 1).array() + ).sum(); + } + + // "standard" computation for remaining cells + for (int cell = Cell0; cell < _disc.nParCell[parType]; cell++) { + stateDer.segment(cell * nNodes, nNodes) -= _disc.Dr[_disc.offsetMetric[parType] + cell] * state.segment(cell * nNodes, nNodes); + } + } + } + + /* + * @brief calculates the interface fluxes h* of bulk mass balance equation + */ + void InterfaceFlux(Eigen::Map>& C, const VectorXd& g, const unsigned int comp) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // Conv.Disp. flux: h* = h*_conv + h*_disp = numFlux(v c_l, v c_r) + 0.5 sqrt(D_ax) (S_l + S_r) + + if (_disc.velocity >= 0.0) { // forward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell - strideNode]) // left cell (i.e. forward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet (left) boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * _disc.boundary[0]; + + // outlet (right) boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * (C[_disc.nCol * strideCell - strideNode]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[_disc.nCol * strideCell - strideNode] // last cell last node + + _disc.boundary[3]); // right boundary value S + } + else { // backward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell]) // right cell (i.e. backward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * _disc.boundary[0]; + + // outlet boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * (C[0]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[0] // first cell first node + + _disc.boundary[2]); // left boundary value g + } + } + + /* + * @brief calculates the interface fluxes g* of particle mass balance equation and implements the respective boundary conditions + * @param [in] aux bool if interface flux for auxiliary equation + * @param [in] addParDisc bool if interface flux for additional particle DG-discretized equation + */ + void InterfaceFluxParticle(int parType, Eigen::Map>& state, + const unsigned int strideCell, const unsigned int strideNode, const bool aux, const int comp, const bool addParDisc = false) { + + // reset surface flux storage as it is used multiple times + _disc.surfaceFluxParticle[parType].setZero(); + + // numerical flux: state* = 0.5 (state^+ + state^-) + + // calculate inner interface fluxes + for (unsigned int Cell = 1u; Cell < _disc.nParCell[parType]; Cell++) { + _disc.surfaceFluxParticle[parType][Cell] // left interfaces + = 0.5 * (state[Cell * strideCell - strideNode] + // outer/left node + state[Cell * strideCell]); // inner/right node + } + + // calculate boundary interface fluxes. + if (aux) { // ghost nodes given by state^- := state^+ for auxiliary equation + _disc.surfaceFluxParticle[parType][0] = state[0]; + + _disc.surfaceFluxParticle[parType][_disc.nParCell[parType]] = state[_disc.nParCell[parType] * strideCell - strideNode]; + } + else if (addParDisc) { + _disc.surfaceFluxParticle[parType][0] = 0.0; + + _disc.surfaceFluxParticle[parType][_disc.nParCell[parType]] = 0.0; + } + else { + + // film diffusion BC + _disc.surfaceFluxParticle[parType][_disc.nParCell[parType]] = _disc.localFlux[comp] + / (static_cast(_parPorosity[parType]) * static_cast(_poreAccessFactor[parType * _disc.nComp + comp])) + * (2.0 / _disc.deltaR[_disc.offsetMetric[parType]]); // inverse squared mapping is also applied, so we apply Map * invMap^2 = invMap + + // inner particle BC + _disc.surfaceFluxParticle[parType][0] = 0.0; + + } + } + + /** + * @brief calculates and fills the surface flux values for auxiliary equation + * @param [in] strideCell component-wise cell stride + * @param [in] strideNodecomponent-wise node stride + */ + void InterfaceFluxAuxiliary(Eigen::Map>& C, const unsigned int strideCell, const unsigned int strideNode) { + + // Auxiliary flux: c* = 0.5 (c^+ + c^-) + + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + _disc.surfaceFlux[Cell] // left interfaces + = 0.5 * (C[Cell * strideCell - strideNode] + // left node + C[Cell * strideCell]); // right node + } + // calculate boundary interface fluxes + + _disc.surfaceFlux[0] // left boundary interface + = 0.5 * (C[0] + // boundary value + C[0]); // first cell first node + + _disc.surfaceFlux[(_disc.nCol)] // right boundary interface + = 0.5 * (C[_disc.nCol * strideCell - strideNode] + // last cell last node + C[_disc.nCol * strideCell - strideNode]);// // boundary value + } + + /** + * @brief calculates the surface Integral, depending on the approach (exact/inexact integration) + * @param [in] state relevant state vector + * @param [in] stateDer state derivative vector the solution is added to + * @param [in] aux true for auxiliary equation, false for main equation + surfaceIntegral(cPtr, &(disc.g[0]), disc,&(disc.h[0]), resPtrC, 0, secIdx); + * @param [in] strideCell component-wise cell stride + * @param [in] strideNodecomponent-wise node stride + */ + void surfaceIntegral(Eigen::Map>& C, Eigen::Map>& state, + Eigen::Map>& stateDer, const bool aux, const unsigned int Comp, const unsigned int strideCell, const unsigned int strideNode) { + + // calc numerical flux values c* or h* depending on equation switch aux + (aux == 1) ? InterfaceFluxAuxiliary(C, strideCell, strideNode) : InterfaceFlux(C, _disc.g, Comp); + if (_disc.exactInt) { // exact integration approach -> dense mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + for (unsigned int Node = 0; Node < _disc.nNodes; Node++) { + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.invMM(Node, 0) * (state[Cell * strideCell] + - _disc.surfaceFlux[Cell]) + - _disc.invMM(Node, _disc.polyDeg) * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux[(Cell + 1u)]); + } + } + } + else { // inexact integration approach -> diagonal mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + stateDer[Cell * strideCell] // first cell node + -= _disc.invWeights[0] * (state[Cell * strideCell] // first node + - _disc.surfaceFlux(Cell)); + stateDer[Cell * strideCell + _disc.polyDeg * strideNode] // last cell node + += _disc.invWeights[_disc.polyDeg] * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux(Cell + 1u)); + } + } + } + + /** + * @brief calculates the particle surface Integral (type- and component-wise) + * @param [in] parType current particle type + * @param [in] state relevant state vector + * @param [in] stateDer state derivative vector the solution is added to + * @param [in] aux true for auxiliary equation, false for main equation + * @param [in] strideCell component-wise cell stride + * @param [in] strideNodecomponent-wise node stride + * @param [in] comp current component + */ + void parSurfaceIntegral(int parType, Eigen::Map>& state, + Eigen::Map>& stateDer, unsigned const int strideCell, unsigned const int strideNode, + const bool aux, const int comp = 0, const bool addParDisc = false) { + + // calc numerical flux values + InterfaceFluxParticle(parType, state, strideCell, strideNode, aux, comp, addParDisc); + + // strong surface integral -> M^-1 B [state - state*] + if (!_disc.parExactInt[parType]) { // inexact integration approach -> diagonal mass matrix + int Cell0 = 0; // auxiliary variable to distinguish special case + // special case for sphere and cylinder if particle core = 0.0 -> leave out inner particle boundary flux + if (_parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab && _parCoreRadius[parType] == 0.0) { + + Cell0 = 1; + + stateDer[_disc.parPolyDeg[parType] * strideNode] // last cell node + += _disc.parInvWeights[parType][_disc.parPolyDeg[parType]] * (state[_disc.parPolyDeg[parType] * strideNode] + - _disc.surfaceFluxParticle[parType][1]); + } + + for (unsigned int Cell = Cell0; Cell < _disc.nParCell[parType]; Cell++) { + + stateDer[Cell * strideCell] // first cell node + -= _disc.parInvWeights[parType][0] * (state[Cell * strideCell] + - _disc.surfaceFluxParticle[parType][Cell]); + + stateDer[Cell * strideCell + _disc.parPolyDeg[parType] * strideNode] // last cell node + += _disc.parInvWeights[parType][_disc.parPolyDeg[parType]] * (state[Cell * strideCell + _disc.parPolyDeg[parType] * strideNode] + - _disc.surfaceFluxParticle[parType][Cell + 1u]); + } + } + else { // exact integration approach -> dense mass matrix + for (unsigned int Cell = 0; Cell < _disc.nParCell[parType]; Cell++) { + + for (unsigned int Node = 0; Node < _disc.nParNode[parType]; Node++) { + if (aux) { // strong surface integral -> M^-1 B [state - state*] and B has two non-zero entries, -1 and 1 + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.parInvMM_Leg[parType](Node, 0) * (state[Cell * strideCell] + - _disc.surfaceFluxParticle[parType][Cell]) + - _disc.parInvMM_Leg[parType](Node, _disc.parPolyDeg[parType]) * (state[Cell * strideCell + _disc.parPolyDeg[parType] * strideNode] + - _disc.surfaceFluxParticle[parType][Cell + 1u]); + } + else { + if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab) { // strong surface integral -> M^-1 B [state - state*] and B has two non-zero entries, -1 and 1 + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.parInvMM[parType](Node, 0) * (state[Cell * strideCell] + - _disc.surfaceFluxParticle[parType][Cell]) + - _disc.parInvMM[parType](Node, _disc.parPolyDeg[parType]) * (state[Cell * strideCell + _disc.parPolyDeg[parType] * strideNode] + - _disc.surfaceFluxParticle[parType][Cell + 1u]); + } + else if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioCylinder) { // weak surface integral -> M^-1 B [- state*] and B has two non-zero entries, which depend on metrics + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.Ir[_disc.offsetMetric[parType] + Cell][0] * _disc.parInvMM[_disc.offsetMetric[parType] + Cell](Node, 0) * (-_disc.surfaceFluxParticle[parType][Cell]) + + _disc.Ir[_disc.offsetMetric[parType] + Cell][_disc.nParNode[parType] - 1] * _disc.parInvMM[_disc.offsetMetric[parType] + Cell](Node, _disc.parPolyDeg[parType]) * _disc.surfaceFluxParticle[parType][Cell + 1u]; + } + else if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSphere) { // weak surface integral -> M^-1 B [- state*] and B has two non-zero entries, which depend on metrics + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.Ir[_disc.offsetMetric[parType] + Cell][0] * _disc.parInvMM[_disc.offsetMetric[parType] + Cell](Node, 0) * (-_disc.surfaceFluxParticle[parType][Cell]) + + _disc.Ir[_disc.offsetMetric[parType] + Cell][_disc.nParNode[parType] - 1] * _disc.parInvMM[_disc.offsetMetric[parType] + Cell](Node, _disc.parPolyDeg[parType]) * _disc.surfaceFluxParticle[parType][Cell + 1u]; + } + } + } + } + } + } + + /** + * @brief calculates the substitute h = vc - sqrt(D_ax) g(c) + */ + void calcH(Eigen::Map>& C, const unsigned int Comp) { + _disc.h = _disc.velocity * C - sqrt(_disc.dispersion[Comp]) * _disc.g; + } + + /** + * @brief applies the inverse Jacobian of the mapping + */ + void applyMapping(Eigen::Map>& state) { + state *= (2.0 / _disc.deltaZ); + } + /** + * @brief applies the inverse Jacobian of the mapping and auxiliary factor -1 + */ + void applyMapping_Aux(Eigen::Map>& state, const unsigned int Comp) { + state *= (-2.0 / _disc.deltaZ) * ((_disc.dispersion[Comp] == 0.0) ? 1.0 : std::sqrt(_disc.dispersion[Comp])); + } + /** + * @brief calculates the convection and dispersion right-hand side of the bulk mass balance DG discretization + */ + void ConvDisp_DG(Eigen::Map>& C, Eigen::Map>& resC, double t, unsigned int Comp) { + + // ===================================// + // reset cache // + // ===================================// + + resC.setZero(); + _disc.h.setZero(); + _disc.g.setZero(); + _disc.surfaceFlux.setZero(); + // get Map objects of auxiliary variable memory + Eigen::Map> g(&_disc.g[0], _disc.nPoints, InnerStride<>(1)); + Eigen::Map> h(&_disc.h[0], _disc.nPoints, InnerStride<>(1)); + + // ======================================// + // solve auxiliary system g = d c / d x // + // ======================================// + + volumeIntegral(C, g); // DG volumne integral in strong form + + surfaceIntegral(C, C, g, 1, Comp, _disc.nNodes, 1u); // surface integral in strong form + + applyMapping_Aux(g, Comp); // inverse mapping from reference space and auxiliary factor + + _disc.surfaceFlux.setZero(); // reset surface flux storage as it is used twice + + // ======================================// + // solve main equation w_t = d h / d x // + // ======================================// + + calcH(C, Comp); // calculate the substitute h(S(c), c) = sqrt(D_ax) g(c) - v c + + volumeIntegral(h, resC); // DG volumne integral in strong form + + calcBoundaryValues(C);// update boundary values including auxiliary variable g + + surfaceIntegral(C, h, resC, 0, Comp, _disc.nNodes, 1u); // DG surface integral in strong form + + applyMapping(resC); // inverse mapping to reference space + + } + /** + * @brief computes ghost nodes used to implement Danckwerts boundary conditions of bulk phase mass balance + */ + void calcBoundaryValues(Eigen::Map>& C) { + + //cache.boundary[0] = c_in -> inlet DOF idas suggestion + //_disc.boundary[1] = (_disc.velocity >= 0.0) ? C[_disc.nPoints - 1] : C[0]; // c_r outlet not required + _disc.boundary[2] = -_disc.g[0]; // g_l left boundary (inlet/outlet for forward/backward flow) + _disc.boundary[3] = -_disc.g[_disc.nPoints - 1]; // g_r right boundary (outlet/inlet for forward/backward flow) + } + /** + * @brief solves the auxiliary system g = d c / d xi + * @detail computes g = Dc - M^-1 B [c - c^*] and stores this in _disc.g_p + */ + void solve_auxiliary_DG(int parType, Eigen::Map>& conc, unsigned int strideCell, unsigned int strideNode, int comp) { + + Eigen::Map> g_p(&_disc.g_p[parType][0], _disc.nParPoints[parType], InnerStride<>(1)); + + // ========================================================================================// + // solve auxiliary systems g = d c / d xi => g_p = Dc - M^-1 B [c - c^*] // + // ========================================================================================// + + _disc.surfaceFluxParticle[parType].setZero(); // reset surface flux storage as it is used multiple times + + g_p.setZero(); // reset auxiliary variable g + + parVolumeIntegral(parType, true, conc, g_p); // volumne integral in strong DG form: - D c + + parSurfaceIntegral(parType, conc, g_p, strideCell, strideNode, true, comp); // surface integral in strong DG form: M^-1 B [c - c^*] + + g_p *= -1.0; // auxiliary factor -1 + } + + // ========================================================================================================================================================== // + // ======================================== DG Jacobian ========================================================= // + // ========================================================================================================================================================== // + + /** + * @brief computes the jacobian via finite differences (testing purpose) + */ + MatrixXd calcFDJacobian(const double* y_, const double* yDot_, const SimulationTime simTime, util::ThreadLocalStorage& threadLocalMem, double alpha) { + + // create solution vectors + Eigen::Map hmpf(y_, numDofs()); + VectorXd y = hmpf; + VectorXd yDot; + if (yDot_) { + Eigen::Map hmpf2(yDot_, numDofs()); + yDot = hmpf2; + } + else { + return MatrixXd::Zero(numDofs(), numDofs()); + } + VectorXd res = VectorXd::Zero(numDofs()); + const double* yPtr = &y[0]; + const double* yDotPtr = &yDot[0]; + double* resPtr = &res[0]; + // create FD jacobian + MatrixXd Jacobian = MatrixXd::Zero(numDofs(), numDofs()); + // set FD step + double epsilon = 0.01; + + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + + for (int col = 0; col < Jacobian.cols(); col++) { + Jacobian.col(col) = -(1.0 + alpha) * res; + } + /* Residual(y+h) */ + // state DOFs + for (int dof = 0; dof < Jacobian.cols(); dof++) { + y[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + y[dof] -= epsilon; + Jacobian.col(dof) += res; + } + + // state derivative Jacobian + for (int dof = 0; dof < Jacobian.cols(); dof++) { + yDot[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + yDot[dof] -= epsilon; + Jacobian.col(dof) += alpha * res; + } + + ///* exterminate numerical noise */ + //for (int i = 0; i < Jacobian.rows(); i++) { + // for (int j = 0; j < Jacobian.cols(); j++) { + // if (std::abs(Jacobian(i, j)) < 1e-10) Jacobian(i, j) = 0.0; + // } + //} + Jacobian /= epsilon; + + return Jacobian; + } + + typedef Eigen::Triplet T; + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the inexact integration DG scheme + */ + int ConvDispInexactIntPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + int sComp = idxr.strideColComp(); + int offC = idxr.offsetC(); // global jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int polyDeg = _disc.polyDeg; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + // special inlet DOF treatment for inlet boundary cell (first cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + /*======================================================*/ + /* Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + + // insert Blocks to Jacobian inner cells (only for nCells >= 3) + if (nCells >= 3u) { + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + (cell - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + if (nCells != 1) { // Note: special case nCells = 1 already set by advection block + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - nNodes) * sNode, + 0.0)); + } + } + } + + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1 - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 1; + } + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the exact integration DG scheme + */ + int ConvDispExactIntPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + int sComp = idxr.strideColComp(); + int offC = idxr.offsetC(); // global jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /*======================================================*/ + /* Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells */ + if (nCells >= 5u) { + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + for (unsigned int cell = 2; cell < nCells - 2; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* boundary cell neighbours */ + + // left boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + else if (nCells == 3u) { // special case: only depends on the two neighbouring cells + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + // right boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2 - 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 2) * sCell + comp * sComp + i * sNode, + offC + (nCells - 2) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + /* boundary cells */ + + // left boundary cell + unsigned int end = 3u * nNodes + 2u; + if (nCells == 1u) end = 2u * nNodes + 1u; + else if (nCells == 2u) end = 3u * nNodes + 1u; + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes + 1; j < end; j++) { + // row: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset, adjust for iterator j (-Nnodes-1) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - (nNodes + 1)) * sNode, + 0.0)); + } + } + } + // right boundary cell + if (nCells >= 3u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + else if (nCells == 2u) { // special case for nCells == 2: depends only on left cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes)*sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 1; + } + + + /** + * @brief calculates the particle dispersion jacobian Pattern of the exact/inexact integration DG scheme for the given particle type and bead + */ + void calcParticleJacobianPattern(std::vector& tripletList, unsigned int parType, unsigned int colNode, unsigned int secIdx) { + + // Ordering of particle surface diffusion: + // bnd0comp0, bnd0comp1, bnd0comp2, bnd1comp0, bnd1comp1, bnd1comp2 + active const* const _parSurfDiff = getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], secIdx) + _disc.nBoundBeforeType[parType]; + + Indexer idxr(_disc); + + // (global) strides + unsigned int sCell = _disc.nParNode[parType] * idxr.strideParNode(parType); + unsigned int sNode = idxr.strideParNode(parType); + unsigned int sComp = 1u; + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + unsigned int nNodes = _disc.nParNode[parType]; + + // case: one cell -> diffBlock \in R^(nParNodes x nParNodes), GBlock = parPolyDerM + if (_disc.nParCell[parType] == 1) { + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes; j++) { + // handle liquid state + // row: add component offset and go node strides from there for each dispersion block entry + // col: add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + comp * sComp + j * sNode, 0.0)); + + // handle surface diffusion of bound states. + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add current component offset and go node strides from there for each dispersion block entry + // col: jump oover liquid states, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode, 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode, 0.0)); + + } + } + } + } + } + } + } + } + else { + + if (!_disc.parExactInt[parType]) { + + /* left boundary cell */ + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) { + // handle liquid state + // row: add component offset and go node strides from there for each dispersion block entry + // col: add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + comp * sComp + (j - nNodes) * sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add current component offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (j - nNodes) * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (j - nNodes) * sNode, + 0.0)); + + } + } + } + } + } + } + } + } + + /* right boundary cell */ + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) { + // handle liquid state + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + comp * sComp + (_disc.nParCell[parType] - 2) * sCell + j * sNode, + 0.0)); + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (_disc.nParCell[parType] - 2) * sCell + j * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over previous cells and liquid states, go back one cell, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + (_disc.nParCell[parType] - 1) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (_disc.nParCell[parType] - 2) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode, + 0.0)); + + } + } + } + } + } + } + } + } + + /* inner cells */ + + for (int cell = 1; cell < _disc.nParCell[parType] - 1; cell++) { + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) { + // handle liquid state + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + cell * sCell + i * sNode, + offset + comp * sComp + (cell - 1) * sCell + j * sNode, 0.0)); + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + cell * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (cell - 1) * sCell + j * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over previous cells and liquid states, go back one cell, add current bound state offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + cell * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (cell - 1) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode, + 0.0)); + + } + } + } + } + } + } + } + } + } + } + else { //exact integration + + /* boundary cells */ + + /* left boundary cell */ + + unsigned int special = 0u; if (_disc.nParCell[parType] < 3u) special = 1u; // limits the iterator for special case nCells = 3 (dependence on additional entry) + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes + 1; j < 3 * nNodes + 2 - special; j++) { + // handle liquid state + // row: add component offset and go node strides from there for each dispersion block entry + // col: add component offset and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + comp * sComp + j * sNode - (nNodes + 1) * sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add current component offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - (nNodes + 1) * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - (nNodes + 1) * sNode, + 0.0)); + + } + } + } + } + } + } + } + + /* right boundary cell */ + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + + for (unsigned int j = special; j < 2 * nNodes + 1; j++) { + // handle liquid state + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell (and node or adjust for start) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell - sCell - sNode + j * sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell (and node or adjust for start) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (_disc.nParCell[parType] - 2) * sCell - sNode + j * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over previous cells and over liquid states, add current bound state offset. go back one cell (and node or adjust for start) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offset + (_disc.nParCell[parType] - 1) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (_disc.nParCell[parType] - 2) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) - sNode + bnd + j * sNode, + 0.0)); + } + } + } + } + } + } + } + if (_disc.nParCell[parType] == 3) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 2 - 1; j++) { + // handle liquid state + // row: add component offset and jump over previous cell. Go node strides from there for each dispersion block entry + // col: add component offset. Go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + sCell + i * sNode, + offset + comp * sComp + j * sNode - sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cell. go back one cell and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cell. go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - sNode, + 0.0)); + + } + } + } + } + + } + } + } + }// special case nCells == 3 + /* boundary cell neighbours (exist only if nCells >= 4) */ + if (_disc.nParCell[parType] >= 4) { + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 2; j++) { + // handle liquid state + // row: add component offset and jump over previous cell. Go node strides from there for each dispersion block entry + // col: add component offset. Go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + sCell + i * sNode, + offset + comp * sComp + j * sNode - sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + comp * sComp + sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over previous cells and over liquid states, add current bound state offset. go back one cell and go node strides from there for each dispersion block entry. adjust for j start + tripletList.push_back(T(offset + sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode - sNode, + 0.0)); + + } + } + } + } + + } + } + } + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2 - 1; j++) { + // handle liquid state + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and node. Go node strides from there for each dispersion block entry. + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 2) * sCell + i * sNode, + offset + comp * sComp + (_disc.nParCell[parType] - 2) * sCell - sCell - sNode + j * sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and node and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + (_disc.nParCell[parType] - 2) * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (_disc.nParCell[parType] - 2) * sCell - sCell - sNode + j * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over previous cells and over liquid states, add current bound state offset. go back one cell and node and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + (_disc.nParCell[parType] - 2) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (_disc.nParCell[parType] - 2) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) - sCell - sNode + bnd + j * sNode, + 0.0)); + + } + } + } + } + } + } + } + } + + /* Inner cells (exist only if nCells >= 5) */ + + if (_disc.nParCell[parType] >= 5) { + + for (unsigned int cell = 2; cell < _disc.nParCell[parType] - 2; cell++) { + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2; j++) { + // handle liquid state + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and node. Go node strides from there for each dispersion block entry. + tripletList.push_back(T(offset + comp * sComp + cell * sCell + i * sNode, + offset + comp * sComp + cell * sCell - sCell - sNode + j * sNode, + 0.0)); + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (_parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)] != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and node and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + comp * sComp + cell * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + cell * sCell - sCell - sNode + j * sNode, + 0.0)); + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over previous cells and over liquid states, add current bound state offset. go back one cell and node and go node strides from there for each dispersion block entry + tripletList.push_back(T(offset + cell * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + cell * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd - sCell - sNode + j * sNode, + 0.0)); + + } + } + } + } + } + } + } + } + + } + + } // parExactInt + } // if nCells > 1 + } + + /** + * @brief calculates the number of entries for the DG convection dispersion jacobian + * @note only dispersion entries are relevant for jacobian NNZ as the convection entries are a subset of these + */ + unsigned int nConvDispEntries(bool pureNNZ = false) { + + if (_disc.exactInt) { + if (pureNNZ) { + return _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + } + return _disc.nComp * _disc.nNodes * _disc.nNodes + _disc.nNodes // convection entries + + _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + } + else { + if (pureNNZ) { + return _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + } + return _disc.nComp * _disc.nNodes * _disc.nNodes + 1u // convection entries + + _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + } + } + unsigned int calcParDispNNZ(int parType) { + + if (_disc.parExactInt[parType]) { + return _disc.nComp * ((3u * _disc.nParCell[parType] - 2u) * _disc.nParNode[parType] * _disc.nParNode[parType] + (2u * _disc.nParCell[parType] - 3u) * _disc.nParNode[parType]); + } + else { + return _disc.nComp * (_disc.nParCell[parType] * _disc.nParNode[parType] * _disc.nParNode[parType] + 8u * _disc.nParNode[parType]); + } + } + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian + */ + void setConvDispJacPattern(std::vector& tripletList) { + + tripletList.reserve(nConvDispEntries()); + + if (_disc.exactInt) + ConvDispExactIntPattern(tripletList); + else + ConvDispInexactIntPattern(tripletList); + + } + /** + * @brief sets the sparsity pattern of the binding Jacobian + */ + void parBindingPattern_GRM(std::vector& tripletList, unsigned int parType, unsigned int colNode) { + + Indexer idxr(_disc); + + int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + + // every bound state might depend on every bound and liquid state + for (int parNode = 0; parNode < _disc.nParPoints[parType]; parNode++) { + for (int bnd = 0; bnd < _disc.strideBound[parType]; bnd++) { + for (int conc = 0; conc < idxr.strideParNode(parType); conc++) { + // row: jump over previous nodes and liquid states and add current bound state offset + // col: jump over previous nodes and add current concentration offset (liquid and bound) + tripletList.push_back(T(offset + parNode * idxr.strideParNode(parType) + idxr.strideParLiquid() + bnd, + offset + parNode * idxr.strideParNode(parType) + conc, 0.0)); + } + } + } + } + /** + *@brief adds the time derivative entries from particle equations + *@detail since the main diagonal entries are already set, we actually only set the solid phase time derivative entries for the discretized particle mass balance equations + */ + void parTimeDerJacPattern_GRM(std::vector& tripletList, unsigned int parType, unsigned int colNode, unsigned int secIdx) { + + Indexer idxr(_disc); + + active const* const _parSurfDiff = getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], secIdx) + _disc.nBoundBeforeType[parType]; + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + + for (unsigned int parNode = 0; parNode < _disc.nParPoints[parType]; parNode++) { + + // discretization special case: we get an algebraic equation at inner particle boundary + if (!_disc.parExactInt[parType] && parNode == 0u && _parGeomSurfToVol[parType] != _disc.SurfVolRatioSlab && _parCoreRadius[parType] == 0.0) + continue; + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + // row: jump over previous nodes add current component offset + // col: jump over previous nodes, liquid phase and previous bound states + tripletList.push_back(T(offset + parNode * idxr.strideParNode(parType) + comp, + offset + parNode * idxr.strideParNode(parType) + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd, + 0.0)); + } + } + } + } + /** + * @brief sets the sparsity pattern of the global Jacobian + */ + void setParJacPattern(std::vector& tripletList, unsigned int parType, unsigned int colNode, unsigned int secIdx) { + + calcParticleJacobianPattern(tripletList, parType, colNode, secIdx); + + parTimeDerJacPattern_GRM(tripletList, parType, colNode, secIdx); + + parBindingPattern_GRM(tripletList, parType, colNode); + } + /** + * @brief returns the offset between state order and parameter storage (bnd0comp0, bnd0comp1, bnd0comp2, bnd1comp0, bnd1comp1, bnd1comp2, ...) for one components bound state for a certain particle type + * @todo code review, is there a different more elegant/easy way? + */ + unsigned int getOffsetSurfDiff(unsigned int parType, unsigned int comp, unsigned int bnd) { + + unsigned int offNextBound = 0; + + // we need to estimate the offset to the next parameter of current components next bound state + // Ordering of particle surface diffusion: bnd0comp0, bnd0comp1, bnd0comp2, bnd1comp0, bnd1comp1, bnd1comp2 + for (unsigned int _comp = 0; _comp < _disc.nComp; _comp++) { + if(_comp < comp) // if its a component that occurs before comp, add all bound states of that component up to bnd + 1 (note that bound index starts at 0 -> +1). + offNextBound += std::min(bnd + 1u, _disc.nBound[parType * _disc.nComp + _comp]); + else // Otherwise, only add all previous (i.e. up to bnd) bound states of that component. This includes the current component itself (comp == _comp). + offNextBound += std::min(bnd, _disc.nBound[parType * _disc.nComp + _comp]); + } + + return offNextBound; + } + /** + * @brief calculate offsets between surface diffusion parameter storage and state ordering + */ + void orderSurfDiff() { + + Indexer idxr(_disc); + + for (unsigned int type = 0; type < _disc.nParType; type++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[type * _disc.nComp + comp]; bnd++) { + _disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd] = getOffsetSurfDiff(type, comp, bnd); + } + } + } + } + /** + * @brief analytically calculates the particle dispersion jacobian of the DGSEM (exact integration) for a single particle type and bead + */ + int calcParticleDGSEMJacobian(unsigned int parType, unsigned int colNode, const active* const parDiff, const active* const parSurfDiff, const double* const invBetaP) { + + Indexer idxr(_disc); + + // (global) strides + unsigned int sCell = _disc.nParNode[parType] * idxr.strideParNode(parType); + unsigned int sNode = idxr.strideParNode(parType); + unsigned int sComp = 1u; + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + unsigned int nNodes = _disc.nParNode[parType]; + + /* Special case */ + if (_disc.nParCell[parType] == 1) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{parType}, ParticleIndex{colNode})); // row iterator starting at first cell, first component + + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 0].block(0, nNodes + 1, nNodes, nNodes), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, 0); + return 1; + } + + /* Special case */ + if (_disc.nParCell[parType] == 2) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode })); // row iterator starting at first cell, first component + + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 0].block(0, nNodes + 1, nNodes, 2 * nNodes), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, 0); + // right Bacobian block, iterator is already moved to second cell + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 1].block(0, 1, nNodes, 2 * nNodes), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -idxr.strideParShell(parType)); + return 1; + } + + /* Special case */ + if (_disc.nParCell[parType] == 3) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + idxr.strideParShell(parType)); // row iterator starting at first cell, first component + + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 1].block(0, 1, nNodes, 3 * nNodes), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -idxr.strideParShell(parType)); + } + + /* Inner cells (exist only if nCells >= 5) */ + if (_disc.nParCell[parType] >= 5) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + idxr.strideParShell(parType) * 2); // row iterator starting at third cell, first component + + // insert all (nCol - 4) inner cell blocks + for (unsigned int cell = 2; cell < _disc.nParCell[parType] - 2; cell++) + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + cell], jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -(idxr.strideParShell(parType) + idxr.strideParNode(parType))); + } + + /* boundary cell neighbours (exist only if nCells >= 4) */ + if (_disc.nParCell[parType] >= 4) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) + idxr.strideParShell(parType)); // row iterator starting at second cell, first component + + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 1].block(0, 1, nNodes, 3 * nNodes + 1), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -idxr.strideParShell(parType)); + + jacIt += (_disc.nParCell[parType] - 4) * idxr.strideParShell(parType); // move iterator to preultimate cell (already at third cell) + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + _disc.nParCell[parType] - 2u].block(0, 0, nNodes, 3 * nNodes + 1), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -(idxr.strideParShell(parType) + idxr.strideParNode(parType))); + } + + /* boundary cells (exist only if nCells >= 3) */ + if (_disc.nParCell[parType] >= 3) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode })); // row iterator starting at first cell, first component + + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + 0].block(0, nNodes + 1, nNodes, 2 * nNodes + 1), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, 0); + + jacIt += (_disc.nParCell[parType] - 2) * idxr.strideParShell(parType); // move iterator to last cell (already at second cell) + insertParJacBlock(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + _disc.nParCell[parType] - 1u].block(0, 0, nNodes, 2 * nNodes + 1), jacIt, idxr, parDiff, parSurfDiff, invBetaP, _binding[parType]->reactionQuasiStationarity(), parType, 1u, -(idxr.strideParShell(parType) + idxr.strideParNode(parType))); + } + + return 1; + } + /** + * @brief analytically calculates the particle dispersion jacobian of the collocation DGSEM (inexact integration) for one particle type and bead + * @note deprecated, not further development + */ + int calcParticleCollocationDGSEMJacobian(unsigned int parType, unsigned int colNode, const active* const parDiff, const active* const parSurfDiff, const double* const invBetaP) { + + Indexer idxr(_disc); + + // (global) strides + unsigned int sCell = _disc.nParNode[parType] * idxr.strideParNode(parType); + unsigned int sNode = idxr.strideParNode(parType); + unsigned int sComp = 1u; + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + unsigned int nNodes = _disc.nParNode[parType]; + + // blocks to compute jacobian + Eigen::MatrixXd dispBlock; + Eigen::MatrixXd B = MatrixXd::Zero(nNodes, nNodes); + B(0, 0) = -1.0; B(nNodes - 1, nNodes - 1) = 1.0; + + // special case: one cell -> diffBlock \in R^(nParNodes x nParNodes), GBlock = parPolyDerM + if (_disc.nParCell[parType] == 1) { + + double invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType]]); + + if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab || _parCoreRadius[parType] != 0.0) + dispBlock = invMap * invMap * (_disc.Dr[parType] - _disc.parInvWeights[parType].asDiagonal() * B) * _disc.parPolyDerM[parType]; + + else { // special treatment of inner boundary node for spherical and cylindrical particles without particle core + + dispBlock = MatrixXd::Zero(nNodes, nNodes); + + // reduced system + dispBlock.block(1, 0, nNodes - 1, nNodes) + = (_disc.Dr[parType].block(1, 1, nNodes - 1, nNodes - 1) + - _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1)) + * _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, nNodes); + + // inner boundary node + dispBlock.block(0, 0, 1, nNodes) + = -(_disc.Ir[parType].segment(1, nNodes - 1).cwiseProduct( + _disc.parInvWeights[parType].segment(1, nNodes - 1).cwiseInverse()).cwiseProduct( + _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, 1))).transpose() + * _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, nNodes); + + dispBlock *= invMap * invMap; + } + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int j = 0; j < dispBlock.cols(); j++) { + // handle liquid state + // row: add component offset and go node strides from there for each dispersion block entry + // col: add component offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + i * sNode, + offset + comp * sComp + j * sNode) + = -(static_cast(parDiff[comp])) * dispBlock(i, j); // - D_p * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + /* add surface diffusion dispersion block to liquid */ + // row: add current component offset and go node strides from there for each dispersion block entry + // col: jump oover liquid states, add current bound state offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) * invBetaP[comp]) * dispBlock(i, j); // - D_s * (1 / Beta_p) * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump oover liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump oover liquid states, add current bound state offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * dispBlock(i, j); // - D_s * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + } + } + } + } + } + } + } + } + else { + + /* boundary cells */ + + // initialize dispersion and metric block matrices + MatrixXd bnd_dispBlock = MatrixXd::Zero(nNodes, 2 * nNodes); // boundary cell specific + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes); + + // compute blocks used for inexact integration scheme + // auxiliary block [ d g(c) / d c ] for left boundary cell + MatrixXd GBlock_l = MatrixXd::Zero(nNodes, nNodes + 1); + GBlock_l.block(0, 0, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock_l(nNodes - 1, nNodes - 1) -= 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + GBlock_l(nNodes - 1, nNodes) += 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + // auxiliary block [ d g(c) / d c ] for right boundary cell + MatrixXd GBlock_r = MatrixXd::Zero(nNodes, nNodes + 1); + GBlock_r.block(0, 1, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock_r(0, 0) -= 0.5 * _disc.parInvWeights[parType][0]; + GBlock_r(0, 1) += 0.5 * _disc.parInvWeights[parType][0]; + // numerical flux contribution for right interface of left boundary cell -> d f^*_N / d cp + MatrixXd bnd_gStarDC = MatrixXd::Zero(nNodes, 2 * nNodes); + bnd_gStarDC.block(nNodes - 1, 0, 1, nNodes + 1) = GBlock_l.block(nNodes - 1, 0, 1, nNodes + 1); + bnd_gStarDC.block(nNodes - 1, nNodes - 1, 1, nNodes + 1) += GBlock_r.block(0, 0, 1, nNodes + 1); + bnd_gStarDC *= 0.5; + + /* left boundary cell */ + double invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType]]); + + // "standard" computation for slab-shaped particles and spherical, cylindrical particles without core + if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab || _parCoreRadius[parType] != 0.0) { + // dispBlock <- invMap^2 * ( D * G_l - M^-1 * B * [G_l - g^*] ) + bnd_dispBlock.block(0, 0, nNodes, nNodes + 1) = (_disc.Dr[_disc.offsetMetric[parType]] - _disc.parInvWeights[parType].asDiagonal() * B) * GBlock_l; + bnd_dispBlock.block(0, 0, nNodes, 2 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * bnd_gStarDC; + bnd_dispBlock *= invMap * invMap; + } + else { // special treatment of inner boundary node for spherical and cylindrical particles without particle core + + // inner boundary node + bnd_dispBlock.block(0, 0, 1, nNodes + 1) + = -(_disc.Ir[_disc.offsetMetric[parType]].segment(1, nNodes - 1).cwiseProduct( + _disc.parInvWeights[parType].segment(1, nNodes - 1).cwiseInverse()).cwiseProduct( + _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, 1))).transpose() + * GBlock_l.block(1, 0, nNodes - 1, nNodes + 1); + + // reduced system for remaining nodes + bnd_dispBlock.block(1, 0, nNodes - 1, nNodes + 1) + = (_disc.Dr[_disc.offsetMetric[parType]].block(1, 1, nNodes - 1, nNodes - 1) + - _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1) + ) * GBlock_l.block(1, 0, nNodes - 1, nNodes + 1); + + bnd_dispBlock.block(1, 0, nNodes - 1, 2 * nNodes) + += _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1) * bnd_gStarDC.block(1, 0, nNodes - 1, 2 * nNodes); + + // mapping + bnd_dispBlock *= invMap * invMap; + } + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < bnd_dispBlock.rows(); i++) { + for (unsigned int j = 0; j < bnd_dispBlock.cols(); j++) { + // handle liquid state + // inexact integration pattern is more sparse than a nNodes x 2*nNodes block. + if ((j <= nNodes) || (i == nNodes - 1)) { + // row: add component offset and go node strides from there for each dispersion block entry + // col: add component offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + i * sNode, + offset + comp * sComp + j * sNode) + = -static_cast(parDiff[comp]) * bnd_dispBlock(i, j); // dispBlock <- D_p * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + // row: add current component offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) * invBetaP[comp] * bnd_dispBlock(i, j); // dispBlock <- D_s * invBeta * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * bnd_dispBlock(i, j); // - D_s * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + } + } + } + } + } + } + } + } + + /* right boundary cell */ + invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType] + _disc.nParCell[parType] - 1]); + + // numerical flux contribution for left interface of right boundary cell -> d f^*_0 / d cp + bnd_gStarDC.setZero(); + bnd_gStarDC.block(0, nNodes - 1, 1, nNodes + 1) = GBlock_r.block(0, 0, 1, nNodes + 1); + bnd_gStarDC.block(0, 0, 1, nNodes + 1) += GBlock_l.block(nNodes - 1, 0, 1, nNodes + 1); + bnd_gStarDC *= 0.5; + // dispBlock <- invMap * ( D_r * G_r - M^-1 * B * [G_r - g^*] ) + bnd_dispBlock.setZero(); + bnd_dispBlock.block(0, nNodes - 1, nNodes, nNodes + 1) = (_disc.Dr[_disc.offsetMetric[parType] + _disc.nParCell[parType] - 1] - _disc.parInvWeights[parType].asDiagonal() * B) * GBlock_r; + bnd_dispBlock.block(0, 0, nNodes, 2 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * bnd_gStarDC; + bnd_dispBlock *= invMap * invMap; + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < bnd_dispBlock.rows(); i++) { + for (unsigned int j = 0; j < bnd_dispBlock.cols(); j++) { + // handle liquid state + // inexact integration pattern is more sparse than a nNodes x 2*nNodes block. + if ((j <= nNodes) || (i == nNodes - 1)) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + comp * sComp + (_disc.nParCell[parType] - 2) * sCell + j * sNode) + = -static_cast(parDiff[comp]) * bnd_dispBlock(i, j); // dispBlock <- D_p * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + (_disc.nParCell[parType] - 1) * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (_disc.nParCell[parType] - 2) * sCell + j * sNode) + = -static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) * invBetaP[comp] * bnd_dispBlock(i, j); // dispBlock <- D_s * invBeta * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and over liquid states, add current bound state offset. go node strides from there for each dispersion block entry + // col: jump over previous cells and over liquid states, add current bound state offset. go back one cell and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + (_disc.nParCell[parType] - 1) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (_disc.nParCell[parType] - 2) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * bnd_dispBlock(i, j); // - D_s * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + } + } + } + } + } + } + } + } + + /* inner cells */ + + // auxiliary block [ d g(c) / d c ] for inner cells + MatrixXd GBlock = MatrixXd::Zero(nNodes, nNodes + 2); + GBlock.block(0, 1, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock(0, 0) -= 0.5 * _disc.parInvWeights[parType][0]; + GBlock(0, 1) += 0.5 * _disc.parInvWeights[parType][0]; + GBlock(nNodes - 1, nNodes) -= 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + GBlock(nNodes - 1, nNodes + 1) += 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + + // numerical flux contribution + MatrixXd gStarDC = MatrixXd::Zero(nNodes, 3 * nNodes); + gStarDC.block(0, nNodes - 1, 1, nNodes + 2) = GBlock.block(0, 0, 1, nNodes + 2); + gStarDC.block(0, 0, 1, nNodes + 1) += GBlock.block(nNodes - 1, 1, 1, nNodes + 1); + gStarDC.block(nNodes - 1, nNodes - 1, 1, nNodes + 2) += GBlock.block(nNodes - 1, 0, 1, nNodes + 2); + gStarDC.block(nNodes - 1, 2 * nNodes - 1, 1, nNodes + 1) += GBlock.block(0, 0, 1, nNodes + 1); + gStarDC *= 0.5; + + dispBlock.setZero(); + // dispersion block part without metrics + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) = -1.0 * _disc.parInvWeights[parType].asDiagonal() * B * GBlock; + dispBlock.block(0, 0, nNodes, 3 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * gStarDC; + dispBlock *= invMap * invMap; + + for (int cell = 1; cell < _disc.nParCell[parType] - 1; cell++) { + double invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType] + cell]); + + // add metric part, dependent on current cell + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) += _disc.Dr[_disc.offsetMetric[parType] + cell] * GBlock * invMap * invMap; + + // fill the jacobian: add dispersion block for each unbound and bound component, adjusted for the respective coefficients + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int j = 0; j < dispBlock.cols(); j++) { + // handle liquid state + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: add component offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + cell * sCell + i * sNode, + offset + comp * sComp + (cell - 1) * sCell + j * sNode) + = -static_cast(parDiff[comp]) * dispBlock(i, j); // dispBlock <- D_p * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + // handle surface diffusion of bound states. binding is handled in residualKernel(). + if (_hasSurfaceDiffusion[parType]) { + + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + // row: add component offset and jump over previous cells. Go node strides from there for each dispersion block entry + // col: jump over liquid states, add current bound state offset and jump over previous cells. Go back one cell and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + comp * sComp + cell * sCell + i * sNode, + offset + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + (cell - 1) * sCell + j * sNode) + = -static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) * invBetaP[comp] * dispBlock(i, j); // dispBlock <- D_s * invBeta * [ M^-1 * M_r * G_l + invMap * ( D * G_l - M^-1 * B * [G_l - g^*] ) ] + + /* add surface diffusion dispersion block to solid */ + if (!qsReaction[idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd]) { + // row: jump over previous cells and liquid states, add current bound state offset and go node strides from there for each dispersion block entry + // col: jump over previous cells and liquid states, go back one cell and add current bound state offset and go node strides from there for each dispersion block entry + _globalJac.coeffRef(offset + cell * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + i * sNode, + offset + (cell - 1) * sCell + idxr.strideParLiquid() + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ comp }) + bnd + j * sNode) + = -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * dispBlock(i, j); // - D_s * (Delta r / 2)^2 * (D_r D - M^-1 B D) + + } + } + } + } + } + } + } + } + // substract metric part in preparation of next iteration + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) -= _disc.Dr[_disc.offsetMetric[parType] + cell] * GBlock * invMap * invMap; + } + + } // if nCells > 1 + + return 1; + } + /** + * @brief adds jacobian entries which have been overwritten by the binding kernel (only use for surface diffusion combined with kinetic binding) + * @detail only adds the entries d RHS_i / d c^s_i, which lie on the diagonal + * @parType[in] current particle type + * @parSurfDiff[in] pointer to particle surface diffusion at current section and particle type + */ + int addSolidDGentries(unsigned int parType, const active* const parSurfDiff) { + + if (!_disc.parExactInt[parType]) + return addSolidDGentries_inexInt(parType, parSurfDiff); + + Indexer idxr(_disc); + + for (unsigned int col = 0; col < _disc.nPoints; col++) { + // Get jacobian iterator at first solid entry of first particle of current type + linalg::BandedEigenSparseRowIterator jac(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ col }) + idxr.strideParLiquid()); + + for (unsigned int cell = 0; cell < _disc.nParCell[parType]; cell++) + addDiagonalSolidJacobianEntries(_disc.DGjacParDispBlocks[_disc.offsetMetric[parType] + cell].block(0, _disc.nParNode[parType] + 1, _disc.nParNode[parType], _disc.nParNode[parType]), + jac, idxr, parSurfDiff, _binding[parType]->reactionQuasiStationarity(), parType); + } + + return 1; + } + /** + * @brief adds jacobian entries which have been overwritten by the binding kernel (only use for surface diffusion combined with kinetic binding) + * @detail only adds the entries d RHS_i / d c^s_i, which lie on the diagonal + * @parType[in] current particle type + * @parSurfDiff[in] pointer to particle surface diffusion at current section and particle type + */ + int addSolidDGentries_inexInt(unsigned int parType, const active* const parSurfDiff) { + + Indexer idxr(_disc); + + // (global) strides + unsigned int sCell = _disc.nParNode[parType] * idxr.strideParNode(parType); + unsigned int sNode = idxr.strideParNode(parType); + unsigned int sComp = 1u; + unsigned int nNodes = _disc.nParNode[parType]; + + // blocks to compute jacobian + Eigen::MatrixXd dispBlock; + Eigen::MatrixXd B = MatrixXd::Zero(nNodes, nNodes); + B(0, 0) = -1.0; B(nNodes - 1, nNodes - 1) = 1.0; + + // special case: one cell -> diffBlock \in R^(nParNodes x nParNodes), GBlock = parPolyDerM + if (_disc.nParCell[parType] == 1) { + + double invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType]]); + + if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab || _parCoreRadius[parType] != 0.0) + dispBlock = invMap * invMap * (_disc.Dr[parType] - _disc.parInvWeights[parType].asDiagonal() * B) * _disc.parPolyDerM[parType]; + + else { // special treatment of inner boundary node for spherical and cylindrical particles without particle core + + dispBlock = MatrixXd::Zero(nNodes, nNodes); + + // reduced system + dispBlock.block(1, 0, nNodes - 1, nNodes) + = (_disc.Dr[parType].block(1, 1, nNodes - 1, nNodes - 1) + - _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1)) + * _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, nNodes); + + // inner boundary node + dispBlock.block(0, 0, 1, nNodes) + = -(_disc.Ir[parType].segment(1, nNodes - 1).cwiseProduct( + _disc.parInvWeights[parType].segment(1, nNodes - 1).cwiseInverse()).cwiseProduct( + _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, 1))).transpose() + * _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, nNodes); + + dispBlock *= invMap * invMap; + } + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + // start at first solid entry + linalg::BandedEigenSparseRowIterator jac(_globalJac, offset + idxr.strideParLiquid()); + + for (unsigned int node = 0; node < _disc.nParNode[parType]; node++, jac += idxr.strideParLiquid()) { + + // @TODO test if we use correct diffusion parameter entry for more complicated cases, i.e. multiple comp, bnd states + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++, ++jac) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + jac[0] += -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * dispBlock(node, node); + } + } + } + } + } + } + else { + + /* boundary cells */ + // initialize dispersion and metric block matrices + MatrixXd bnd_dispBlock = MatrixXd::Zero(nNodes, 2 * nNodes); // boundary cell specific + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes); + + // auxiliary block [ d g(c) / d c ] for left boundary cell + MatrixXd GBlock_l = MatrixXd::Zero(nNodes, nNodes + 1); + GBlock_l.block(0, 0, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock_l(nNodes - 1, nNodes - 1) -= 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + GBlock_l(nNodes - 1, nNodes) += 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + // auxiliary block [ d g(c) / d c ] for right boundary cell + MatrixXd GBlock_r = MatrixXd::Zero(nNodes, nNodes + 1); + GBlock_r.block(0, 1, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock_r(0, 0) -= 0.5 * _disc.parInvWeights[parType][0]; + GBlock_r(0, 1) += 0.5 * _disc.parInvWeights[parType][0]; + + /* left boundary cell */ + int _cell = 0; + double invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType] + _cell]); + + // numerical flux contribution for right interface of left boundary cell -> d f^*_N / d cp + MatrixXd bnd_gStarDC = MatrixXd::Zero(nNodes, 2 * nNodes); + bnd_gStarDC.block(nNodes - 1, 0, 1, nNodes + 1) = GBlock_l.block(nNodes - 1, 0, 1, nNodes + 1); + bnd_gStarDC.block(nNodes - 1, nNodes - 1, 1, nNodes + 1) += GBlock_r.block(0, 0, 1, nNodes + 1); + bnd_gStarDC *= 0.5; + + // "standard" computation for slab-shaped particles and spherical, cylindrical particles without core + if (_parGeomSurfToVol[parType] == _disc.SurfVolRatioSlab || _parCoreRadius[parType] != 0.0) { + // dispBlock <- invMap^2 * ( D * G_l - M^-1 * B * [G_l - g^*] ) + bnd_dispBlock.block(0, 0, nNodes, nNodes + 1) = (_disc.Dr[_disc.offsetMetric[parType]] - _disc.parInvWeights[parType].asDiagonal() * B) * GBlock_l; + bnd_dispBlock.block(0, 0, nNodes, 2 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * bnd_gStarDC; + bnd_dispBlock *= invMap * invMap; + } + else { // special treatment of inner boundary node for spherical and cylindrical particles without particle core + + // inner boundary node + bnd_dispBlock.block(0, 0, 1, nNodes + 1) + = -(_disc.Ir[_disc.offsetMetric[parType]].segment(1, nNodes - 1).cwiseProduct( + _disc.parInvWeights[parType].segment(1, nNodes - 1).cwiseInverse()).cwiseProduct( + _disc.parPolyDerM[parType].block(1, 0, nNodes - 1, 1))).transpose() + * GBlock_l.block(1, 0, nNodes - 1, nNodes + 1); + + // reduced system for remaining nodes + bnd_dispBlock.block(1, 0, nNodes - 1, nNodes + 1) + = (_disc.Dr[_disc.offsetMetric[parType]].block(1, 1, nNodes - 1, nNodes - 1) + - _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1) + ) * GBlock_l.block(1, 0, nNodes - 1, nNodes + 1); + + bnd_dispBlock.block(1, 0, nNodes - 1, 2 * nNodes) + += _disc.parInvWeights[parType].segment(1, nNodes - 1).asDiagonal() * B.block(1, 1, nNodes - 1, nNodes - 1) * bnd_gStarDC.block(1, 0, nNodes - 1, 2 * nNodes); + + // mapping + bnd_dispBlock *= invMap * invMap; + } + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + // start at first solid entry of first cell + linalg::BandedEigenSparseRowIterator jac_left(_globalJac, offset + idxr.strideParLiquid()); + + for (unsigned int node = 0; node < _disc.nParNode[parType]; node++, jac_left += idxr.strideParLiquid()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++, ++jac_left) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + jac_left[0] += -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * bnd_dispBlock(node, node); + } + } + } + } + } + + /* right boundary cell */ + _cell = _disc.nParCell[parType] - 1; + invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType] + _cell]); + + bnd_gStarDC = MatrixXd::Zero(nNodes, 2 * nNodes); + // numerical flux contribution for left interface of right boundary cell -> d f^*_0 / d cp + bnd_gStarDC.setZero(); + bnd_gStarDC.block(0, nNodes - 1, 1, nNodes + 1) = GBlock_r.block(0, 0, 1, nNodes + 1); + bnd_gStarDC.block(0, 0, 1, nNodes + 1) += GBlock_l.block(nNodes - 1, 0, 1, nNodes + 1); + bnd_gStarDC *= 0.5; + // dispBlock <- invMap * ( D_r * G_r - M^-1 * B * [G_r - g^*] ) + bnd_dispBlock.setZero(); + bnd_dispBlock.block(0, nNodes - 1, nNodes, nNodes + 1) = (_disc.Dr[_disc.offsetMetric[parType] + _cell] - _disc.parInvWeights[parType].asDiagonal() * B) * GBlock_r; + bnd_dispBlock.block(0, 0, nNodes, 2 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * bnd_gStarDC; + bnd_dispBlock *= invMap * invMap; + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + // start at first solid entry of last cell + linalg::BandedEigenSparseRowIterator jac_right(_globalJac, offset + (_disc.nParCell[parType] - 1) * sCell + idxr.strideParLiquid()); + + for (unsigned int node = 0; node < _disc.nParNode[parType]; node++, jac_right += idxr.strideParLiquid()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++, ++jac_right) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + jac_right[0] += -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * bnd_dispBlock(node, _disc.nParNode[parType] + node); + } + } + } + } + } + + /* inner cells */ + + // auxiliary block [ d g(c) / d c ] for inner cells + MatrixXd GBlock = MatrixXd::Zero(nNodes, nNodes + 2); + GBlock.block(0, 1, nNodes, nNodes) = _disc.parPolyDerM[parType]; + GBlock(0, 0) -= 0.5 * _disc.parInvWeights[parType][0]; + GBlock(0, 1) += 0.5 * _disc.parInvWeights[parType][0]; + GBlock(nNodes - 1, nNodes) -= 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + GBlock(nNodes - 1, nNodes + 1) += 0.5 * _disc.parInvWeights[parType][nNodes - 1]; + + // numerical flux contribution + MatrixXd gStarDC = MatrixXd::Zero(nNodes, 3 * nNodes); + gStarDC.block(0, nNodes - 1, 1, nNodes + 2) = GBlock.block(0, 0, 1, nNodes + 2); + gStarDC.block(0, 0, 1, nNodes + 1) += GBlock.block(nNodes - 1, 1, 1, nNodes + 1); + gStarDC.block(nNodes - 1, nNodes - 1, 1, nNodes + 2) += GBlock.block(nNodes - 1, 0, 1, nNodes + 2); + gStarDC.block(nNodes - 1, 2 * nNodes - 1, 1, nNodes + 1) += GBlock.block(0, 0, 1, nNodes + 1); + gStarDC *= 0.5; + + dispBlock.setZero(); + // dispersion block part without metrics + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) = -1.0 * _disc.parInvWeights[parType].asDiagonal() * B * GBlock; + dispBlock.block(0, 0, nNodes, 3 * nNodes) += _disc.parInvWeights[parType].asDiagonal() * B * gStarDC; + + for (int cell = 1; cell < _disc.nParCell[parType] - 1; cell++) { + + // add metric part, dependent on current cell + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) += _disc.Dr[_disc.offsetMetric[parType] + cell] * GBlock; + invMap = (2.0 / _disc.deltaR[_disc.offsetMetric[parType] + cell]); + dispBlock *= invMap * invMap; + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + + unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + // start at first solid entry of current inner cell + linalg::BandedEigenSparseRowIterator jac_inner(_globalJac, offset + cell * sCell + idxr.strideParLiquid()); + + for (unsigned int node = 0; node < _disc.nParNode[parType]; node++, jac_inner += idxr.strideParLiquid()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[parType * _disc.nComp + comp]; bnd++, ++jac_inner) { + if (static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)]) != 0.0) { + jac_inner[0] += -(static_cast(parSurfDiff[getOffsetSurfDiff(parType, comp, bnd)])) * dispBlock(node, _disc.nParNode[parType] + node); + } + } + } + } + } + + // substract metric part in preparation of next iteration + dispBlock /= invMap * invMap; + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) -= _disc.Dr[_disc.offsetMetric[parType] + cell] * GBlock; + } + } // if nCells > 1 + + return 1; + } + /** + * @brief analytically calculates the convection dispersion jacobian for the nodal DG scheme + */ + int calcConvDispCollocationDGSEMJacobian() { + + Indexer idxr(_disc); + + unsigned int offC = idxr.offsetC(); + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + if (nCells >= 3u) { + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[1]; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at second cell and component + + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + /* left cell */ + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[0]; + + if (nCells != 1u) { // "standard" case + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + else { // special case + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < nNodes * 2u; j++) { + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /* right cell */ + if (nCells != 1u) { // "standard" case + dispBlock = _disc.DGjacAxDispBlocks[std::min(nCells, 3u) - 1]; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + (nCells - 1) * idxr.strideColCell()); // row iterator starting at last cell + + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on first entry of previous cell + MatrixXd convBlock = _disc.DGjacAxConvBlock; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + + if (_disc.velocity >= 0.0) { // forward flow upwind convection + // special inlet DOF treatment for first cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(0, 0); // only first node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + //jacIt[0] = -convBlock(i, 0); // dependency on inlet DOFs is handled in _jacInlet + for (unsigned int j = 1; j < convBlock.cols(); j++) { + jacIt[((j - 1) - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + // remaining cells + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at previous cells last node and go to node j. + jacIt[-idxr.strideColNode() + (j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + } + else { // backward flow upwind convection + // non-inlet cells + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at current cells first node and go to node j. + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + // special inlet DOF treatment for last cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(convBlock.rows() - 1, convBlock.cols() - 1); // only last node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols() - 1; j++) { + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + + return 1; + } + /** + * @brief inserts a state block with different factors for components into the system jacobian. + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] offRowToCol column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + * @param [in] stateFactor state dependend factors + * @param [in] nStates how many states are concerned, defaults to nComp + * @param [in] strideDead how many (dead) states to be jumped over after each node (rows) + */ + template + void insertJacBlockStateDepFactor(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offRowToCol, Indexer& idxr, unsigned int nCells, ParamType* stateFactor, unsigned int strideNode, unsigned int nStates, unsigned int strideDead = 0) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++, jac += strideDead) { + for (unsigned int state = 0; state < nStates; state++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * strideNode + offRowToCol] = block(i, j) * static_cast(stateFactor[state]); + } + } + } + } + } + /** + * @brief inserts a liquid state block with different factors for components into the system jacobian + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] offRowToCol column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + * @param [in] Compfactor component dependend factors + */ + template + void insertCompDepLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offRowToCol, Indexer& idxr, unsigned int nCells, ParamType* Compfactor, unsigned int strideNode, unsigned int strideBound = 0) { + insertJacBlockStateDepFactor(block, jac, offRowToCol, idxr, nCells, Compfactor, strideNode, _disc.nComp, strideBound); + } + /** + * @brief adds a state block into the system jacobian. + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] offRowToCol column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + * @param [in] stateFactor state dependend factors + * @param [in] strideDead how many (dead) states to be jumped over after each state block + * @param [in] nStates how many states are concerned, defaults to nComp + */ + void addJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offRowToCol, Indexer& idxr, unsigned int nCells, unsigned int strideNode, unsigned int nStates, unsigned int strideDead = 0) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++, jac += strideDead) { + for (unsigned int state = 0; state < nStates; state++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * strideNode + offRowToCol] += block(i, j); + } + } + } + } + } + /** + * @brief adds a state block into the system jacobian. + * @param [in] block (sub)block whose diagonal entries are to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] idxr Indexer + * @param [in] surfDiff pointer to surfaceDiffusion storage + * @param [in] nonKinetic pointer to binding kinetics + * @param [in] type particle type + */ + template + void addDiagonalSolidJacobianEntries(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, Indexer& idxr, ParamType* surfDiff, const int* nonKinetic, unsigned int type) { + + for (unsigned int i = 0; i < block.rows(); i++, jac += idxr.strideParLiquid()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[type * _disc.nComp + comp]; bnd++, ++jac) { + if (static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]) != 0.0 + && !nonKinetic[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]) { + // row, col: at current node and bound state + jac[0] += block(i, i) + * static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]); + } + } + } + } + } + /** + * @brief adds liquid state blocks for all components to the system jacobian + * @param [in] block to be added + * @param [in] jac row iterator at first (i.e. upper left) entry + * @param [in] column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + */ + void addLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offCol, Indexer& idxr, unsigned int nCells, unsigned int strideNode, unsigned int strideBound = 0) { + addJacBlock(block, jac, offCol, idxr, nCells, strideNode, _disc.nComp, strideBound); + } + /** + * @brief adds a state block into the particle jacobian. + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] idxr Indexer + * @param [in] idxr parDiff pointer to particle diffusion parameters + * @param [in] idxr surfDiff pointer to particle surface diffusion parameters + * @param [in] idxr beta_p pointer to particle porosity parameters + * @param [in] idxr nonKinetic pointer to binding kinetics parameters + * @param [in] type particle type + * @param [in] nBlocks number of blocks, i.e. cells/elements, to be inserted + * @param [in] offRowToCol column to row offset (i.e. start at upper left corner of block) + */ + void insertParJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, Indexer& idxr, const active* const parDiff, const active* const surfDiff, const double* const beta_p, const int* nonKinetic, unsigned int type, unsigned int nBlocks, int offRowToCol) { + + for (unsigned int cell = 0; cell < nBlocks; cell++) { + for (unsigned int i = 0; i < block.rows(); i++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + /* liquid on liquid blocks */ + // row: at current node and component; col: jump to node j + jac[(j - i) * idxr.strideParNode(type) + offRowToCol] = block(i, j) * static_cast(parDiff[comp]); + } + /* liquid on solid blocks */ + for (unsigned int bnd = 0; bnd < _disc.nBound[type * _disc.nComp + comp]; bnd++) { + if (static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]) != 0.0) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node and component; col: jump to node j and to current bound state + jac[(j - i) * idxr.strideParNode(type) + offRowToCol + idxr.strideParLiquid() - comp + + idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd + ] + = block(i, j) * static_cast(beta_p[comp]) + * static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]); + } + } + } + } + /* solid on solid blocks */ + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int bnd = 0; bnd < _disc.nBound[type * _disc.nComp + comp]; bnd++, ++jac) { + if (static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]) != 0.0 + && !nonKinetic[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node and bound state; col: jump to node j + jac[(j - i) * idxr.strideParNode(type) + offRowToCol + bnd] + = block(i, j) + * static_cast(surfDiff[_disc.offsetSurfDiff[idxr.offsetBoundComp(ParticleTypeIndex{ type }, ComponentIndex{ comp }) + bnd]]); + } + } + } + } + } + } + } + /** + * @brief analytically calculates the convection dispersion jacobian for the exact integration (here: modal) DG scheme + */ + int calcConvDispDGSEMJacobian() { + + Indexer idxr(_disc); + + unsigned int offC = idxr.offsetC(); + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells (exist only if nCells >= 5) */ + if (nCells >= 5) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell() * 2); // row iterator starting at third cell, first component + // insert all (nCol - 4) inner cell blocks + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[2], jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, _disc.nCol - 4u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + + /* boundary cell neighbours (exist only if nCells >= 4) */ + if (nCells >= 4) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at second cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes + 1), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + + jacIt += (_disc.nCol - 4) * idxr.strideColCell(); // move iterator to preultimate cell (already at third cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[nCells > 4 ? 3 : 2].block(0, 0, nNodes, 3 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + + /* boundary cells (exist only if nCells >= 3) */ + if (nCells >= 3) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes + 1), jacIt, 0, idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + + jacIt += (_disc.nCol - 2) * idxr.strideColCell(); // move iterator to last cell (already at second cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[std::min(nCells, 5u) - 1u].block(0, 0, nNodes, 2 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + + /* For special cases nCells = 1, 2, 3, some cells still have to be treated separately*/ + + if (nCells == 1) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + // insert the only block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + else if (nCells == 2) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + // left Bacobian block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + // right Bacobian block, iterator is already moved to second cell + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 2 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + else if (nCells == 3) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at first cell, first component + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0]), idxr.strideColNode()); + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + int sComp = idxr.strideColComp(); + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + + linalg::BandedEigenSparseRowIterator jac(_globalJac, offC); + + if (_disc.velocity >= 0.0) { // Forward flow + // special inlet DOF treatment for inlet (first) cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(0); // only first cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 1, nNodes, nNodes), jac, 0, idxr, 1, idxr.strideColNode()); + if (_disc.nCol > 1) // iterator already moved to second cell + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, -idxr.strideColNode(), idxr, _disc.nCol - 1, idxr.strideColNode()); + } + else { // Backward flow + // non-inlet cells first + if (_disc.nCol > 1) + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, 0, idxr, _disc.nCol - 1, idxr.strideColNode()); + // special inlet DOF treatment for inlet (last) cell. Iterator already moved to last cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(_disc.DGjacAxConvBlock.cols() - 1); // only last cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 0, nNodes, nNodes), jac, 0, idxr, 1, idxr.strideColNode()); + } + + return 1; + } + /** + * @brief analytically calculates the static (per section) bulk jacobian (inlet DOFs included!) + * @return 1 if jacobain estimation fits the predefined pattern of the jacobian, 0 if not. + */ + int calcStaticAnaBulkJacobian() { + + // DG convection dispersion Jacobian + if (_disc.exactInt) + calcConvDispDGSEMJacobian(); + else + calcConvDispCollocationDGSEMJacobian(); + + return _globalJac.isCompressed(); // if matrix lost its compressed storage, the pattern did not fit. + } + + /** + * @brief analytically calculates the static (per section) particle jacobian + * @return 1 if jacobain calculation fits the predefined pattern of the jacobian, 0 if not. + */ + int calcStaticAnaParticleDispJacobian(unsigned int parType, unsigned int colNode, const active* const parDiff, const active* const parSurfDiff, const double* const invBetaP) { + + // DG particle dispersion Jacobian + if(_disc.parExactInt[parType]) + calcParticleDGSEMJacobian(parType, colNode, parDiff, parSurfDiff, invBetaP); + else // deprecated + calcParticleCollocationDGSEMJacobian(parType, colNode, parDiff, parSurfDiff, invBetaP); + + return _globalJac.isCompressed(); // if matrix lost its compressed storage, the calculation did not fit the pre-defined pattern. + } + + + void setJacobianPattern_GRM(SparseMatrix& globalJ, unsigned int secIdx, bool hasBulkReaction) { + + Indexer idxr(_disc); + + std::vector tripletList; + // reserve space for all entries + int bulkEntries = nConvDispEntries(); + if (hasBulkReaction) + bulkEntries += _disc.nPoints * _disc.nComp * _disc.nComp; // add nComp entries for every component at each discrete bulk point + + // particle + int addTimeDer = 0; // additional time derivative entries: bound states in particle dispersion equation + int isothermNNZ = 0; + int particleEntries = 0; + for (int type = 0; type < _disc.nParType; type++) { + isothermNNZ = (idxr.strideParNode(type)) * _disc.nParPoints[type] * _disc.strideBound[type]; // every bound satte might depend on every bound and liquid state + addTimeDer = _disc.nParPoints[type] * _disc.strideBound[type]; + particleEntries += calcParDispNNZ(type) + addTimeDer + isothermNNZ; + } + + int fluxEntries = 4 * _disc.nParType * _disc.nPoints * _disc.nComp; + + tripletList.reserve(fluxEntries + bulkEntries + particleEntries); + + // NOTE: inlet and jacF flux jacobian are set in calc jacobian function (identity matrices) + // Note: flux jacobian (identity matrix) is handled in calc jacobian function + + // convection dispersion bulk jacobian + setConvDispJacPattern(tripletList); + + // bulk reaction jacobian + if (hasBulkReaction) { + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int toComp = 0; toComp < _disc.nComp; toComp++) { + tripletList.push_back(T(idxr.offsetC() + colNode * idxr.strideColNode() + comp * idxr.strideColComp(), + idxr.offsetC() + colNode * idxr.strideColNode() + toComp * idxr.strideColComp(), + 0.0)); + } + } + } + } + + // particle jacobian (including isotherm and time derivative) + for (int colNode = 0; colNode < _disc.nPoints; colNode++) { + for (int type = 0; type < _disc.nParType; type++) { + setParJacPattern(tripletList, type, colNode, secIdx); + } + } + + // flux jacobians + for (unsigned int type = 0; type < _disc.nParType; type++) { + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + // add Cl on Cp entries + // row: add bulk offset, jump over previous nodes and components + // col: add flux offset to current parType, jump over previous nodes and components + tripletList.push_back(T(idxr.offsetC() + colNode * idxr.strideColNode() + comp * idxr.strideColComp(), + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ colNode }) + (_disc.nParPoints[type] - 1) * idxr.strideParNode(type) + comp * idxr.strideParComp(), 0.0)); + + // add Cp on Cl entries + if(!_disc.parExactInt[type]) + // row: add particle offset to current parType and particle, go to last node and add component offset + // col: add flux offset to current component, jump over previous nodes and components + tripletList.push_back(T(idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ colNode }) + + (_disc.nParPoints[type] - 1) * idxr.strideParNode(type) + comp * idxr.strideParComp(), + idxr.offsetC() + colNode * idxr.strideColNode() + comp, 0.0)); + else { + for (unsigned int node = 0; node < _disc.nParNode[type]; node++) { + // row: add particle offset to current parType and particle, go to last cell and current node and add component offset + // col: add flux offset to current component, jump over previous nodes and components + tripletList.push_back(T(idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ colNode }) + (_disc.nParCell[type] - 1) * _disc.nParNode[type] * idxr.strideParNode(type) + + node * idxr.strideParNode(type) + comp * idxr.strideParComp(), + idxr.offsetC() + colNode * idxr.strideColNode() + comp, 0.0)); + } + } + } + } + } + + globalJ.setFromTriplets(tripletList.begin(), tripletList.end()); + } + + int calcFluxJacobians(unsigned int secIdx) { + + Indexer idxr(_disc); + + for (unsigned int type = 0; type < _disc.nParType; type++) { + + // lifting matrix entry for exact integration scheme depends on metrics for sphere and cylinder + double exIntLiftContribution = _disc.Ir[_disc.offsetMetric[type] + _disc.nParCell[type] - 1][_disc.nParNode[type] - 1]; + if (_parGeomSurfToVol[type] == _disc.SurfVolRatioSlab) + exIntLiftContribution = 1.0; + + // Ordering of diffusion: + // sec0type0comp0, sec0type0comp1, sec0type0comp2, sec0type1comp0, sec0type1comp1, sec0type1comp2, + // sec1type0comp0, sec1type0comp1, sec1type0comp2, sec1type1comp0, sec1type1comp1, sec1type1comp2, ... + active const* const filmDiff = getSectionDependentSlice(_filmDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + + linalg::BandedEigenSparseRowIterator jacCl(_globalJac, idxr.offsetC()); + linalg::BandedEigenSparseRowIterator jacCp(_globalJac, idxr.offsetCp(ParticleTypeIndex{ type }) + (_disc.nParPoints[type] - 1) * idxr.strideParNode(type)); + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++) + { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jacCp, ++jacCl) { + // add Cl on Cl entries (added since these entries are also touched by bulk jacobian) + // row: already at bulk phase. already at current node and component. + // col: already at bulk phase. already at current node and component. + jacCl[0] += static_cast(filmDiff[comp]) * (1.0 - static_cast(_colPorosity)) / static_cast(_colPorosity) + * _parGeomSurfToVol[type] / static_cast(_parRadius[type]) + * _parTypeVolFrac[type + colNode * _disc.nParType].getValue(); + // add Cl on Cp entries (added since these entries are also touched by bulk jacobian) + // row: already at bulk phase. already at current node and component. + // col: go to current particle phase entry. + jacCl[jacCp.row() - jacCl.row()] = -static_cast(filmDiff[comp]) * (1.0 - static_cast(_colPorosity)) / static_cast(_colPorosity) + * _parGeomSurfToVol[type] / static_cast(_parRadius[type]) + * _parTypeVolFrac[type + colNode * _disc.nParType].getValue(); + + // add Cp on Flux entries + if (!_disc.parExactInt[type]) { + // row: already at particle. already at current node and liquid state. + // col: already at particle. already at current node and liquid state. + jacCp[0] += static_cast(filmDiff[comp]) * 2.0 / _disc.deltaR[_disc.offsetMetric[type]] * _disc.parInvWeights[type][0] / static_cast(_parPorosity[type]) / static_cast(_poreAccessFactor[type * _disc.nComp + comp]); + // row: already at particle. already at current node and liquid state. + // col: go to current bulk phase. + jacCp[jacCl.row() - jacCp.row()] = -static_cast(filmDiff[comp]) * 2.0 / _disc.deltaR[_disc.offsetMetric[type]] * _disc.parInvWeights[type][0] / static_cast(_parPorosity[type]) / static_cast(_poreAccessFactor[type * _disc.nComp + comp]); + } + else { + unsigned int entry = jacCp.row(); + for (int node = _disc.parPolyDeg[type]; node >= 0; node--, jacCp -= idxr.strideParNode(type)) { + // row: already at particle. Already at current node and liquid state. + // col: original entry at outer node. + jacCp[entry - jacCp.row()] + += static_cast(filmDiff[comp]) * 2.0 / _disc.deltaR[_disc.offsetMetric[type]] * _disc.parInvMM[_disc.offsetMetric[type] + _disc.nParCell[type] - 1](node, _disc.nParNode[type] - 1) * exIntLiftContribution / static_cast(_parPorosity[type]) / static_cast(_poreAccessFactor[type * _disc.nComp + comp]); + // row: already at particle. Already at current node and liquid state. + // col: go to current bulk phase. + jacCp[jacCl.row() - jacCp.row()] + = -static_cast(filmDiff[comp]) * 2.0 / _disc.deltaR[_disc.offsetMetric[type]] * _disc.parInvMM[_disc.offsetMetric[type] + _disc.nParCell[type] - 1](node, _disc.nParNode[type] - 1) * exIntLiftContribution / static_cast(_parPorosity[type]) / static_cast(_poreAccessFactor[type * _disc.nComp + comp]); + } + // set back iterator to first node as required by component loop + jacCp += _disc.nParNode[type] * idxr.strideParNode(type); + } + } + if (colNode < _disc.nPoints - 1) // execute iteration statement only when condition is true in next loop. + jacCp += _disc.strideBound[type] + (_disc.nParPoints[type] - 1) * idxr.strideParNode(type); + } + } + + return 1; + } + + int calcStaticAnaJacobian_GRM(unsigned int secIdx) { + + Indexer idxr(_disc); + // inlet and bulk jacobian + calcStaticAnaBulkJacobian(); + + // particle jacobian (without isotherm, which is handled in residualKernel) + for (int colNode = 0; colNode < _disc.nPoints; colNode++) { + for (int type = 0; type < _disc.nParType; type++) { + + // Prepare parameters + const active* const parDiff = getSectionDependentSlice(_parDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + + // Ordering of particle surface diffusion: + // bnd0comp0, bnd0comp1, bnd0comp2, bnd1comp0, bnd1comp1, bnd1comp2 + const active* const parSurfDiff = getSectionDependentSlice(_parSurfDiffusion, _disc.strideBound[_disc.nParType], secIdx) + _disc.nBoundBeforeType[type]; + + double* invBetaP = new double[_disc.nComp]; + for (int comp = 0; comp < _disc.nComp; comp++) { + invBetaP[comp] = (1.0 - static_cast(_parPorosity[type])) / (static_cast(_poreAccessFactor[_disc.nComp * type + comp]) * static_cast(_parPorosity[type])); + } + + calcStaticAnaParticleDispJacobian(type, colNode, parDiff, parSurfDiff, invBetaP); + } + } + + calcFluxJacobians(secIdx); + + return _globalJac.isCompressed(); // check if the jacobian estimation fits the pattern + } + +}; + +} // namespace model +} // namespace cadet + +#endif // LIBCADET_GENERALRATEMODELDG_HPP_ diff --git a/src/libcadet/model/LumpedRateModelWithPoresDG-InitialConditions.cpp b/src/libcadet/model/LumpedRateModelWithPoresDG-InitialConditions.cpp new file mode 100644 index 000000000..0898d641c --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithPoresDG-InitialConditions.cpp @@ -0,0 +1,1232 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2022: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/LumpedRateModelWithPoresDG.hpp" +#include "model/BindingModel.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "ParamReaderHelper.hpp" +#include "AdUtils.hpp" +#include "model/parts/BindingCellKernel.hpp" +#include "SimulationTypes.hpp" +#include "SensParamUtil.hpp" +#include "linalg/Subset.hpp" + +#include +#include + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include "ParallelSupport.hpp" +#ifdef CADET_PARALLELIZE +#include +#endif + +namespace cadet +{ + + namespace model + { + + int LumpedRateModelWithPoresDG::multiplexInitialConditions(const cadet::ParameterId& pId, unsigned int adDirection, double adValue) + { + if (_singleBinding) + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initCp[pId.component]); + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initCp[t * _disc.nComp + pId.component].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initQ[_disc.nBoundBeforeType[0] + _disc.boundOffset[pId.component] + pId.boundState]); + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initQ[_disc.nBoundBeforeType[t] + _disc.boundOffset[t * _disc.nComp + pId.component] + pId.boundState].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + else + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initCp[pId.particleType * _disc.nComp + pId.component]); + _initCp[pId.particleType * _disc.nComp + pId.component].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + _sensParams.insert(&_initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState]); + _initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState].setADValue(adDirection, adValue); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + return 0; + } + + int LumpedRateModelWithPoresDG::multiplexInitialConditions(const cadet::ParameterId& pId, double val, bool checkSens) + { + if (_singleBinding) + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initCp[pId.component])) + return -1; + + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initCp[t * _disc.nComp + pId.component].setValue(val); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType == ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initQ[_disc.nBoundBeforeType[0] + _disc.boundOffset[pId.component] + pId.boundState])) + return -1; + + for (unsigned int t = 0; t < _disc.nParType; ++t) + _initQ[_disc.nBoundBeforeType[t] + _disc.boundOffset[t * _disc.nComp + pId.component] + pId.boundState].setValue(val); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + else + { + if ((pId.name == hashString("INIT_CP")) && (pId.section == SectionIndep) && (pId.boundState == BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initCp[pId.particleType * _disc.nComp + pId.component])) + return -1; + + _initCp[pId.particleType * _disc.nComp + pId.component].setValue(val); + } + else if (pId.name == hashString("INIT_CP")) + return -1; + + if ((pId.name == hashString("INIT_Q")) && (pId.section == SectionIndep) && (pId.boundState != BoundStateIndep) && (pId.particleType != ParTypeIndep) && (pId.component != CompIndep) && (pId.reaction == ReactionIndep)) + { + if (checkSens && !contains(_sensParams, &_initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState])) + return -1; + + _initQ[_disc.nBoundBeforeType[pId.particleType] + _disc.boundOffset[pId.particleType * _disc.nComp + pId.component] + pId.boundState].setValue(val); + } + else if (pId.name == hashString("INIT_Q")) + return -1; + } + return 0; + } + + void LumpedRateModelWithPoresDG::applyInitialCondition(const SimulationState& simState) const + { + Indexer idxr(_disc); + + // Check whether full state vector is available as initial condition + if (!_initState.empty()) + { + std::fill(simState.vecStateY, simState.vecStateY + idxr.offsetC(), 0.0); + std::copy(_initState.data(), _initState.data() + numPureDofs(), simState.vecStateY + idxr.offsetC()); + + if (!_initStateDot.empty()) + { + std::fill(simState.vecStateYdot, simState.vecStateYdot + idxr.offsetC(), 0.0); + std::copy(_initStateDot.data(), _initStateDot.data() + numPureDofs(), simState.vecStateYdot + idxr.offsetC()); + } + else + std::fill(simState.vecStateYdot, simState.vecStateYdot + numDofs(), 0.0); + + return; + } + + double* const stateYbulk = simState.vecStateY + idxr.offsetC(); + + // Loop over column cells + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + // Loop over components in cell + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[point * idxr.strideColNode() + comp * idxr.strideColComp()] = static_cast(_initC[comp]); + } + + // Loop over particles + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + const unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ point }); + + // Initialize c_p + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + simState.vecStateY[offset + comp] = static_cast(_initCp[comp + _disc.nComp * type]); + + // Initialize q + active const* const iq = _initQ.data() + _disc.nBoundBeforeType[type]; + for (unsigned int bnd = 0; bnd < _disc.strideBound[type]; ++bnd) + simState.vecStateY[offset + idxr.strideParLiquid() + bnd] = static_cast(iq[bnd]); + } + } + } + + void LumpedRateModelWithPoresDG::readInitialCondition(IParameterProvider& paramProvider) + { + _initState.clear(); + _initStateDot.clear(); + + // Check if INIT_STATE is present + if (paramProvider.exists("INIT_STATE")) + { + const std::vector initState = paramProvider.getDoubleArray("INIT_STATE"); + _initState = std::vector(initState.begin(), initState.begin() + numPureDofs()); + + // Check if INIT_STATE contains the full state and its time derivative + if (initState.size() >= 2 * numPureDofs()) + _initStateDot = std::vector(initState.begin() + numPureDofs(), initState.begin() + 2 * numPureDofs()); + return; + } + + const std::vector initC = paramProvider.getDoubleArray("INIT_C"); + + if (initC.size() < _disc.nComp) + throw InvalidParameterException("INIT_C does not contain enough values for all components"); + + ad::copyToAd(initC.data(), _initC.data(), _disc.nComp); + + // Check if INIT_CP is present + if (paramProvider.exists("INIT_CP")) + { + const std::vector initCp = paramProvider.getDoubleArray("INIT_CP"); + + if (((initCp.size() < _disc.nComp) && _singleBinding) || ((initCp.size() < _disc.nComp * _disc.nParType) && !_singleBinding)) + throw InvalidParameterException("INIT_CP does not contain enough values for all components"); + + if (!_singleBinding) + ad::copyToAd(initCp.data(), _initCp.data(), _disc.nComp * _disc.nParType); + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initCp.data(), _initCp.data() + t * _disc.nComp, _disc.nComp); + } + } + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initC.data(), _initCp.data() + t * _disc.nComp, _disc.nComp); + } + + std::vector initQ; + if (paramProvider.exists("INIT_Q")) + initQ = paramProvider.getDoubleArray("INIT_Q"); + + if (initQ.empty() || (_disc.strideBound[_disc.nParType] == 0)) + return; + + if ((_disc.strideBound[_disc.nParType] > 0) && (((initQ.size() < _disc.strideBound[_disc.nParType]) && !_singleBinding) || ((initQ.size() < _disc.strideBound[0]) && _singleBinding))) + throw InvalidParameterException("INIT_Q does not contain enough values for all bound states"); + + if (!_singleBinding) + ad::copyToAd(initQ.data(), _initQ.data(), _disc.strideBound[_disc.nParType]); + else + { + for (unsigned int t = 0; t < _disc.nParType; ++t) + ad::copyToAd(initQ.data(), _initQ.data() + _disc.nBoundBeforeType[t], _disc.strideBound[t]); + } + } + + /** + * @brief Computes consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function performs step 1. See consistentInitialTimeDerivative() for step 2. + * + * This function is to be used with consistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithPoresDG::consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // Step 1: Solve algebraic equations + + // Step 1a: Compute quasi-stationary binding model state + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_binding[type]->hasQuasiStationaryReactions()) + continue; + + // Copy quasi-stationary binding mask to a local array that also includes the mobile phase + std::vector qsMask(_disc.nComp + _disc.strideBound[type], false); + int const* const qsMaskSrc = _binding[type]->reactionQuasiStationarity(); + std::copy_n(qsMaskSrc, _disc.strideBound[type], qsMask.data() + _disc.nComp); + + // Activate mobile phase components that have at least one active bound state + unsigned int bndStartIdx = 0; + unsigned int numActiveComp = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd) + { + if (qsMaskSrc[bndStartIdx + bnd]) + { + ++numActiveComp; + qsMask[comp] = true; + break; + } + } + + bndStartIdx += _disc.nBound[_disc.nComp * type + comp]; + } + + const linalg::ConstMaskArray mask{ qsMask.data(), static_cast(_disc.nComp + _disc.strideBound[type]) }; + const int probSize = linalg::numMaskActive(mask); + + //Problem capturing variables here +#ifdef CADET_PARALLELIZE + BENCH_SCOPE(_timerConsistentInitPar); + tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints), [&](std::size_t pblk) +#else + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) +#endif + { + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + // Reuse memory of band matrix for dense matrix + linalg::DenseMatrixView fullJacobianMatrix(_globalJacDisc.valuePtr() + _globalJacDisc.outerIndexPtr()[idxr.offsetCp(ParticleTypeIndex{ type }) - idxr.offsetC() + pblk], nullptr, mask.len, mask.len); + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(pblk / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[pblk % _disc.nNodes])) / _disc.length_; + + // Get workspace memory + BufferedArray nonlinMemBuffer = tlmAlloc.array(_nonlinearSolver->workspaceSize(probSize)); + double* const nonlinMem = static_cast(nonlinMemBuffer); + + BufferedArray solutionBuffer = tlmAlloc.array(probSize); + double* const solution = static_cast(solutionBuffer); + + BufferedArray fullResidualBuffer = tlmAlloc.array(mask.len); + double* const fullResidual = static_cast(fullResidualBuffer); + + BufferedArray fullXBuffer = tlmAlloc.array(mask.len); + double* const fullX = static_cast(fullXBuffer); + + BufferedArray jacobianMemBuffer = tlmAlloc.array(probSize * probSize); + linalg::DenseMatrixView jacobianMatrix(static_cast(jacobianMemBuffer), _globalJacDisc.outerIndexPtr() + pblk * probSize, probSize, probSize); + + BufferedArray conservedQuantsBuffer = tlmAlloc.array(numActiveComp); + double* const conservedQuants = static_cast(conservedQuantsBuffer); + + const parts::cell::CellParameters cellResParams + { + _disc.nComp, + _disc.nBound + _disc.nComp * type, + _disc.boundOffset + _disc.nComp * type, + _disc.strideBound[type], + _binding[type]->reactionQuasiStationarity(), + _parPorosity[type], + _poreAccessFactor.data() + _disc.nComp * type, + _binding[type], + (_dynReaction[type] && (_dynReaction[type]->numReactionsCombined() > 0)) ? _dynReaction[type] : nullptr + }; + + const int localOffsetToParticle = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); + const int localOffsetInParticle = idxr.strideParLiquid(); + + // Get pointer to q variables in a shell of particle pblk + double* const qShell = vecStateY + localOffsetToParticle + localOffsetInParticle; + active* const localAdRes = adJac.adRes ? adJac.adRes + localOffsetToParticle : nullptr; + active* const localAdY = adJac.adY ? adJac.adY + localOffsetToParticle : nullptr; + + const ColumnPosition colPos{ z, 0.0, static_cast(_parRadius[type]) * 0.5 }; + + // Determine whether nonlinear solver is required + if (!_binding[type]->preConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideParLiquid(), tlmAlloc)) + CADET_PAR_CONTINUE; + + // Extract initial values from current state + linalg::selectVectorSubset(qShell - _disc.nComp, mask, solution); + + // Save values of conserved moieties + const double epsQ = 1.0 - static_cast(_parPorosity[type]); + linalg::conservedMoietiesFromPartitionedMask(mask, _disc.nBound + type * _disc.nComp, _disc.nComp, qShell - _disc.nComp, conservedQuants, static_cast(_parPorosity[type]), epsQ); + + std::function jacFunc; + // if (localAdY && localAdRes) + // { + // jacFunc = [&](double const* const x, linalg::detail::DenseMatrixBase& mat) + // { + // // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // // and initialize residuals with zero (also resetting directional values) + // ad::copyToAd(qShell - _disc.nComp, localAdY, mask.len); + // // @todo Check if this is necessary + // ad::resetAd(localAdRes, mask.len); + // + // // Prepare input vector by overwriting masked items + // linalg::applyVectorSubset(x, mask, localAdY); + // + // // Call residual function + // parts::cell::residualKernel( + // simTime.t, simTime.secIdx, colPos, localAdY, nullptr, localAdRes, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + // ); + // + //#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + // std::copy_n(qShell - _disc.nComp, mask.len, fullX); + // linalg::applyVectorSubset(x, mask, fullX); + // + // // Compute analytic Jacobian + // parts::cell::residualKernel( + // simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + // ); + // + // // Compare + // const double diff = ad::compareDenseJacobianWithBandedAd( + // adJac.adRes + idxr.offsetCp(ParticleTypeIndex{ type }), pblk * idxr.strideParBlock(type), adJac.adDirOffset, _jacP[type].lowerBandwidth(), + // _jacP[type].lowerBandwidth(), _jacP[type].upperBandwidth(), fullJacobianMatrix + // ); + // LOG(Debug) << "MaxDiff: " << diff; + //#endif + // + // // Extract Jacobian from AD + // ad::extractDenseJacobianFromBandedAd( + // adJac.adRes + idxr.offsetCp(ParticleTypeIndex{ type }), pblk * idxr.strideParBlock(type), adJac.adDirOffset, _jacP[type].lowerBandwidth(), + // _jacP[type].lowerBandwidth(), _jacP[type].upperBandwidth(), fullJacobianMatrix + // ); + // + // // Extract Jacobian from full Jacobian + // mat.setAll(0.0); + // linalg::copyMatrixSubset(fullJacobianMatrix, mask, mask, mat); + // + // // Replace upper part with conservation relations + // mat.submatrixSetAll(0.0, 0, 0, numActiveComp, probSize); + // + // unsigned int bndIdx = 0; + // unsigned int rIdx = 0; + // unsigned int bIdx = 0; + // for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + // { + // if (!mask.mask[comp]) + // { + // bndIdx += _disc.nBound[_disc.nComp * type + comp]; + // continue; + // } + // + // mat.native(rIdx, rIdx) = static_cast(_parPorosity[type]); + // + // for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) + // { + // if (mask.mask[bndIdx]) + // { + // mat.native(rIdx, bIdx + numActiveComp) = epsQ; + // ++bIdx; + // } + // } + // + // ++rIdx; + // } + // + // return true; + // }; + // } + // else + //{ + jacFunc = [&](double const* const x, linalg::detail::DenseMatrixBase& mat) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract Jacobian from full Jacobian + mat.setAll(0.0); + linalg::copyMatrixSubset(fullJacobianMatrix, mask, mask, mat); + + // Replace upper part with conservation relations + mat.submatrixSetAll(0.0, 0, 0, numActiveComp, probSize); + + unsigned int bndIdx = 0; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[_disc.nComp * type + comp]; + continue; + } + + mat.native(rIdx, rIdx) = static_cast(_parPorosity[type]); + + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + mat.native(rIdx, bIdx + numActiveComp) = epsQ; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }; + //} + + // Apply nonlinear solver + _nonlinearSolver->solve( + [&](double const* const x, double* const r) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract values from residual + linalg::selectVectorSubset(fullResidual, mask, r); + + // Calculate residual of conserved moieties + std::fill_n(r, numActiveComp, 0.0); + unsigned int bndIdx = _disc.nComp; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[_disc.nComp * type + comp]; + continue; + } + + r[rIdx] = static_cast(_parPorosity[type]) * x[rIdx] - conservedQuants[rIdx]; + + for (unsigned int bnd = 0; bnd < _disc.nBound[_disc.nComp * type + comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + r[rIdx] += epsQ * x[bIdx + numActiveComp]; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }, + jacFunc, errorTol, solution, nonlinMem, jacobianMatrix, probSize); + + // Apply solution + linalg::applyVectorSubset(solution, mask, qShell - idxr.strideParLiquid()); + + // Refine / correct solution + _binding[type]->postConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideParLiquid(), tlmAlloc); + + } CADET_PARFOR_END; + } + + // reset jacobian pattern //@todo can this be avoided? + setGlobalJacPattern(_globalJacDisc, _dynReactionBulk); + } + + /** + * @brief Computes consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function performs step 2. See consistentInitialState() for step 1. + * + * This function is to be used with consistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] vecStateY Consistently initialized state vector + * @param [in,out] vecStateYdot On entry, residual without taking time derivatives into account. On exit, consistent state time derivatives. + */ + void LumpedRateModelWithPoresDG::consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Eigen::Map y(vecStateY, numDofs()); + Eigen::Map yDot(vecStateYdot, numDofs()); + + Indexer idxr(_disc); + + // Step 2: Compute the correct time derivative of the state vector + + // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system + + // Note that the residual has not been negated, yet. We will do that now. + for (unsigned int i = 0; i < numDofs(); ++i) + vecStateYdot[i] = -vecStateYdot[i]; + + // Handle bulk column block + //_convDispOp.solveTimeDerivativeSystem(simTime, vecStateYdot + idxr.offsetC()); + + // Assemble + double* vPtr = _globalJacDisc.valuePtr(); + for (int k = 0; k < _globalJacDisc.nonZeros(); k++) { + vPtr[k] = 0.0; + } + addTimeDerBulkJacobian(1.0, idxr); + + // Process the particle blocks +#ifdef CADET_PARALLELIZE + BENCH_START(_timerConsistentInitPar); + tbb::parallel_for(std::size_t(0), static_cast(_disc.nParType), [&](std::size_t type) +#else + for (unsigned int type = 0; type < _disc.nParType; ++type) +#endif + { + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + double* const dFluxDt = _tempState + idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }); + + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) + { + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(pblk / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[pblk % _disc.nNodes])) / _disc.length_; + + // Assemble + linalg::BandedEigenSparseRowIterator jacPar(_globalJacDisc, idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }, ParticleIndex{ pblk }) - idxr.offsetC()); + + // Mobile and stationary phase (advances jac accordingly) + addTimeDerivativeToJacobianParticleBlock(jacPar, idxr, 1.0, type); + + if (!_binding[type]->hasQuasiStationaryReactions()) + continue; + + // Get iterators to beginning of solid phase + linalg::BandedEigenSparseRowIterator jacSolidOrig(_globalJac, idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }, ParticleIndex{ pblk }) - idxr.offsetC() + idxr.strideParLiquid()); + linalg::BandedEigenSparseRowIterator jacSolid = jacPar - idxr.strideParBound(type); + + int const* const mask = _binding[type]->reactionQuasiStationarity(); + double* const qShellDot = vecStateYdot + idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }, ParticleIndex{ pblk }) + idxr.strideParLiquid(); + + // Obtain derivative of fluxes wrt. time + std::fill_n(dFluxDt, _disc.strideBound[type], 0.0); + if (_binding[type]->dependsOnTime()) + { + _binding[type]->timeDerivativeQuasiStationaryFluxes(simTime.t, simTime.secIdx, + ColumnPosition{ z, 0.0, static_cast(_parRadius[type]) * 0.5 }, + qShellDot - _disc.nComp, qShellDot, dFluxDt, tlmAlloc); + } + + // Copy row from original Jacobian and set right hand side + for (int i = 0; i < idxr.strideParBound(type); ++i, ++jacSolid, ++jacSolidOrig) + { + if (!mask[i]) + continue; + + jacSolid.copyRowFrom(jacSolidOrig); + qShellDot[i] = -dFluxDt[i]; + } + } + + } CADET_PARFOR_END; + +#ifdef CADET_PARALLELIZE + BENCH_STOP(_timerConsistentInitPar); +#endif + + // Factorize + _globalSolver.factorize(_globalJacDisc); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Factorize() failed"; + } + // Solve + yDot.segment(idxr.offsetC(), numPureDofs()) = _globalSolver.solve(yDot.segment(idxr.offsetC(), numPureDofs())); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Solve() failed"; + } + } + + + + /** + * @brief Computes approximately / partially consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialState(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 in the column + * bulk and flux blocks. The resulting equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for the bulk block and 0 for the flux block. + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function performs step 1. See leanConsistentInitialTimeDerivative() for step 2. + * + * This function is to be used with leanConsistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + */ + void LumpedRateModelWithPoresDG::leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + // Tdod ?? + } + + /** + * @brief Computes approximately / partially consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialTimeDerivative(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 in the column + * bulk and flux blocks. The resulting equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for the bulk block and 0 for the flux block. + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function performs step 2. See leanConsistentInitialState() for step 1. + * + * This function is to be used with leanConsistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] t Current time point + * @param [in] vecStateY (Lean) consistently initialized state vector + * @param [in,out] vecStateYdot On entry, inconsistent state time derivatives. On exit, partially consistent state time derivatives. + * @param [in] res On entry, residual without taking time derivatives into account. The data is overwritten during execution of the function. + */ + void LumpedRateModelWithPoresDG::leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem) + { + // @TODO? + + //BENCH_SCOPE(_timerConsistentInit); + + //Indexer idxr(_disc); + + //// Step 2: Compute the correct time derivative of the state vector + + //// Step 2a: Assemble, factorize, and solve column bulk block of linear system + + //// Note that the residual is not negated as required at this point. We will fix that later. + + //double* const resSlice = res + idxr.offsetC(); + + //// Handle bulk block + //_convDispOp.solveTimeDerivativeSystem(SimulationTime{ t, 0u }, resSlice); + + //// Note that we have solved with the *positive* residual as right hand side + //// instead of the *negative* one. Fortunately, we are dealing with linear systems, + //// which means that we can just negate the solution. + //double* const yDotSlice = vecStateYdot + idxr.offsetC(); + //for (unsigned int i = 0; i < _disc.nCol * _disc.nComp; ++i) + // yDotSlice[i] = -resSlice[i]; + + //// Step 2b: Solve for fluxes j_f by backward substitution + + //// Reset \dot{j}_f to 0.0 + //double* const jfDot = vecStateYdot + idxr.offsetJf(); + //std::fill(jfDot, jfDot + _disc.nComp * _disc.nCol * _disc.nParType, 0.0); + + //solveForFluxes(vecStateYdot, idxr); + } + + void LumpedRateModelWithPoresDG::initializeSensitivityStates(const std::vector& vecSensY) const + { + Indexer idxr(_disc); + for (std::size_t param = 0; param < vecSensY.size(); ++param) + { + double* const stateYbulk = vecSensY[param] + idxr.offsetC(); + + // Loop over column cells + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + // Loop over components in cell + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[point * idxr.strideColCell() + comp * idxr.strideColComp()] = _initC[comp].getADValue(param); + } + + // Loop over particles + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + const unsigned int offset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ point }); + double* const stateYparticle = vecSensY[param] + offset; + double* const stateYparticleSolid = stateYparticle + idxr.strideParLiquid(); + + // Initialize c_p + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + stateYparticle[comp] = _initCp[comp + _disc.nComp * type].getADValue(param); + + // Initialize q + for (unsigned int bnd = 0; bnd < _disc.strideBound[type]; ++bnd) + stateYparticleSolid[bnd] = _initQ[bnd + _disc.nBoundBeforeType[type]].getADValue(param); + } + } + } + } + + /** + * @brief Computes consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of consistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Once all @f$ c_i @f$, @f$ c_{p,i} @f$, and @f$ q_i^{(j)} @f$ have been computed, solve for the + * fluxes @f$ j_{f,i} @f$. Let @f$ \mathcal{I}_a @f$ be the index set of algebraic equations, then, at this point, we have + * \f[ \left( \frac{\partial F}{\partial y}(t, y_0, \dot{y}_0) s + \frac{\partial F}{\partial p}(t, y_0, \dot{y}_0) \right)_{\mathcal{I}_a} = 0. \f]
  2. + *
  3. Compute the time derivatives of the sensitivity @f$ \dot{s} @f$ such that the differential equations hold. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{s}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the sensitivity vector @f$ s @f$ is fixed). The resulting system + * has a similar structure as the system Jacobian. + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * & \dot{J}_1 & & & \\ + * & & \ddots & & \\ + * & & & \dot{J}_{N_z} & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_i @f$ denotes the Jacobian with respect to @f$ \dot{y}@f$. Note that the + * @f$ J_{i,f} @f$ matrices in the right column are missing. + * + * Let @f$ \mathcal{I}_d @f$ denote the index set of differential equations. + * The right hand side of the linear system is given by @f[ -\frac{\partial F}{\partial y}(t, y, \dot{y}) s - \frac{\partial F}{\partial p}(t, y, \dot{y}), @f] + * which is 0 for algebraic equations (@f$ -\frac{\partial^2 F}{\partial t \partial p}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the diagonal blocks are solved in parallel. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * diagonal blocks.
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithPoresDG::consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) + { + // @TODO? + // BENCH_SCOPE(_timerConsistentInit); + // + // Indexer idxr(_disc); + // + // for (std::size_t param = 0; param < vecSensY.size(); ++param) + // { + // double* const sensY = vecSensY[param]; + // double* const sensYdot = vecSensYdot[param]; + // + // // Copy parameter derivative dF / dp from AD and negate it + // for (unsigned int i = _disc.nComp; i < numDofs(); ++i) + // sensYdot[i] = -adRes[i].getADValue(param); + // + // // Step 1: Solve algebraic equations + // + // // Step 1a: Compute quasi-stationary binding model state + // for (unsigned int type = 0; type < _disc.nParType; ++type) + // { + // if (!_binding[type]->hasQuasiStationaryReactions()) + // continue; + // + // int const* const qsMask = _binding[type]->reactionQuasiStationarity(); + // const linalg::ConstMaskArray mask{ qsMask, static_cast(_disc.strideBound[type]) }; + // const int probSize = linalg::numMaskActive(mask); + // + //#ifdef CADET_PARALLELIZE + // BENCH_SCOPE(_timerConsistentInitPar); + // tbb::parallel_for(std::size_t(0), static_cast(_disc.nCol), [&](std::size_t pblk) + //#else + // for (unsigned int pblk = 0; pblk < _disc.nCol; ++pblk) + //#endif + // { + // // Reuse memory of band matrix for dense matrix + // linalg::DenseMatrixView jacobianMatrix(_jacPdisc[type].data() + pblk * probSize * probSize, _jacPdisc[type].pivot() + pblk * probSize, probSize, probSize); + // + // // Get workspace memory + // LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + // + // BufferedArray rhsBuffer = tlmAlloc.array(probSize); + // double* const rhs = static_cast(rhsBuffer); + // + // BufferedArray rhsUnmaskedBuffer = tlmAlloc.array(idxr.strideParBound(type)); + // double* const rhsUnmasked = static_cast(rhsUnmaskedBuffer); + // + // double* const maskedMultiplier = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); + // double* const scaleFactors = _tempState + idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }); + // + // const int jacRowOffset = idxr.strideParBlock(type) * pblk + _disc.nComp; + // const int localQOffset = idxr.offsetCp(ParticleTypeIndex{ type }, ParticleIndex{ static_cast(pblk) }) + idxr.strideParLiquid(); + // + // // Extract subproblem Jacobian from full Jacobian + // jacobianMatrix.setAll(0.0); + // linalg::copyMatrixSubset(_jacP[type], mask, mask, jacRowOffset, 0, jacobianMatrix); + // + // // Construct right hand side + // linalg::selectVectorSubset(sensYdot + localQOffset, mask, rhs); + // + // // Zero out masked elements + // std::copy_n(sensY + localQOffset - idxr.strideParLiquid(), _disc.nComp + _disc.strideBound[type], maskedMultiplier); + // linalg::fillVectorSubset(maskedMultiplier + _disc.nComp, mask, 0.0); + // + // // Assemble right hand side + // _jacP[type].submatrixMultiplyVector(maskedMultiplier, jacRowOffset, -static_cast(_disc.nComp), _disc.strideBound[type], _disc.nComp + _disc.strideBound[type], rhsUnmasked); + // linalg::vectorSubsetAdd(rhsUnmasked, mask, -1.0, 1.0, rhs); + // + // // Precondition + // jacobianMatrix.rowScaleFactors(scaleFactors); + // jacobianMatrix.scaleRows(scaleFactors); + // + // // Solve + // jacobianMatrix.factorize(); + // jacobianMatrix.solve(scaleFactors, rhs); + // + // // Write back + // linalg::applyVectorSubset(rhs, mask, sensY + localQOffset); + // } CADET_PARFOR_END; + // } + // + // // Step 1b: Compute fluxes j_f, right hand side is -dF / dp + // std::copy(sensYdot + idxr.offsetJf(), sensYdot + numDofs(), sensY + idxr.offsetJf()); + // + // solveForFluxes(sensY, idxr); + // + // // Step 2: Compute the correct time derivative of the state vector + // + // // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system + // + // // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in sensYdot + // multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, sensYdot); + // + // // Note that we have correctly negated the right hand side + // + // // Handle bulk block + // _convDispOp.solveTimeDerivativeSystem(simTime, sensYdot + idxr.offsetC()); + // + // // Process the particle blocks + //#ifdef CADET_PARALLELIZE + // BENCH_START(_timerConsistentInitPar); + // tbb::parallel_for(std::size_t(0), static_cast(_disc.nParType), [&](std::size_t type) + //#else + // for (unsigned int type = 0; type < _disc.nParType; ++type) + //#endif + // { + // _jacPdisc[type].setAll(0.0); + // for (unsigned int pblk = 0; pblk < _disc.nCol; ++pblk) + // { + // // Assemble + // linalg::FactorizableBandMatrix::RowIterator jac = _jacPdisc[type].row(idxr.strideParBlock(type) * pblk); + // + // // Mobile and solid phase + // addTimeDerivativeToJacobianParticleBlock(jac, idxr, 1.0, type); + // // Iterator jac has already been advanced to next shell + // + // // Overwrite rows corresponding to algebraic equations with the Jacobian and set right hand side to 0 + // if (_binding[type]->hasQuasiStationaryReactions()) + // { + // // Get iterators to beginning of solid phase + // linalg::BandMatrix::RowIterator jacSolidOrig = _jacP[type].row(idxr.strideParBlock(type) * pblk + static_cast(idxr.strideParLiquid())); + // linalg::FactorizableBandMatrix::RowIterator jacSolid = jac - idxr.strideParBound(type); + // + // int const* const mask = _binding[type]->reactionQuasiStationarity(); + // double* const qShellDot = sensYdot + idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }, ParticleIndex{ pblk }) + idxr.strideParLiquid(); + // + // // Copy row from original Jacobian and set right hand side + // for (int i = 0; i < idxr.strideParBound(type); ++i, ++jacSolid, ++jacSolidOrig) + // { + // if (!mask[i]) + // continue; + // + // jacSolid.copyRowFrom(jacSolidOrig); + // + // // Right hand side is -\frac{\partial^2 res(t, y, \dot{y})}{\partial p \partial t} + // // If the residual is not explicitly depending on time, this expression is 0 + // // @todo This is wrong if external functions are used. Take that into account! + // qShellDot[i] = 0.0; + // } + // } + // } + // + // // Precondition + // double* const scaleFactors = _tempState + idxr.offsetCp(ParticleTypeIndex{ static_cast(type) }); + // _jacPdisc[type].rowScaleFactors(scaleFactors); + // _jacPdisc[type].scaleRows(scaleFactors); + // + // // Factorize + // const bool result = _jacPdisc[type].factorize(); + // if (!result) + // { + // LOG(Error) << "Factorize() failed for par type block " << type; + // } + // + // // Solve + // const bool result2 = _jacPdisc[type].solve(scaleFactors, sensYdot + idxr.offsetCp(ParticleTypeIndex{ static_cast(type) })); + // if (!result2) + // { + // LOG(Error) << "Solve() failed for par type block " << type; + // } + // } CADET_PARFOR_END; + // + //#ifdef CADET_PARALLELIZE + // BENCH_STOP(_timerConsistentInitPar); + //#endif + // + // // TODO: Right hand side for fluxes should be -d^2res/(dp dy) * \dot{y} + // // If parameters depend on time, then it should be + // // -d^2res/(dp dy) * \dot{y} - d^2res/(dt dy) * s - d^2res/(dp dt) + // + // // Step 2b: Solve for fluxes j_f by backward substitution + // solveForFluxes(sensYdot, idxr); + // } + } + + /** + * @brief Computes approximately / partially consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of leanConsistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations). + * Only solve for the fluxes @f$ j_{f,i} @f$ (only linear equations).
  2. + *
  3. Compute the time derivatives of the sensitivity @f$ \dot{s} @f$ such that the differential equations hold. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{s}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the sensitivity vector @f$ s @f$ is fixed). The resulting + * equations are stated below: + * @f[ \begin{align} + * \left[\begin{array}{c|ccc|c} + * \dot{J}_0 & & & & \\ + * \hline + * J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + * \end{array}\right], + * \end{align} @f] + * where @f$ \dot{J}_0 @f$ denotes the bulk block Jacobian with respect to @f$ \dot{y}@f$. + * + * Let @f$ \mathcal{I}_d @f$ denote the index set of differential equations. + * The right hand side of the linear system is given by @f[ -\frac{\partial F}{\partial y}(t, y, \dot{y}) s - \frac{\partial F}{\partial p}(t, y, \dot{y}), @f] + * which is 0 for algebraic equations (@f$ -\frac{\partial^2 F}{\partial t \partial p}@f$, to be more precise). + * + * The linear system is solved by backsubstitution. First, the bulk block is solved. + * Then, the equations for the fluxes @f$ j_f @f$ are solved by substituting in the solution of the + * bulk block and the unchanged particle block time derivative vectors.
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithPoresDG::leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) + { + //BENCH_SCOPE(_timerConsistentInit); + + //Indexer idxr(_disc); + + //for (std::size_t param = 0; param < vecSensY.size(); ++param) + //{ + // double* const sensY = vecSensY[param]; + // double* const sensYdot = vecSensYdot[param]; + + // // Copy parameter derivative from AD to tempState and negate it + // // We need to use _tempState in order to keep sensYdot unchanged at this point + // for (int i = 0; i < idxr.offsetCp(); ++i) + // _tempState[i] = -adRes[i].getADValue(param); + + // std::fill(_tempState + idxr.offsetCp(), _tempState + idxr.offsetJf(), 0.0); + + // for (unsigned int i = idxr.offsetJf(); i < numDofs(); ++i) + // _tempState[i] = -adRes[i].getADValue(param); + + // // Step 1: Compute fluxes j_f, right hand side is -dF / dp + // std::copy(_tempState + idxr.offsetJf(), _tempState + numDofs(), sensY + idxr.offsetJf()); + + // solveForFluxes(sensY, idxr); + + // // Step 2: Compute the correct time derivative of the state vector + + // // Step 2a: Assemble, factorize, and solve diagonal blocks of linear system + + // // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in _tempState + // multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, _tempState); + + // // Copy relevant parts to sensYdot for use as right hand sides + // std::copy(_tempState + idxr.offsetC(), _tempState + idxr.offsetCp(), sensYdot + idxr.offsetC()); + // std::copy(_tempState + idxr.offsetJf(), _tempState + numDofs(), sensYdot); + + // // Handle bulk block + // _convDispOp.solveTimeDerivativeSystem(simTime, sensYdot + idxr.offsetC()); + + // // Step 2b: Solve for fluxes j_f by backward substitution + // solveForFluxes(sensYdot, idxr); + //} + } + + } // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/LumpedRateModelWithPoresDG-LinearSolver.cpp b/src/libcadet/model/LumpedRateModelWithPoresDG-LinearSolver.cpp new file mode 100644 index 000000000..f68c237b1 --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithPoresDG-LinearSolver.cpp @@ -0,0 +1,249 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2022: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + // @TODO: delete inlcude iostream and iomanip +#include +#include +#include + +#include "model/LumpedRateModelWithPoresDG.hpp" +#include "model/BindingModel.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "AdUtils.hpp" + +#include +#include + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include "ParallelSupport.hpp" + +#ifdef CADET_PARALLELIZE + #include + #include + + typedef tbb::flow::continue_node< tbb::flow::continue_msg > node_t; + typedef const tbb::flow::continue_msg & msg_t; +#endif + +namespace cadet +{ + +namespace model +{ + +/** + * @brief Computes the solution of the linear system involving the system Jacobian + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The right hand side \f$ b \f$ is given by @p rhs, the Jacobians are evaluated at the + * point \f$(y, \dot{y})\f$ given by @p y and @p yDot. The residual @p res at this point, \f$ F(t, y, \dot{y}) \f$, + * may help with this. Error weights (see IDAS guide) are given in @p weight. The solution is returned in @p rhs. + * + * The full Jacobian @f$ J = \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) @f$ is given by + * @f[ \begin{align} + J = + \left[\begin{array}{c|ccc|c} + J_0 & & & & J_{0,f} \\ + \hline + & J_1 & & & J_{1,f} \\ + & & \ddots & & \vdots \\ + & & & J_{N_z} & J_{N_z,f} \\ + \hline + J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & J_f + \end{array}\right]. + \end{align} @f] + * By decomposing the Jacobian @f$ J @f$ into @f$ J = LU @f$, we get + * @f[ \begin{align} + L &= \left[\begin{array}{c|ccc|c} + J_0 & & & & \\ + \hline + & J_1 & & & \\ + & & \ddots & & \\ + & & & J_{N_z} & \\ + \hline + J_{f,0} & J_{f,1} & \dots & J_{f,N_z} & I + \end{array}\right], \\ + U &= \left[\begin{array}{c|ccc|c} + I & & & & J_0^{-1} \, J_{0,f} \\ + \hline + & I & & & J_1^{-1} \, J_{1,f} \\ + & & \ddots & & \vdots \\ + & & & I & J_{N_z}^{-1} \, J_{N_z,f} \\ + \hline + & & & & S + \end{array}\right]. + \end{align} @f] + * Note that @f$ J_f = I @f$ is the identity matrix and that the off-diagonal blocks @f$ J_{i,f} @f$ + * and @f$ J_{f,i} @f$ for @f$ i = 0, \dots, N_{z} @f$ are sparse. + * + * The solution procedure works as follows: + * -# Factorize the global jacobian + * -# Solve the system via a direct LU solver (Eigen3 lib) + * -# Inlet DOFs are treated separately since they are only coupled with first DG element + * + * + * @param [in] t Current time point + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] outerTol Error tolerance for the solution of the linear system from outer Newton iteration + * @param [in,out] rhs On entry the right hand side of the linear equation system, on exit the solution + * @param [in] weight Vector with error weights + * @param [in] simState State of the simulation (state vector and its time derivatives) at which the Jacobian is evaluated + * @return @c 0 on success, @c -1 on non-recoverable error, and @c +1 on recoverable error + */ +int LumpedRateModelWithPoresDG::linearSolve(double t, double alpha, double outerTol, double* const rhs, double const* const weight, + const ConstSimulationState& simState) +{ + BENCH_SCOPE(_timerLinearSolve); + + Indexer idxr(_disc); + + Eigen::Map r(rhs, numDofs()); // map rhs to Eigen object + + // ==== Step 1: Factorize global Jacobian (without inlet DOFs) + + // Factorize partial Jacobians only if required + if (_factorizeJacobian) + { + + // Assemble and factorize discretized bulk Jacobian + assembleDiscretizedGlobalJacobian(alpha, idxr); + + _globalSolver.factorize(_globalJacDisc); + //_bulkSolver.compute(_jacCdisc); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) { + LOG(Error) << "Factorize() failed"; + } + + // Do not factorize again at next call without changed Jacobians + _factorizeJacobian = false; + } + + // ====== Step 1.5: Solve J c_uo = b_uo - A * c_in = b_uo - A*b_in + + // rhs is passed twice but due to the values in jacA the writes happen to a different area of the rhs than the reads. + + // Handle inlet DOFs: + // Inlet at z = 0 for forward flow, at z = L for backward flow. + unsigned int offInlet = (_disc.velocity >= 0.0) ? 0 : (_disc.nCol - 1u) * idxr.strideColCell(); + + for (int comp = 0; comp < _disc.nComp; comp++) { + for (int node = 0; node < (_disc.exactInt ? _disc.nNodes : 1); node++) { + r[idxr.offsetC() + offInlet + comp * idxr.strideColComp() + node * idxr.strideColNode()] += _jacInlet(node, 0) * r[comp]; + } + } + + // ==== Step 2: Solve system of pure DOFs + // The result is stored in rhs (in-place solution) + + r.segment(idxr.offsetC(), numPureDofs()) = _globalSolver.solve(r.segment(idxr.offsetC(), numPureDofs())); + + if (cadet_unlikely(_globalSolver.info() != Eigen::Success)) + { + LOG(Error) << "Solve() failed"; + } + + // The full solution is now stored in rhs + return 0; +} + +/** + * @brief Assembles bulk Jacobian @f$ J_i @f$ (@f$ i > 0 @f$) of the time-discretized equations + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The system Jacobian of the original equations, + * \f[ \frac{\partial F}{\partial y}, \f] + * is already computed (by AD or manually in residualImpl() with @c wantJac = true). This function is responsible + * for adding + * \f[ \alpha \frac{\partial F}{\partial \dot{y}} \f] + * to the system Jacobian, which yields the Jacobian of the time-discretized equations + * \f[ F\left(t, y_0, \sum_{k=0}^N \alpha_k y_k \right) = 0 \f] + * when a BDF method is used. The time integrator needs to solve this equation for @f$ y_0 @f$, which requires + * the solution of the linear system mentioned above (@f$ \alpha_0 = \alpha @f$ given in @p alpha). + * + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + */ +void LumpedRateModelWithPoresDG::assembleDiscretizedGlobalJacobian(double alpha, Indexer idxr) { + + double* vPtr = _globalJacDisc.valuePtr(); + for (int k = 0; k < _globalJacDisc.nonZeros(); k++) { + *vPtr = 0.0; + vPtr++; + } + + // add time derivative to bulk jacobian + addTimeDerBulkJacobian(alpha, idxr); + + // Add time derivatives to particle shells + for (unsigned int parType = 0; parType < _disc.nParType; parType++) { + linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, idxr.offsetCp(ParticleTypeIndex{ parType }) - idxr.offsetC()); + for (unsigned int j = 0; j < _disc.nPoints; ++j) + { + // Mobile and solid phase (advances jac accordingly) + addTimeDerivativeToJacobianParticleBlock(jac, idxr, alpha, parType); + } + } + + // add static (per section) jacobian + _globalJacDisc += _globalJac; + +} + +/** + * @brief Adds Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ to bead rows of system Jacobian + * @details Actually adds @f$ \alpha \frac{\partial F}{\partial \dot{y}} @f$, which is useful + * for constructing the linear system in BDF time discretization. + * @param [in,out] jac On entry, RowIterator of the particle block pointing to the beginning of a bead shell; + * on exit, the iterator points to the end of the bead shell + * @param [in] idxr Indexer + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] parType Index of the particle type + */ +void LumpedRateModelWithPoresDG::addTimeDerivativeToJacobianParticleBlock(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, unsigned int parType) +{ + // Mobile phase + for (int comp = 0; comp < static_cast(_disc.nComp); ++comp, ++jac) + { + // Add derivative with respect to dc_p / dt to Jacobian + jac[0] += alpha; + + const double invBetaP = (1.0 - static_cast(_parPorosity[parType])) / (static_cast(_poreAccessFactor[parType * _disc.nComp + comp]) * static_cast(_parPorosity[parType])); + + // Add derivative with respect to dq / dt to Jacobian + const int nBound = static_cast(_disc.nBound[parType * _disc.nComp + comp]); + for (int i = 0; i < nBound; ++i) + { + // Index explanation: + // -comp -> go back to beginning of liquid phase + // + strideParLiquid() skip to solid phase + // + offsetBoundComp() jump to component (skips all bound states of previous components) + // + i go to current bound state + jac[idxr.strideParLiquid() - comp + idxr.offsetBoundComp(ParticleTypeIndex{ parType }, ComponentIndex{ static_cast(comp) }) + i] += alpha * invBetaP; + } + } + + // Solid phase + int const* const qsReaction = _binding[parType]->reactionQuasiStationarity(); + for (unsigned int bnd = 0; bnd < _disc.strideBound[parType]; ++bnd, ++jac) + { + // Add derivative with respect to dynamic states to Jacobian + if (qsReaction[bnd]) + continue; + + // Add derivative with respect to dq / dt to Jacobian + jac[0] += alpha; + } +} + +} // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/LumpedRateModelWithPoresDG.cpp b/src/libcadet/model/LumpedRateModelWithPoresDG.cpp new file mode 100644 index 000000000..c8127555a --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithPoresDG.cpp @@ -0,0 +1,1541 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/LumpedRateModelWithPoresDG.hpp" +#include "BindingModelFactory.hpp" +#include "ParamReaderHelper.hpp" +#include "ParamReaderScopes.hpp" +#include "cadet/Exceptions.hpp" +#include "cadet/ExternalFunction.hpp" +#include "cadet/SolutionRecorder.hpp" +#include "ConfigurationHelper.hpp" +#include "model/BindingModel.hpp" +#include "model/ReactionModel.hpp" +#include "model/parts/BindingCellKernel.hpp" +#include "SimulationTypes.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/Norms.hpp" + +#include "Stencil.hpp" +#include "Weno.hpp" +#include "AdUtils.hpp" +#include "SensParamUtil.hpp" + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include +#include + +#include "ParallelSupport.hpp" +#ifdef CADET_PARALLELIZE +#include +#endif + +namespace cadet +{ + +namespace model +{ + +constexpr double SurfVolRatioSphere = 3.0; +constexpr double SurfVolRatioCylinder = 2.0; +constexpr double SurfVolRatioSlab = 1.0; + + +LumpedRateModelWithPoresDG::LumpedRateModelWithPoresDG(UnitOpIdx unitOpIdx) : UnitOperationBase(unitOpIdx), +_dynReactionBulk(nullptr), _globalJac(), _jacInlet(), _analyticJac(true), +_jacobianAdDirs(0), _factorizeJacobian(false), _tempState(nullptr), _initC(0), _initCp(0), _initQ(0), +_initState(0), _initStateDot(0) +{ +} + +LumpedRateModelWithPoresDG::~LumpedRateModelWithPoresDG() CADET_NOEXCEPT +{ + delete[] _tempState; + + delete _dynReactionBulk; + + delete[] _disc.parTypeOffset; + delete[] _disc.nBound; + delete[] _disc.boundOffset; + delete[] _disc.strideBound; + delete[] _disc.nBoundBeforeType; +} + +unsigned int LumpedRateModelWithPoresDG::numDofs() const CADET_NOEXCEPT +{ + // Column bulk DOFs: nPoints * nComp + // Particle DOFs: nPoints * nParType particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs + // Inlet DOFs: nComp + return _disc.nComp + _disc.nComp * _disc.nPoints + _disc.parTypeOffset[_disc.nParType]; +} + +unsigned int LumpedRateModelWithPoresDG::numPureDofs() const CADET_NOEXCEPT +{ + // Column bulk DOFs: nPoints * nComp + // Particle DOFs: nPoints * nParType particles each having nComp (liquid phase) + sum boundStates (solid phase) DOFs + return _disc.nComp * _disc.nPoints + _disc.parTypeOffset[_disc.nParType]; +} + + +bool LumpedRateModelWithPoresDG::usesAD() const CADET_NOEXCEPT +{ +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + // We always need AD if we want to check the analytical Jacobian + return true; +#else + // We only need AD if we are not computing the Jacobian analytically + return !_analyticJac; +#endif +} + +bool LumpedRateModelWithPoresDG::configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper) +{ + // ==== Read discretization + _disc.nComp = paramProvider.getInt("NCOMP"); + + paramProvider.pushScope("discretization"); + + _disc.nCol = paramProvider.getInt("NCOL"); + if (_disc.nCol < 1) + throw InvalidParameterException("Number of column cells must be at least 1!"); + + if (paramProvider.getInt("POLYDEG") < 1) + throw InvalidParameterException("Polynomial degree must be at least 1!"); + else + _disc.polyDeg = paramProvider.getInt("POLYDEG"); + + _disc.exactInt = paramProvider.getBool("EXACT_INTEGRATION"); + + // Compute discretization + _disc.initializeDG(); + + const std::vector nBound = paramProvider.getIntArray("NBOUND"); + if (nBound.size() < _disc.nComp) + throw InvalidParameterException("Field NBOUND contains too few elements (NCOMP = " + std::to_string(_disc.nComp) + " required)"); + + if (nBound.size() % _disc.nComp != 0) + throw InvalidParameterException("Field NBOUND must have a size divisible by NCOMP (" + std::to_string(_disc.nComp) + ")"); + + if (paramProvider.exists("NPARTYPE")) + { + _disc.nParType = paramProvider.getInt("NPARTYPE"); + _disc.nBound = new unsigned int[_disc.nComp * _disc.nParType]; + if (nBound.size() < _disc.nComp * _disc.nParType) + { + // Multiplex number of bound states to all particle types + for (unsigned int i = 0; i < _disc.nParType; ++i) + std::copy_n(nBound.begin(), _disc.nComp, _disc.nBound + i * _disc.nComp); + } + else + std::copy_n(nBound.begin(), _disc.nComp * _disc.nParType, _disc.nBound); + } + else + { + // Infer number of particle types + _disc.nParType = nBound.size() / _disc.nComp; + _disc.nBound = new unsigned int[_disc.nComp * _disc.nParType]; + std::copy_n(nBound.begin(), _disc.nComp * _disc.nParType, _disc.nBound); + } + + // Precompute offsets and total number of bound states (DOFs in solid phase) + const unsigned int nTotalBound = std::accumulate(_disc.nBound, _disc.nBound + _disc.nComp * _disc.nParType, 0u); + + // Precompute offsets and total number of bound states (DOFs in solid phase) + _disc.boundOffset = new unsigned int[_disc.nComp * _disc.nParType]; + _disc.strideBound = new unsigned int[_disc.nParType + 1]; + _disc.nBoundBeforeType = new unsigned int[_disc.nParType]; + _disc.strideBound[_disc.nParType] = nTotalBound; + _disc.nBoundBeforeType[0] = 0; + for (unsigned int j = 0; j < _disc.nParType; ++j) + { + unsigned int* const ptrOffset = _disc.boundOffset + j * _disc.nComp; + unsigned int* const ptrBound = _disc.nBound + j * _disc.nComp; + + ptrOffset[0] = 0; + for (unsigned int i = 1; i < _disc.nComp; ++i) + { + ptrOffset[i] = ptrOffset[i - 1] + ptrBound[i - 1]; + } + _disc.strideBound[j] = ptrOffset[_disc.nComp - 1] + ptrBound[_disc.nComp - 1]; + + if (j != _disc.nParType - 1) + _disc.nBoundBeforeType[j + 1] = _disc.nBoundBeforeType[j] + _disc.strideBound[j]; + } + + // Precompute offsets of particle type DOFs + _disc.parTypeOffset = new unsigned int[_disc.nParType + 1]; + _disc.parTypeOffset[0] = 0; + for (unsigned int j = 1; j < _disc.nParType + 1; ++j) + { + _disc.parTypeOffset[j] = _disc.parTypeOffset[j - 1] + (_disc.nComp + _disc.strideBound[j - 1]) * _disc.nPoints; + } + + // Determine whether analytic Jacobian should be used but don't set it right now. + // We need to setup Jacobian matrices first. +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + const bool analyticJac = paramProvider.getBool("USE_ANALYTIC_JACOBIAN"); +#else + const bool analyticJac = false; +#endif + + // Allocate space for initial conditions + _initC.resize(_disc.nComp); + _initCp.resize(_disc.nComp * _disc.nParType); + _initQ.resize(nTotalBound); + + // Create nonlinear solver for consistent initialization + configureNonlinearSolver(paramProvider); + + // Read particle geometry and default to "SPHERICAL" + _parGeomSurfToVol = std::vector(_disc.nParType, SurfVolRatioSphere); + if (paramProvider.exists("PAR_GEOM")) + { + std::vector pg = paramProvider.getStringArray("PAR_GEOM"); + if ((pg.size() == 1) && (_disc.nParType > 1)) + { + // Multiplex using first value + pg.resize(_disc.nParType, pg[0]); + } + else if (pg.size() < _disc.nParType) + throw InvalidParameterException("Field PAR_GEOM contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (pg[i] == "SPHERE") + _parGeomSurfToVol[i] = SurfVolRatioSphere; + else if (pg[i] == "CYLINDER") + _parGeomSurfToVol[i] = SurfVolRatioCylinder; + else if (pg[i] == "SLAB") + _parGeomSurfToVol[i] = SurfVolRatioSlab; + else + throw InvalidParameterException("Unknown particle geometry type \"" + pg[i] + "\" at index " + std::to_string(i) + " of field PAR_GEOM"); + } + } + + paramProvider.popScope(); + + const bool transportSuccess = _convDispOp.configureModelDiscretization(paramProvider, _disc.nComp, _disc.nPoints, 0); // strideCell not needed for DG, so just set to zero + + _disc.dispersion = Eigen::VectorXd::Zero(_disc.nComp); // fill later on with convDispOp (section and component dependent) + + _disc.velocity = static_cast(_convDispOp.currentVelocity()); // updated later on with convDispOp (section dependent) + _disc.curSection = -1; + + _disc.length_ = paramProvider.getDouble("COL_LENGTH"); + _disc.deltaZ = _disc.length_ / _disc.nCol; + + // Allocate memory + Indexer idxr(_disc); + + // Set whether analytic Jacobian is used + useAnalyticJacobian(analyticJac); + + // ==== Construct and configure binding model + + paramProvider.pushScope("adsorption"); + readScalarParameterOrArray(_disc.isKinetic, paramProvider, "IS_KINETIC", *_disc.strideBound); + paramProvider.popScope(); + + clearBindingModels(); + _binding = std::vector(_disc.nParType, nullptr); + + std::vector bindModelNames = { "NONE" }; + if (paramProvider.exists("ADSORPTION_MODEL")) + bindModelNames = paramProvider.getStringArray("ADSORPTION_MODEL"); + + if (paramProvider.exists("ADSORPTION_MODEL_MULTIPLEX")) + _singleBinding = (paramProvider.getInt("ADSORPTION_MODEL_MULTIPLEX") == 1); + else + { + // Infer multiplex mode + _singleBinding = (bindModelNames.size() == 1); + } + + if (!_singleBinding && (bindModelNames.size() < _disc.nParType)) + throw InvalidParameterException("Field ADSORPTION_MODEL contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + else if (_singleBinding && (bindModelNames.size() != 1)) + throw InvalidParameterException("Field ADSORPTION_MODEL requires (only) 1 element"); + + bool bindingConfSuccess = true; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_singleBinding && (i > 0)) + { + // Reuse first binding model + _binding[i] = _binding[0]; + } + else + { + _binding[i] = helper.createBindingModel(bindModelNames[i]); + if (!_binding[i]) + throw InvalidParameterException("Unknown binding model " + bindModelNames[i]); + + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", _singleBinding, i, _disc.nParType == 1, _binding[i]->usesParamProviderInDiscretizationConfig()); + bindingConfSuccess = _binding[i]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound + i * _disc.nComp, _disc.boundOffset + i * _disc.nComp) && bindingConfSuccess; + } + } + + // ==== Construct and configure dynamic reaction model + bool reactionConfSuccess = true; + + _dynReactionBulk = nullptr; + if (paramProvider.exists("REACTION_MODEL")) + { + const std::string dynReactName = paramProvider.getString("REACTION_MODEL"); + _dynReactionBulk = helper.createDynamicReactionModel(dynReactName); + if (!_dynReactionBulk) + throw InvalidParameterException("Unknown dynamic reaction model " + dynReactName); + + if (_dynReactionBulk->usesParamProviderInDiscretizationConfig()) + paramProvider.pushScope("reaction_bulk"); + + reactionConfSuccess = _dynReactionBulk->configureModelDiscretization(paramProvider, _disc.nComp, nullptr, nullptr); + + if (_dynReactionBulk->usesParamProviderInDiscretizationConfig()) + paramProvider.popScope(); + } + + clearDynamicReactionModels(); + _dynReaction = std::vector(_disc.nParType, nullptr); + + if (paramProvider.exists("REACTION_MODEL_PARTICLES")) + { + const std::vector dynReactModelNames = paramProvider.getStringArray("REACTION_MODEL_PARTICLES"); + + if (paramProvider.exists("REACTION_MODEL_PARTICLES_MULTIPLEX")) + _singleDynReaction = (paramProvider.getInt("REACTION_MODEL_PARTICLES_MULTIPLEX") == 1); + else + { + // Infer multiplex mode + _singleDynReaction = (dynReactModelNames.size() == 1); + } + + if (!_singleDynReaction && (dynReactModelNames.size() < _disc.nParType)) + throw InvalidParameterException("Field REACTION_MODEL_PARTICLES contains too few elements (" + std::to_string(_disc.nParType) + " required)"); + else if (_singleDynReaction && (dynReactModelNames.size() != 1)) + throw InvalidParameterException("Field REACTION_MODEL_PARTICLES requires (only) 1 element"); + + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_singleDynReaction && (i > 0)) + { + // Reuse first binding model + _dynReaction[i] = _dynReaction[0]; + } + else + { + _dynReaction[i] = helper.createDynamicReactionModel(dynReactModelNames[i]); + if (!_dynReaction[i]) + throw InvalidParameterException("Unknown dynamic reaction model " + dynReactModelNames[i]); + + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", _singleDynReaction, i, _disc.nParType == 1, _dynReaction[i]->usesParamProviderInDiscretizationConfig()); + reactionConfSuccess = _dynReaction[i]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound + i * _disc.nComp, _disc.boundOffset + i * _disc.nComp) && reactionConfSuccess; + } + } + } + + // Setup the memory for tempState based on state vector + _tempState = new double[numDofs()]; + + // Allocate Jacobian memory, set and analyze pattern + if (_disc.exactInt) + _jacInlet.resize(_disc.nNodes, 1); // first cell depends on inlet concentration (same for every component) + else + _jacInlet.resize(1, 1); // first cell depends on inlet concentration (same for every component) + _globalJac.resize(numPureDofs(), numPureDofs()); + _globalJacDisc.resize(numPureDofs(), numPureDofs()); + setGlobalJacPattern(_globalJac, _dynReactionBulk); + _globalJacDisc = _globalJac; + // the solver repetitively solves the linear system with a static pattern of the jacobian (set above). + // The goal of analyzePattern() is to reorder the nonzero elements of the matrix, such that the factorization step creates less fill-in + _globalSolver.analyzePattern(_globalJacDisc); + + return transportSuccess && bindingConfSuccess && reactionConfSuccess; +} + +bool LumpedRateModelWithPoresDG::configure(IParameterProvider& paramProvider) +{ + _parameters.clear(); + + const bool transportSuccess = _convDispOp.configure(_unitOpIdx, paramProvider, _parameters); + + // Read geometry parameters + _colPorosity = paramProvider.getDouble("COL_POROSITY"); + _singleParRadius = readAndRegisterMultiplexTypeParam(paramProvider, _parameters, _parRadius, "PAR_RADIUS", _disc.nParType, _unitOpIdx); + _singleParPorosity = readAndRegisterMultiplexTypeParam(paramProvider, _parameters, _parPorosity, "PAR_POROSITY", _disc.nParType, _unitOpIdx); + + // Read vectorial parameters (which may also be section dependent; transport) + _filmDiffusionMode = readAndRegisterMultiplexCompTypeSecParam(paramProvider, _parameters, _filmDiffusion, "FILM_DIFFUSION", _disc.nParType, _disc.nComp, _unitOpIdx); + + if (paramProvider.exists("PORE_ACCESSIBILITY")) + _poreAccessFactorMode = readAndRegisterMultiplexCompTypeSecParam(paramProvider, _parameters, _poreAccessFactor, "PORE_ACCESSIBILITY", _disc.nParType, _disc.nComp, _unitOpIdx); + else + { + _poreAccessFactorMode = MultiplexMode::ComponentType; + _poreAccessFactor = std::vector(_disc.nComp * _disc.nParType, 1.0); + } + + // Check whether PAR_TYPE_VOLFRAC is required or not + if ((_disc.nParType > 1) && !paramProvider.exists("PAR_TYPE_VOLFRAC")) + throw InvalidParameterException("The required parameter \"PAR_TYPE_VOLFRAC\" was not found"); + + // Let PAR_TYPE_VOLFRAC default to 1.0 for backwards compatibility + if (paramProvider.exists("PAR_TYPE_VOLFRAC")) + { + readScalarParameterOrArray(_parTypeVolFrac, paramProvider, "PAR_TYPE_VOLFRAC", 1); + if (_parTypeVolFrac.size() == _disc.nParType) + { + _axiallyConstantParTypeVolFrac = true; + + // Expand to all axial cells + _parTypeVolFrac.resize(_disc.nPoints * _disc.nParType, 1.0); + for (unsigned int i = 1; i < _disc.nPoints; ++i) + std::copy(_parTypeVolFrac.begin(), _parTypeVolFrac.begin() + _disc.nParType, _parTypeVolFrac.begin() + _disc.nParType * i); + } + else + _axiallyConstantParTypeVolFrac = false; + } + else + { + _parTypeVolFrac.resize(_disc.nPoints, 1.0); + _axiallyConstantParTypeVolFrac = false; + } + + // Check whether all sizes are matched + if (_disc.nParType != _parRadius.size()) + throw InvalidParameterException("Number of elements in field PAR_RADIUS does not match number of particle types"); + if (_disc.nParType * _disc.nPoints != _parTypeVolFrac.size()) + throw InvalidParameterException("Number of elements in field PAR_TYPE_VOLFRAC does not match number of particle types"); + if (_disc.nParType != _parPorosity.size()) + throw InvalidParameterException("Number of elements in field PAR_POROSITY does not match number of particle types"); + + if ((_filmDiffusion.size() < _disc.nComp * _disc.nParType) || (_filmDiffusion.size() % (_disc.nComp * _disc.nParType) != 0)) + throw InvalidParameterException("Number of elements in field FILM_DIFFUSION is not a positive multiple of NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ")"); + if (_disc.nComp * _disc.nParType != _poreAccessFactor.size()) + throw InvalidParameterException("Number of elements in field PORE_ACCESSIBILITY differs from NCOMP * NPARTYPE (" + std::to_string(_disc.nComp * _disc.nParType) + ")"); + + // Check that particle volume fractions sum to 1.0 + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + const double volFracSum = std::accumulate(_parTypeVolFrac.begin() + i * _disc.nParType, _parTypeVolFrac.begin() + (i + 1) * _disc.nParType, 0.0, + [](double a, const active& b) -> double { return a + static_cast(b); }); + if (std::abs(1.0 - volFracSum) > 1e-10) + throw InvalidParameterException("Sum of field PAR_TYPE_VOLFRAC differs from 1.0 (is " + std::to_string(volFracSum) + ") in axial cell " + std::to_string(i)); + } + + // Add parameters to map + _parameters[makeParamId(hashString("COL_POROSITY"), _unitOpIdx, CompIndep, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = &_colPorosity; + + if (_axiallyConstantParTypeVolFrac) + { + // Register only the first nParType items + for (unsigned int i = 0; i < _disc.nParType; ++i) + _parameters[makeParamId(hashString("PAR_TYPE_VOLFRAC"), _unitOpIdx, CompIndep, i, BoundStateIndep, ReactionIndep, SectionIndep)] = &_parTypeVolFrac[i]; + } + else + registerParam2DArray(_parameters, _parTypeVolFrac, [=](bool multi, unsigned cell, unsigned int type) { return makeParamId(hashString("PAR_TYPE_VOLFRAC"), _unitOpIdx, CompIndep, type, BoundStateIndep, ReactionIndep, cell); }, _disc.nParType); + + // Register initial conditions parameters + registerParam1DArray(_parameters, _initC, [=](bool multi, unsigned int comp) { return makeParamId(hashString("INIT_C"), _unitOpIdx, comp, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep); }); + + if (_singleBinding) + { + for (unsigned int c = 0; c < _disc.nComp; ++c) + _parameters[makeParamId(hashString("INIT_CP"), _unitOpIdx, c, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = &_initCp[c]; + } + else + registerParam2DArray(_parameters, _initCp, [=](bool multi, unsigned int type, unsigned int comp) { return makeParamId(hashString("INIT_CP"), _unitOpIdx, comp, type, BoundStateIndep, ReactionIndep, SectionIndep); }, _disc.nComp); + + + if (!_binding.empty()) + { + const unsigned int maxBoundStates = *std::max_element(_disc.strideBound, _disc.strideBound + _disc.nParType); + std::vector initParams(maxBoundStates); + + if (_singleBinding) + { + _binding[0]->fillBoundPhaseInitialParameters(initParams.data(), _unitOpIdx, ParTypeIndep); + + active* const iq = _initQ.data() + _disc.nBoundBeforeType[0]; + for (unsigned int i = 0; i < _disc.strideBound[0]; ++i) + _parameters[initParams[i]] = iq + i; + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + _binding[type]->fillBoundPhaseInitialParameters(initParams.data(), _unitOpIdx, type); + + active* const iq = _initQ.data() + _disc.nBoundBeforeType[type]; + for (unsigned int i = 0; i < _disc.strideBound[type]; ++i) + _parameters[initParams[i]] = iq + i; + } + } + } + + bool bindingConfSuccess = true; + if (!_binding.empty()) + { + if (_singleBinding) + { + if (_binding[0] && _binding[0]->requiresConfiguration()) + { + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", true); + bindingConfSuccess = _binding[0]->configure(paramProvider, _unitOpIdx, ParTypeIndep); + } + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_binding[type] || !_binding[type]->requiresConfiguration()) + continue; + + // Check whether required = true and no isActive() check should be performed + MultiplexedScopeSelector scopeGuard(paramProvider, "adsorption", type, _disc.nParType == 1, false); + if (!scopeGuard.isActive()) + continue; + + bindingConfSuccess = _binding[type]->configure(paramProvider, _unitOpIdx, type) && bindingConfSuccess; + } + } + } + + // Reconfigure reaction model + bool dynReactionConfSuccess = true; + if (_dynReactionBulk && _dynReactionBulk->requiresConfiguration()) + { + paramProvider.pushScope("reaction_bulk"); + dynReactionConfSuccess = _dynReactionBulk->configure(paramProvider, _unitOpIdx, ParTypeIndep); + paramProvider.popScope(); + } + + if (_singleDynReaction) + { + if (_dynReaction[0] && _dynReaction[0]->requiresConfiguration()) + { + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", true); + dynReactionConfSuccess = _dynReaction[0]->configure(paramProvider, _unitOpIdx, ParTypeIndep) && dynReactionConfSuccess; + } + } + else + { + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + if (!_dynReaction[type] || !_dynReaction[type]->requiresConfiguration()) + continue; + + MultiplexedScopeSelector scopeGuard(paramProvider, "reaction_particle", type, _disc.nParType == 1, true); + dynReactionConfSuccess = _dynReaction[type]->configure(paramProvider, _unitOpIdx, type) && dynReactionConfSuccess; + } + } + + return transportSuccess && bindingConfSuccess && dynReactionConfSuccess; +} + +unsigned int LumpedRateModelWithPoresDG::threadLocalMemorySize() const CADET_NOEXCEPT +{ + LinearMemorySizer lms; + + // Memory for residualImpl() + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + if (_binding[i] && _binding[i]->requiresWorkspace()) + lms.fitBlock(_binding[i]->workspaceSize(_disc.nComp, _disc.strideBound[i], _disc.nBound + i * _disc.nComp)); + + if (_dynReaction[i] && _dynReaction[i]->requiresWorkspace()) + lms.fitBlock(_dynReaction[i]->workspaceSize(_disc.nComp, _disc.strideBound[i], _disc.nBound + i * _disc.nComp)); + } + + if (_dynReactionBulk && _dynReactionBulk->requiresWorkspace()) + lms.fitBlock(_dynReactionBulk->workspaceSize(_disc.nComp, 0, nullptr)); + + const unsigned int maxStrideBound = *std::max_element(_disc.strideBound, _disc.strideBound + _disc.nParType); + lms.add(_disc.nComp + maxStrideBound); + lms.add((maxStrideBound + _disc.nComp) * (maxStrideBound + _disc.nComp)); + + lms.commit(); + const std::size_t resImplSize = lms.bufferSize(); + + // Memory for consistentInitialState() + lms.add(_nonlinearSolver->workspaceSize(_disc.nComp + maxStrideBound) * sizeof(double)); + lms.add(_disc.nComp + maxStrideBound); + lms.add(_disc.nComp + maxStrideBound); + lms.add(_disc.nComp + maxStrideBound); + lms.add((_disc.nComp + maxStrideBound) * (_disc.nComp + maxStrideBound)); + lms.add(_disc.nComp); + + lms.addBlock(resImplSize); + lms.commit(); + + // Memory for consistentInitialSensitivity + lms.add(_disc.nComp + maxStrideBound); + lms.add(maxStrideBound); + lms.commit(); + + return lms.bufferSize(); +} + +//@TODO for Eigen::SparseMatrix +unsigned int LumpedRateModelWithPoresDG::numAdDirsForJacobian() const CADET_NOEXCEPT +{ + // We need as many directions as the highest bandwidth of the diagonal blocks: + // The bandwidth of the column block depends on the size of the WENO stencil, whereas + // the bandwidth of the particle blocks are given by the number of components and bound states. + + // Get maximum stride of particle type blocks + //int maxStride = 0; + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // maxStride = std::max(maxStride, _jacP[type].stride()); + //} + + return 1; // std::max(_convDispOp.requiredADdirs(), maxStride); +} + +void LumpedRateModelWithPoresDG::useAnalyticJacobian(const bool analyticJac) +{ +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + _analyticJac = analyticJac; + if (!_analyticJac) + _jacobianAdDirs = 0; // numAdDirsForJacobian(); @TODO for Eigen::SparseMatrix + else + _jacobianAdDirs = 0; +#else + // If CADET_CHECK_ANALYTIC_JACOBIAN is active, we always enable AD for comparison and use it in simulation + _analyticJac = false; + _jacobianAdDirs = numAdDirsForJacobian(); +#endif +} + +// @TODO: backwards flow +void LumpedRateModelWithPoresDG::notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac) +{ + // Setup flux Jacobian blocks at the beginning of the simulation or in case of + // section dependent film or particle diffusion coefficients + if ((secIdx == 0) || isSectionDependent(_filmDiffusionMode)) + assembleFluxJacobian(t, secIdx); + + Indexer idxr(_disc); + + // ConvectionDispersionOperator tells us whether flow direction has changed + if (!_convDispOp.notifyDiscontinuousSectionTransition(t, secIdx)) { + // (re)compute DG Jaconian blocks + updateSection(secIdx); + _disc.initializeDGjac(); + return; + } + else { + // (re)compute DG Jaconian blocks + updateSection(secIdx); + _disc.initializeDGjac(); + } + + //// Setup the matrix connecting inlet DOFs to first column cells + //_jacInlet.clear(); + const double h = static_cast(_convDispOp.columnLength()) / static_cast(_disc.nCol); + const double u = static_cast(_convDispOp.currentVelocity()); + + //if (u >= 0.0) + //{ + // // Forwards flow + + // // Place entries for inlet DOF to first column cell conversion + // for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + // _jacInlet.addElement(comp * idxr.strideColComp(), comp, -u / h); + //} + //else + //{ + // // Backwards flow + + // // Place entries for inlet DOF to last column cell conversion + // const unsigned int offset = (_disc.nPoints - 1) * idxr.strideColNode(); + // for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + // _jacInlet.addElement(offset + comp * idxr.strideColComp(), comp, u / h); + //} +} + +void LumpedRateModelWithPoresDG::setFlowRates(active const* in, active const* out) CADET_NOEXCEPT +{ + _convDispOp.setFlowRates(in[0], out[0], _colPorosity); +} + +void LumpedRateModelWithPoresDG::reportSolution(ISolutionRecorder& recorder, double const* const solution) const +{ + Exporter expr(_disc, *this, solution); + recorder.beginUnitOperation(_unitOpIdx, *this, expr); + recorder.endUnitOperation(); +} + +void LumpedRateModelWithPoresDG::reportSolutionStructure(ISolutionRecorder& recorder) const +{ + Exporter expr(_disc, *this, nullptr); + recorder.unitOperationStructure(_unitOpIdx, *this, expr); +} + + +unsigned int LumpedRateModelWithPoresDG::requiredADdirs() const CADET_NOEXCEPT +{ +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + return _jacobianAdDirs; +#else + // If CADET_CHECK_ANALYTIC_JACOBIAN is active, we always need the AD directions for the Jacobian + return numAdDirsForJacobian(); +#endif +} +// @TODO: AD +void LumpedRateModelWithPoresDG::prepareADvectors(const AdJacobianParams& adJac) const +{ + // Early out if AD is disabled + if (!adJac.adY) + return; + + Indexer idxr(_disc); + + //// Column block + //_convDispOp.prepareADvectors(adJac); + + //// Particle block + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // const unsigned int lowerParBandwidth = _jacP[type].lowerBandwidth(); + // const unsigned int upperParBandwidth = _jacP[type].upperBandwidth(); + + // ad::prepareAdVectorSeedsForBandMatrix(adJac.adY + idxr.offsetCp(ParticleTypeIndex{ type }), adJac.adDirOffset, idxr.strideParBlock(type) * _disc.nPoints, lowerParBandwidth, upperParBandwidth, lowerParBandwidth); + //} +} + +/** + * @brief Extracts the system Jacobian from band compressed AD seed vectors + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ + // @TODO: AD +void LumpedRateModelWithPoresDG::extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset) +{ + Indexer idxr(_disc); + + //// Column + //_convDispOp.extractJacobianFromAD(adRes, adDirOffset); + + //// Particles + //for (unsigned int type = 0; type < _disc.nParType; ++type) + //{ + // linalg::BandMatrix& jacMat = _jacP[type]; + // ad::extractBandedJacobianFromAd(adRes + idxr.offsetCp(ParticleTypeIndex{ type }), adDirOffset, jacMat.lowerBandwidth(), jacMat); + //} +} + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + +/** + * @brief Compares the analytical Jacobian with a Jacobian derived by AD + * @details The analytical Jacobian is assumed to be stored in the corresponding band matrices. + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ +void LumpedRateModelWithPoresDG::checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const +{ + Indexer idxr(_disc); + + LOG(Debug) << "AD dir offset: " << adDirOffset << " DiagDirCol: " << _convDispOp.jacobian().lowerBandwidth(); + + // Column + const double maxDiffCol = _convDispOp.checkAnalyticJacobianAgainstAd(adRes, adDirOffset); + + // Particles + double maxDiffPar = 0.0; + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + const linalg::BandMatrix& jacMat = _jacP[type]; + const double localDiff = ad::compareBandedJacobianWithAd(adRes + idxr.offsetCp(ParticleTypeIndex{ type }), adDirOffset, jacMat.lowerBandwidth(), jacMat); + LOG(Debug) << "-> Par type " << type << " diff: " << localDiff; + maxDiffPar = std::max(maxDiffPar, localDiff); + } +} + +#endif + +int LumpedRateModelWithPoresDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidual); + + // Evaluate residual do not compute Jacobian or parameter sensitivities + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); +} + +int LumpedRateModelWithPoresDG::residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidual); + + //_FDjac = calcFDJacobian(simTime, threadLocalMem, 2.0); //todo delete + + // Evaluate residual, use AD for Jacobian if required but do not evaluate parameter derivatives + return residual(simTime, simState, res, adJac, threadLocalMem, true, false); +} + +int LumpedRateModelWithPoresDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, + const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity) +{ + if (updateJacobian) + { + _factorizeJacobian = true; + +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + if (_analyticJac) + { + if (paramSensitivity) + { + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } + else + { + // Compute Jacobian via AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; + } +#else + // Compute Jacobian via AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Only do comparison if we have a residuals vector (which is not always the case) + if (res) + { + // Evaluate with analytical Jacobian which is stored in the band matrices + retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + + // Compare AD with anaytic Jacobian + checkAnalyticJacobianAgainstAd(adJac.adRes, adJac.adDirOffset); + } + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; +#endif + } + else + { + if (paramSensitivity) + { + // initialize residuals with zero + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } +} + +template +int LumpedRateModelWithPoresDG::residualImpl(double t, unsigned int secIdx, StateType const* const y, double const* const yDot, ResidualType* const res, util::ThreadLocalStorage& threadLocalMem) +{ + // determine wether we have a section switch. If so, set velocity, dispersion, newStaticJac(bulk) + updateSection(secIdx); + bool success = 1; + + if (wantJac) + { + if (_disc.newStaticJac) { // ConvDisp static (per section) jacobian + + success = calcStaticAnaGlobalJacobian(secIdx); + _disc.newStaticJac = false; + if (cadet_unlikely(!success)) + LOG(Error) << "Jacobian pattern did not fit the Jacobian estimation"; + } + + } + + BENCH_START(_timerResidualPar); + + residualBulk(t, secIdx, y, yDot, res, threadLocalMem); + + for (unsigned int pblk = 0; pblk < _disc.nPoints * _disc.nParType; ++pblk) + { + const unsigned int type = (pblk) / _disc.nPoints; + const unsigned int par = (pblk) % _disc.nPoints; + residualParticle(t, type, par, secIdx, y, yDot, res, threadLocalMem); + + } + + BENCH_STOP(_timerResidualPar); + + residualFlux(t, secIdx, y, yDot, res); + + // Handle inlet DOFs, which are simply copied to res + for (unsigned int i = 0; i < _disc.nComp; ++i) + { + res[i] = y[i]; + } + +//Eigen::Map y_(reinterpret_cast(y), numDofs()); +//Eigen::Map res_(reinterpret_cast(res), numDofs()); +//Indexer idxr(_disc); +//if(!yDot) +// std::cout << "consistent initialization" << std::endl; +//std::cout << "y, res" << std::endl; +//std::cout << "inlet + bulk" << std::endl; +//for (int i = 0; i < _disc.nComp * (1+_disc.nPoints); i++) { +// std::cout << y_[i] << ", " << res_[i] << std::endl; +//} +//std::cout << "particle" << std::endl; +//for (int i = idxr.offsetCp(); i < idxr.offsetJf(); i++) { +// std::cout << y_[i] << ", " << res_[i] << std::endl; +//} +//std::cout << "flux" << std::endl; +//for (int i = idxr.offsetJf(); i < numDofs(); i++) { +// std::cout << y_[i] << ", " << res_[i] << std::endl; +//} + + return 0; +} + +template +int LumpedRateModelWithPoresDG::residualBulk(double t, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase, util::ThreadLocalStorage& threadLocalMem) +{ + Indexer idxr(_disc); + + // Eigen access to data pointers + const double* yPtr = reinterpret_cast(yBase); + const double* const ypPtr = reinterpret_cast(yDotBase); + double* const resPtr = reinterpret_cast(resBase); + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + + // extract current component mobile phase, mobile phase residual, mobile phase derivative (discontinous memory blocks) + Eigen::Map> C_comp(yPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> cRes_comp(resPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> cDot_comp(ypPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + + /* convection dispersion RHS */ + + _disc.boundary[0] = yPtr[comp]; // copy inlet DOFs to ghost node + ConvDisp_DG(C_comp, cRes_comp, t, comp); + + /* residual */ + + if (ypPtr) // NULLpointer for consistent initialization + cRes_comp = cDot_comp - cRes_comp; + } + + if (!_dynReactionBulk || (_dynReactionBulk->numReactionsLiquid() == 0)) + return 0; + + // Dynamic bulk reactions + StateType const* y = yBase + idxr.offsetC(); + ResidualType* res = resBase + idxr.offsetC(); + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + + for (unsigned int col = 0; col < _disc.nPoints; ++col, y += idxr.strideColNode(), res += idxr.strideColNode()) + { + const ColumnPosition colPos{ (0.5 + static_cast(col)) / static_cast(_disc.nCol), 0.0, 0.0 }; + _dynReactionBulk->residualLiquidAdd(t, secIdx, colPos, y, res, -1.0, tlmAlloc); + + if (wantJac) + { + linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, col * idxr.strideColNode()); + // static_cast should be sufficient here, but this statement is also analyzed when wantJac = false + _dynReactionBulk->analyticJacobianLiquidAdd(t, secIdx, colPos, reinterpret_cast(y), -1.0, jac, tlmAlloc); + } + } + + return 0; +} + +template +int LumpedRateModelWithPoresDG::residualParticle(double t, unsigned int parType, unsigned int colNode, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase, util::ThreadLocalStorage& threadLocalMem) +{ + Indexer idxr(_disc); + + // Go to the particle block of the given type and column cell + StateType const* y = yBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + double const* yDot = yDotBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + ResidualType* res = resBase + idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }); + + // Prepare parameters + const ParamType radius = static_cast(_parRadius[parType]); + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(colNode / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[colNode % _disc.nNodes])) / _disc.length_; + + const parts::cell::CellParameters cellResParams + { + _disc.nComp, + _disc.nBound + _disc.nComp * parType, + _disc.boundOffset + _disc.nComp * parType, + _disc.strideBound[parType], + _binding[parType]->reactionQuasiStationarity(), + _parPorosity[parType], + _poreAccessFactor.data() + _disc.nComp * parType, + _binding[parType], + (_dynReaction[parType] && (_dynReaction[parType]->numReactionsCombined() > 0)) ? _dynReaction[parType] : nullptr + }; + + linalg::BandedEigenSparseRowIterator jac(_globalJac, idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ colNode }) - idxr.offsetC()); + + // Handle time derivatives, binding, dynamic reactions + parts::cell::residualKernel( + t, secIdx, ColumnPosition{ z, 0.0, static_cast(radius) * 0.5 }, y, yDotBase ? yDot : nullptr, res, + jac, cellResParams, threadLocalMem.get() + ); + + return 0; +} + +template +int LumpedRateModelWithPoresDG::residualFlux(double t, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase) +{ + Indexer idxr(_disc); + + const ParamType invBetaC = 1.0 / static_cast(_colPorosity) - 1.0; + + // Get offsets + ResidualType* const resCol = resBase + idxr.offsetC(); + StateType const* const yCol = yBase + idxr.offsetC(); + + for (unsigned int type = 0; type < _disc.nParType; ++type) + { + ResidualType* const resParType = resBase + idxr.offsetCp(ParticleTypeIndex{ type }); + StateType const* const yParType = yBase + idxr.offsetCp(ParticleTypeIndex{ type }); + + const ParamType epsP = static_cast(_parPorosity[type]); + const ParamType radius = static_cast(_parRadius[type]); + active const* const filmDiff = getSectionDependentSlice(_filmDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + active const* const poreAccFactor = _poreAccessFactor.data() + type * _disc.nComp; + + const ParamType jacCF_val = invBetaC * _parGeomSurfToVol[type] / radius; + const ParamType jacPF_val = -_parGeomSurfToVol[type] / (epsP * radius); + + // Add flux to column void / bulk volume equations + for (unsigned int i = 0; i < _disc.nPoints * _disc.nComp; ++i) + { + const unsigned int colNode = i / _disc.nComp; + const unsigned int comp = i % _disc.nComp; + resCol[i] += jacCF_val * static_cast(filmDiff[comp]) * static_cast(_parTypeVolFrac[type + _disc.nParType * colNode]) * (yCol[i] - yParType[colNode * idxr.strideParBlock(type) + comp]); + } + + // Add flux to particle / bead volume equations + for (unsigned int pblk = 0; pblk < _disc.nPoints; ++pblk) + { + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + const unsigned int eq = pblk * idxr.strideColNode() + comp * idxr.strideColComp(); + resParType[pblk * idxr.strideParBlock(type) + comp] += jacPF_val / static_cast(poreAccFactor[comp]) * static_cast(filmDiff[comp]) * (yCol[eq] - yParType[pblk * idxr.strideParBlock(type) + comp]); + } + } + } + + return 0; +} + +void LumpedRateModelWithPoresDG::assembleFluxJacobian(double t, unsigned int secIdx) +{ + calcFluxJacobians(secIdx); +} + +int LumpedRateModelWithPoresDG::residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, + const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode and at the same time update the + // Jacobian (in one AD run, if analytic Jacobians are disabled) + return residual(simTime, simState, nullptr, adJac, threadLocalMem, true, true); +} + +int LumpedRateModelWithPoresDG::residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem) +{ + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adRes, threadLocalMem); +} + +int LumpedRateModelWithPoresDG::residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3) +{ + BENCH_SCOPE(_timerResidualSens); + + // tmp1 stores result of (dF / dy) * s + // tmp2 stores result of (dF / dyDot) * sDot + + for (std::size_t param = 0; param < yS.size(); ++param) + { + // Directional derivative (dF / dy) * s + multiplyWithJacobian(SimulationTime{ 0.0, 0u }, ConstSimulationState{ nullptr, nullptr }, yS[param], 1.0, 0.0, tmp1); + + // Directional derivative (dF / dyDot) * sDot + multiplyWithDerivativeJacobian(SimulationTime{ 0.0, 0u }, ConstSimulationState{ nullptr, nullptr }, ySdot[param], tmp2); + + double* const ptrResS = resS[param]; + + BENCH_START(_timerResidualSensPar); + + // Complete sens residual is the sum: + // TODO: Chunk TBB loop +#ifdef CADET_PARALLELIZE + tbb::parallel_for(std::size_t(0), static_cast(numDofs()), [&](std::size_t i) +#else + for (unsigned int i = 0; i < numDofs(); ++i) +#endif + { + ptrResS[i] = tmp1[i] + tmp2[i] + adRes[i].getADValue(param); + } CADET_PARFOR_END; + + BENCH_STOP(_timerResidualSensPar); + } + + return 0; +} + +/** + * @brief Multiplies the given vector with the system Jacobian (i.e., @f$ \frac{\partial F}{\partial y}\left(t, y, \dot{y}\right) @f$) + * @details Actually, the operation @f$ z = \alpha \frac{\partial F}{\partial y} x + \beta z @f$ is performed. + * + * Note that residual() or one of its cousins has to be called with the requested point @f$ (t, y, \dot{y}) @f$ once + * before calling multiplyWithJacobian() as this implementation ignores the given @f$ (t, y, \dot{y}) @f$. + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] yS Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] alpha Factor @f$ \alpha @f$ in front of @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] beta Factor @f$ \beta @f$ in front of @f$ z @f$ + * @param [in,out] ret Vector @f$ z @f$ which stores the result of the operation + */ +void LumpedRateModelWithPoresDG::multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret) +{ + Indexer idxr(_disc); + +// // Handle identity matrix of inlet DOFs +// for (unsigned int i = 0; i < _disc.nComp; ++i) +// { +// ret[i] = alpha * yS[i] + beta * ret[i]; +// } +// +//#ifdef CADET_PARALLELIZE +// tbb::parallel_for(std::size_t(0), static_cast(_disc.nParType + 1), [&](std::size_t idx) +//#else +// for (unsigned int idx = 0; idx < _disc.nParType + 1; ++idx) +//#endif +// { +// if (cadet_unlikely(idx == 0)) +// { +// // Interstitial block +// _convDispOp.jacobian().multiplyVector(yS + idxr.offsetC(), alpha, beta, ret + idxr.offsetC()); +// _jacCF.multiplyVector(yS + idxr.offsetJf(), alpha, 1.0, ret + idxr.offsetC()); +// } +// else +// { +// // Particle blocks +// const unsigned int type = idx - 1; +// const int localOffset = idxr.offsetCp(ParticleTypeIndex{ type }); +// _jacP[type].multiplyVector(yS + localOffset, alpha, beta, ret + localOffset); +// _jacPF[type].multiplyVector(yS + idxr.offsetJf(), alpha, 1.0, ret + localOffset); +// } +// } CADET_PARFOR_END; +// +// // Handle flux equation +// +// // Set fluxes(ret) = fluxes(yS) +// // This applies the identity matrix in the bottom right corner of the Jaocbian (flux equation) +// for (unsigned int i = idxr.offsetJf(); i < numDofs(); ++i) +// ret[i] = alpha * yS[i] + beta * ret[i]; +// +// double* const retJf = ret + idxr.offsetJf(); +// _jacFC.multiplyVector(yS + idxr.offsetC(), alpha, 1.0, retJf); +// for (unsigned int type = 0; type < _disc.nParType; ++type) +// { +// _jacFP[type].multiplyVector(yS + idxr.offsetCp(ParticleTypeIndex{ type }), alpha, 1.0, retJf); +// } +// +// // Map inlet DOFs to the column inlet (first bulk cells) +// _jacInlet.multiplyAdd(yS, ret + idxr.offsetC(), alpha); +} + +/** + * @brief Multiplies the time derivative Jacobian @f$ \frac{\partial F}{\partial \dot{y}}\left(t, y, \dot{y}\right) @f$ with a given vector + * @details The operation @f$ z = \frac{\partial F}{\partial \dot{y}} x @f$ is performed. + * The matrix-vector multiplication is performed matrix-free (i.e., no matrix is explicitly formed). + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] sDot Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ + * @param [out] ret Vector @f$ z @f$ which stores the result of the operation + */ +void LumpedRateModelWithPoresDG::multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret) +{ + Indexer idxr(_disc); + //tdod ? + +} + +void LumpedRateModelWithPoresDG::setExternalFunctions(IExternalFunction** extFuns, unsigned int size) +{ + for (IBindingModel* bm : _binding) + { + if (bm) + bm->setExternalFunctions(extFuns, size); + } +} + +unsigned int LumpedRateModelWithPoresDG::localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT +{ + // Inlets are duplicated so need to be accounted for + if (static_cast(_convDispOp.currentVelocity()) >= 0.0) + // Forward Flow: outlet is last cell + return _disc.nComp + (_disc.nPoints - 1) * _disc.nComp; + else + // Backward flow: Outlet is first cell + return _disc.nComp; +} + +unsigned int LumpedRateModelWithPoresDG::localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT +{ + return 0; +} + +unsigned int LumpedRateModelWithPoresDG::localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT +{ + return 1; +} + +unsigned int LumpedRateModelWithPoresDG::localInletComponentStride(unsigned int port) const CADET_NOEXCEPT +{ + return 1; +} + +void LumpedRateModelWithPoresDG::expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut) +{ + // @todo Write this function +} + +bool LumpedRateModelWithPoresDG::setParameter(const ParameterId& pId, double value) +{ + if (pId.unitOperation == _unitOpIdx) + { + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return false; + if (pId.particleType >= _disc.nParType) + return false; + + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setValue(value); + + return true; + } + + if (multiplexTypeParameterValue(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, value, nullptr)) + return true; + if (multiplexTypeParameterValue(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, value, nullptr)) + return true; + + if (multiplexCompTypeSecParameterValue(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, value, nullptr)) + return true; + if (multiplexCompTypeSecParameterValue(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, value, nullptr)) + return true; + + const int mpIc = multiplexInitialConditions(pId, value, false); + if (mpIc > 0) + return true; + else if (mpIc < 0) + return false; + + if (_convDispOp.setParameter(pId, value)) + return true; + } + + return UnitOperationBase::setParameter(pId, value); +} + +void LumpedRateModelWithPoresDG::setSensitiveParameterValue(const ParameterId& pId, double value) +{ + if (pId.unitOperation == _unitOpIdx) + { + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return; + if (pId.particleType >= _disc.nParType) + return; + + if (!contains(_sensParams, &_parTypeVolFrac[pId.particleType])) + return; + + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setValue(value); + + return; + } + + if (multiplexTypeParameterValue(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, value, &_sensParams)) + return; + if (multiplexTypeParameterValue(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, value, &_sensParams)) + return; + + if (multiplexCompTypeSecParameterValue(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, value, &_sensParams)) + return; + if (multiplexCompTypeSecParameterValue(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, value, &_sensParams)) + return; + if (multiplexInitialConditions(pId, value, true) != 0) + return; + + if (_convDispOp.setSensitiveParameterValue(_sensParams, pId, value)) + return; + } + + UnitOperationBase::setSensitiveParameterValue(pId, value); +} + +bool LumpedRateModelWithPoresDG::setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue) +{ + if (pId.unitOperation == _unitOpIdx) + { + // Intercept changes to PAR_TYPE_VOLFRAC when not specified per axial cell (but once globally) + if (_axiallyConstantParTypeVolFrac && (pId.name == hashString("PAR_TYPE_VOLFRAC"))) + { + if ((pId.section != SectionIndep) || (pId.component != CompIndep) || (pId.boundState != BoundStateIndep) || (pId.reaction != ReactionIndep)) + return false; + if (pId.particleType >= _disc.nParType) + return false; + + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + + // Register parameter and set AD seed / direction + _sensParams.insert(&_parTypeVolFrac[pId.particleType]); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + _parTypeVolFrac[i * _disc.nParType + pId.particleType].setADValue(adDirection, adValue); + + return true; + } + + if (multiplexTypeParameterAD(pId, hashString("PAR_RADIUS"), _singleParRadius, _parRadius, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexTypeParameterAD(pId, hashString("PAR_POROSITY"), _singleParPorosity, _parPorosity, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexCompTypeSecParameterAD(pId, hashString("FILM_DIFFUSION"), _filmDiffusionMode, _filmDiffusion, _disc.nParType, _disc.nComp, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + if (multiplexCompTypeSecParameterAD(pId, hashString("PORE_ACCESSIBILITY"), _poreAccessFactorMode, _poreAccessFactor, _disc.nParType, _disc.nComp, adDirection, adValue, _sensParams)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + const int mpIc = multiplexInitialConditions(pId, adDirection, adValue); + if (mpIc > 0) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + else if (mpIc < 0) + return false; + + if (_convDispOp.setSensitiveParameter(_sensParams, pId, adDirection, adValue)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + } + + return UnitOperationBase::setSensitiveParameter(pId, adDirection, adValue); +} + +int LumpedRateModelWithPoresDG::Exporter::writeMobilePhase(double* buffer) const +{ + const int blockSize = _disc.nComp * _disc.nPoints; + std::copy_n(_idx.c(_data), blockSize, buffer); + return blockSize; +} + +int LumpedRateModelWithPoresDG::Exporter::writeSolidPhase(double* buffer) const +{ + int numWritten = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + const int n = writeParticleMobilePhase(i, buffer); + buffer += n; + numWritten += n; + } + return numWritten; +} + +int LumpedRateModelWithPoresDG::Exporter::writeParticleMobilePhase(double* buffer) const +{ + int numWritten = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + { + const int n = writeParticleMobilePhase(i, buffer); + buffer += n; + numWritten += n; + } + return numWritten; +} + +int LumpedRateModelWithPoresDG::Exporter::writeSolidPhase(unsigned int parType, double* buffer) const +{ + cadet_assert(parType < _disc.nParType); + + const unsigned int stride = _disc.nComp + _disc.strideBound[parType]; + double const* ptr = _data + _idx.offsetCp(ParticleTypeIndex{ parType }) + _idx.strideParLiquid(); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + std::copy_n(ptr, _disc.strideBound[parType], buffer); + buffer += _disc.strideBound[parType]; + ptr += stride; + } + return _disc.nPoints * _disc.strideBound[parType]; +} + +int LumpedRateModelWithPoresDG::Exporter::writeParticleMobilePhase(unsigned int parType, double* buffer) const +{ + cadet_assert(parType < _disc.nParType); + + const unsigned int stride = _disc.nComp + _disc.strideBound[parType]; + double const* ptr = _data + _idx.offsetCp(ParticleTypeIndex{ parType }); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + std::copy_n(ptr, _disc.nComp, buffer); + buffer += _disc.nComp; + ptr += stride; + } + return _disc.nPoints * _disc.nComp; +} + +//int LumpedRateModelWithPoresDG::Exporter::writeParticleFlux(double* buffer) const { return 0; } +// +//int LumpedRateModelWithPoresDG::Exporter::writeParticleFlux(unsigned int parType, double* buffer) const { return 0; } + +int LumpedRateModelWithPoresDG::Exporter::writeInlet(unsigned int port, double* buffer) const +{ + cadet_assert(port == 0); + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; +} + +int LumpedRateModelWithPoresDG::Exporter::writeInlet(double* buffer) const +{ + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; +} + +int LumpedRateModelWithPoresDG::Exporter::writeOutlet(unsigned int port, double* buffer) const +{ + cadet_assert(port == 0); + + if (_model._convDispOp.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; +} + +int LumpedRateModelWithPoresDG::Exporter::writeOutlet(double* buffer) const +{ + if (_model._convDispOp.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; +} + +} // namespace model + +} // namespace cadet + + +#include "model/LumpedRateModelWithPoresDG-InitialConditions.cpp" +#include "model/LumpedRateModelWithPoresDG-LinearSolver.cpp" + +namespace cadet +{ + +namespace model +{ + +void registerLumpedRateModelWithPoresDG(std::unordered_map>& models) +{ + models[LumpedRateModelWithPoresDG::identifier()] = [](UnitOpIdx uoId) { return new LumpedRateModelWithPoresDG(uoId); }; + models["LRMPDG"] = [](UnitOpIdx uoId) { return new LumpedRateModelWithPoresDG(uoId); }; +} + +} // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/LumpedRateModelWithPoresDG.hpp b/src/libcadet/model/LumpedRateModelWithPoresDG.hpp new file mode 100644 index 000000000..73830dca7 --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithPoresDG.hpp @@ -0,0 +1,2001 @@ +// ============================================================================= +// CADET +// +// Copyright � 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +/** + * @file + * Defines the lumped rate model with pores (LRMP). + */ + +#ifndef LIBCADET_LUMPEDRATEMODELWITHPORESDG_HPP_ +#define LIBCADET_LUMPEDRATEMODELWITHPORESDG_HPP_ + +#include "BindingModel.hpp" +#include "ParallelSupport.hpp" + +#include "UnitOperationBase.hpp" +#include "cadet/StrongTypes.hpp" +#include "cadet/SolutionExporter.hpp" +#include "model/parts/ConvectionDispersionOperator.hpp" +#include "AutoDiff.hpp" +#include "linalg/BandedEigenSparseRowIterator.hpp" +#include "linalg/SparseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "Memory.hpp" +#include "model/ModelUtils.hpp" +#include "ParameterMultiplexing.hpp" + +#include +#include +#include +#include + +#include "Benchmark.hpp" + +using namespace Eigen; + +namespace cadet +{ + +namespace model +{ + +class IDynamicReactionModel; + +/** + * @brief Lumped rate model of liquid column chromatography with pores + * @details See @cite Guiochon2006, @cite Gu1995, @cite Felinger2004 + * + * @f[\begin{align} + \frac{\partial c_i}{\partial t} &= - u \frac{\partial c_i}{\partial z} + D_{\text{ax},i} \frac{\partial^2 c_i}{\partial z^2} - \frac{1 - \varepsilon_c}{\varepsilon_c} \frac{3 k_{f,i}}{r_p} j_{f,i} \\ + \frac{\partial c_{p,i}}{\partial t} + \frac{1 - \varepsilon_p}{\varepsilon_p} \frac{\partial q_{i}}{\partial t} &= \frac{3 k_{f,i}}{\varepsilon_p r_p} j_{f,i} \\ + a \frac{\partial q_i}{\partial t} &= f_{\text{iso}}(c_p, q) +\end{align} @f] +@f[ \begin{align} + j_{f,i} = c_i - c_{p,i} +\end{align} @f] + * Danckwerts boundary conditions (see @cite Danckwerts1953) +@f[ \begin{align} +u c_{\text{in},i}(t) &= u c_i(t,0) - D_{\text{ax},i} \frac{\partial c_i}{\partial z}(t,0) \\ +\frac{\partial c_i}{\partial z}(t,L) &= 0 +\end{align} @f] + * Methods are described in @cite Breuer2023 (DGSEM discretization), @cite Puttmann2013 @cite Puttmann2016 (forward sensitivities, AD, band compression) + */ +class LumpedRateModelWithPoresDG : public UnitOperationBase +{ +public: + + LumpedRateModelWithPoresDG(UnitOpIdx unitOpIdx); + virtual ~LumpedRateModelWithPoresDG() CADET_NOEXCEPT; + + virtual unsigned int numDofs() const CADET_NOEXCEPT; + virtual unsigned int numPureDofs() const CADET_NOEXCEPT; + virtual bool usesAD() const CADET_NOEXCEPT; + virtual unsigned int requiredADdirs() const CADET_NOEXCEPT; + + virtual UnitOpIdx unitOperationId() const CADET_NOEXCEPT { return _unitOpIdx; } + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual void setFlowRates(active const* in, active const* out) CADET_NOEXCEPT; + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual bool canAccumulate() const CADET_NOEXCEPT { return false; } + + static const char* identifier() { return "LUMPED_RATE_MODEL_WITH_PORES_DG"; } + virtual const char* unitOperationName() const CADET_NOEXCEPT { return identifier(); } + + virtual bool configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper); + virtual bool configure(IParameterProvider& paramProvider); + virtual void notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac); + + virtual void useAnalyticJacobian(const bool analyticJac); + + virtual void reportSolution(ISolutionRecorder& recorder, double const* const solution) const; + virtual void reportSolutionStructure(ISolutionRecorder& recorder) const; + + virtual int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3); + + virtual int linearSolve(double t, double alpha, double tol, double* const rhs, double const* const weight, + const ConstSimulationState& simState); + + virtual void prepareADvectors(const AdJacobianParams& adJac) const; + + virtual void applyInitialCondition(const SimulationState& simState) const; + virtual void readInitialCondition(IParameterProvider& paramProvider); + + virtual void consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem); + + virtual void initializeSensitivityStates(const std::vector& vecSensY) const; + virtual void consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual bool hasInlet() const CADET_NOEXCEPT { return true; } + virtual bool hasOutlet() const CADET_NOEXCEPT { return true; } + + virtual unsigned int localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentStride(unsigned int port) const CADET_NOEXCEPT; + + virtual void setExternalFunctions(IExternalFunction** extFuns, unsigned int size); + virtual void setSectionTimes(double const* secTimes, bool const* secContinuity, unsigned int nSections) { } + + virtual void expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut); + + virtual void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret); + virtual void multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret); + + inline void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double* ret) + { + multiplyWithJacobian(simTime, simState, yS, 1.0, 0.0, ret); + } + + virtual bool setParameter(const ParameterId& pId, double value); + virtual bool setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue); + virtual void setSensitiveParameterValue(const ParameterId& id, double value); + + virtual unsigned int threadLocalMemorySize() const CADET_NOEXCEPT; + +#ifdef CADET_BENCHMARK_MODE + virtual std::vector benchmarkTimings() const + { + return std::vector({ + static_cast(numDofs()), + _timerResidual.totalElapsedTime(), + _timerResidualPar.totalElapsedTime(), + _timerResidualSens.totalElapsedTime(), + _timerResidualSensPar.totalElapsedTime(), + _timerJacobianPar.totalElapsedTime(), + _timerConsistentInit.totalElapsedTime(), + _timerConsistentInitPar.totalElapsedTime(), + _timerLinearSolve.totalElapsedTime(), + _timerFactorize.totalElapsedTime(), + _timerFactorizePar.totalElapsedTime(), + _timerMatVec.totalElapsedTime(), + }); + } + + virtual char const* const* benchmarkDescriptions() const + { + static const char* const desc[] = { + "DOFs", + "Residual", + "ResidualPar", + "ResidualSens", + "ResidualSensPar", + "JacobianPar", + "ConsistentInit", + "ConsistentInitPar", + "LinearSolve", + "Factorize", + "FactorizePar", + "MatVec", + }; + return desc; + } +#endif + +protected: + + class Indexer; + + int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity); + + template + int residualImpl(double t, unsigned int secIdx, StateType const* const y, double const* const yDot, ResidualType* const res, util::ThreadLocalStorage& threadLocalMem); + + template + int residualBulk(double t, unsigned int secIdx, StateType const* yBase, double const* yDotBase, ResidualType* resBase, util::ThreadLocalStorage& threadLocalMem); + + template + int residualParticle(double t, unsigned int parType, unsigned int colCell, unsigned int secIdx, StateType const* y, double const* yDot, ResidualType* res, util::ThreadLocalStorage& threadLocalMem); + + template + int residualFlux(double t, unsigned int secIdx, StateType const* y, double const* yDot, ResidualType* res); + + void assembleFluxJacobian(double t, unsigned int secIdx); + void extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset); + + void assembleDiscretizedGlobalJacobian(double alpha, Indexer idxr); + + void addTimeDerivativeToJacobianParticleBlock(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, unsigned int parType); + + unsigned int numAdDirsForJacobian() const CADET_NOEXCEPT; + + int multiplexInitialConditions(const cadet::ParameterId& pId, unsigned int adDirection, double adValue); + int multiplexInitialConditions(const cadet::ParameterId& pId, double val, bool checkSens); + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + void checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const; +#endif + + class Discretization + { + public: + unsigned int nParType; //!< Number of particle types + unsigned int* parTypeOffset; //!< Array with offsets (in particle block) to particle type, additional last element contains total number of particle DOFs + unsigned int* nBound; //!< Array with number of bound states for each component and particle type (particle type major ordering) + unsigned int* boundOffset; //!< Array with offset to the first bound state of each component in the solid phase (particle type major ordering) + unsigned int* strideBound; //!< Total number of bound states for each particle type, additional last element contains total number of bound states for all types + unsigned int* nBoundBeforeType; //!< Array with number of bound states before a particle type (cumulative sum of strideBound) + unsigned int nComp; //!< Number of components + + bool exactInt; //!< 1 for exact integration, 0 for LGL quadrature + unsigned int nCol; //!< Number of column cells + unsigned int polyDeg; //!< polynomial degree + unsigned int nNodes; //!< Number of nodes per cell + unsigned int nPoints; //!< Number of discrete Points + + Eigen::VectorXd nodes; //!< Array with positions of nodes in reference element + Eigen::MatrixXd polyDerM; //!< Array with polynomial derivative Matrix + Eigen::VectorXd invWeights; //!< Array with weights for numerical quadrature of size nNodes + Eigen::MatrixXd invMM; //!< dense inverse mass matrix for exact integration + double deltaZ; //!< cell spacing + + Eigen::MatrixXd* DGjacAxDispBlocks; //!< axial dispersion blocks of DG jacobian (unique blocks only) + Eigen::MatrixXd DGjacAxConvBlock; //!< axial convection block of DG jacobian + + Eigen::VectorXd dispersion; //!< Column dispersion (may be section and component dependent) + bool _dispersionCompIndep; //!< Determines whether dispersion is component independent + double velocity; //!< Interstitial velocity (may be section dependent) \f$ u \f$ + double crossSection; //!< Cross section area + int curSection; //!< current section index + + double length_; + double porosity; + + Eigen::VectorXd g; //!< auxiliary variable + Eigen::VectorXd h; //!< auxiliary substitute + Eigen::VectorXd surfaceFlux; //!< stores the surface flux values + Eigen::Vector4d boundary; //!< stores the boundary values from Danckwert boundary conditions + + std::vector isKinetic; //!< binding kinetics + + bool newStaticJac; //!< determines wether static analytical jacobian needs to be computed (every section) + + /** + * @brief computes LGL nodes, integration weights, polynomial derivative matrix + */ + void initializeDG() { + + nNodes = polyDeg + 1; + nPoints = nNodes * nCol; + // Allocate space for DG discretization + nodes.resize(nNodes); + nodes.setZero(); + invWeights.resize(nNodes); + invWeights.setZero(); + polyDerM.resize(nNodes, nNodes); + polyDerM.setZero(); + invMM.resize(nNodes, nNodes); + invMM.setZero(); + + g.resize(nPoints); + g.setZero(); + h.resize(nPoints); + h.setZero(); + boundary.setZero(); + surfaceFlux.resize(nCol + 1); + surfaceFlux.setZero(); + + newStaticJac = true; + + lglNodesWeights(); + invMMatrix(); + derivativeMatrix(); + } + + void initializeDGjac() { + DGjacAxDispBlocks = new MatrixXd[(exactInt ? std::min(nCol, 5u) : std::min(nCol, 3u))]; + // we only need unique dispersion blocks, which are given by cells 1, 2, nCol for inexact integration DG and by cells 1, 2, 3, nCol-1, nCol for eaxct integration DG + DGjacAxDispBlocks[0] = DGjacobianDispBlock(1); + if (nCol > 1) + DGjacAxDispBlocks[1] = DGjacobianDispBlock(2); + if (nCol > 2 && exactInt) + DGjacAxDispBlocks[2] = DGjacobianDispBlock(3); + else if (nCol > 2 && !exactInt) + DGjacAxDispBlocks[2] = DGjacobianDispBlock(nCol); + if (exactInt && nCol > 3) + DGjacAxDispBlocks[3] = DGjacobianDispBlock(std::max(4u, nCol - 1u)); + if (exactInt && nCol > 4) + DGjacAxDispBlocks[4] = DGjacobianDispBlock(nCol); + + DGjacAxConvBlock = DGjacobianConvBlock(); + } + + private: + + /* =================================================================================== + * Polynomial Basis operators and auxiliary functions + * =================================================================================== */ + + /** + * @brief computes the Legendre polynomial L_N and q = L_N+1 - L_N-2 and q' at point x + * @param [in] polyDeg polynomial degree of spatial Discretization + * @param [in] x evaluation point + * @param [in] L <- L(x) + * @param [in] q <- q(x) = L_N+1 (x) - L_N-2(x) + * @param [in] qder <- q'(x) = [L_N+1 (x) - L_N-2(x)]' + */ + void qAndL(const double x, double& L, double& q, double& qder) { + // auxiliary variables (Legendre polynomials) + double L_2 = 1.0; + double L_1 = x; + double Lder_2 = 0.0; + double Lder_1 = 1.0; + double Lder = 0.0; + for (double k = 2; k <= polyDeg; k++) { // note that this function is only called for polyDeg >= 2. + L = ((2 * k - 1) * x * L_1 - (k - 1) * L_2) / k; + Lder = Lder_2 + (2 * k - 1) * L_1; + L_2 = L_1; + L_1 = L; + Lder_2 = Lder_1; + Lder_1 = Lder; + } + q = ((2.0 * polyDeg + 1) * x * L - polyDeg * L_2) / (polyDeg + 1.0) - L_2; + qder = Lder_1 + (2.0 * polyDeg + 1) * L_1 - Lder_2; + } + + /** + * @brief computes the Legendre-Gauss-Lobatto nodes and (inverse) quadrature weights + * @detail inexact LGL-quadrature leads to a diagonal mass matrix (mass lumping), defined by the quadrature weights + */ + void lglNodesWeights() { + + const double pi = 3.1415926535897932384626434; + + // tolerance and max #iterations for Newton iteration + int nIterations = 10; + double tolerance = 1e-15; + // Legendre polynomial and derivative + double L = 0; + double q = 0; + double qder = 0; + switch (polyDeg) { + case 0: + throw std::invalid_argument("Polynomial degree must be at least 1 !"); + break; + case 1: + nodes[0] = -1; + invWeights[0] = 1; + nodes[1] = 1; + invWeights[1] = 1; + break; + default: + nodes[0] = -1; + nodes[polyDeg] = 1; + invWeights[0] = 2.0 / (polyDeg * (polyDeg + 1.0)); + invWeights[polyDeg] = invWeights[0]; + // use symmetrie, only compute half of points and weights + for (unsigned int j = 1; j <= floor((polyDeg + 1) / 2) - 1; j++) { + // first guess for Newton iteration + nodes[j] = -cos(pi * (j + 0.25) / polyDeg - 3 / (8.0 * polyDeg * pi * (j + 0.25))); + // Newton iteration to find roots of Legendre Polynomial + for (unsigned int k = 0; k <= nIterations; k++) { + qAndL(nodes[j], L, q, qder); + nodes[j] = nodes[j] - q / qder; + if (abs(q / qder) <= tolerance * abs(nodes[j])) { + break; + } + } + // calculate weights + qAndL(nodes[j], L, q, qder); + invWeights[j] = 2.0 / (polyDeg * (polyDeg + 1.0) * pow(L, 2.0)); + nodes[polyDeg - j] = -nodes[j]; // copy to second half of points and weights + invWeights[polyDeg - j] = invWeights[j]; + } + } + if (polyDeg % 2 == 0) { // for even polyDeg we have an odd number of points which include 0.0 + qAndL(0.0, L, q, qder); + nodes[polyDeg / 2] = 0; + invWeights[polyDeg / 2] = 2.0 / (polyDeg * (polyDeg + 1.0) * pow(L, 2.0)); + } + // inverse the weights + invWeights = invWeights.cwiseInverse(); + } + + /** + * @brief computation of barycentric weights for fast polynomial evaluation + * @param [in] baryWeights vector to store barycentric weights. Must already be initialized with ones! + */ + void barycentricWeights(Eigen::VectorXd& baryWeights) { + for (unsigned int j = 1; j <= polyDeg; j++) { + for (unsigned int k = 0; k <= j - 1; k++) { + baryWeights[k] = baryWeights[k] * (nodes[k] - nodes[j]) * 1.0; + baryWeights[j] = baryWeights[j] * (nodes[j] - nodes[k]) * 1.0; + } + } + for (unsigned int j = 0; j <= polyDeg; j++) { + baryWeights[j] = 1 / baryWeights[j]; + } + } + + /** + * @brief computation of nodal (lagrange) polynomial derivative matrix + */ + void derivativeMatrix() { + Eigen::VectorXd baryWeights = Eigen::VectorXd::Ones(polyDeg + 1u); + barycentricWeights(baryWeights); + for (unsigned int i = 0; i <= polyDeg; i++) { + for (unsigned int j = 0; j <= polyDeg; j++) { + if (i != j) { + polyDerM(i, j) = baryWeights[j] / (baryWeights[i] * (nodes[i] - nodes[j])); + polyDerM(i, i) += -polyDerM(i, j); + } + } + } + } + + /** + * @brief factor to normalize legendre polynomials + */ + double orthonFactor(int polyDeg) { + + double n = static_cast (polyDeg); + // alpha = beta = 0 to get legendre polynomials as special case from jacobi polynomials. + double a = 0.0; + double b = 0.0; + return std::sqrt(((2.0 * n + a + b + 1.0) * std::tgamma(n + 1.0) * std::tgamma(n + a + b + 1.0)) + / (std::pow(2.0, a + b + 1.0) * std::tgamma(n + a + 1.0) * std::tgamma(n + b + 1.0))); + } + + /** + * @brief calculates the Vandermonde matrix of the normalized legendre polynomials + */ + Eigen::MatrixXd getVandermonde_LEGENDRE() { + + Eigen::MatrixXd V(nodes.size(), nodes.size()); + + double alpha = 0.0; + double beta = 0.0; + + // degree 0 + V.block(0, 0, nNodes, 1) = VectorXd::Ones(nNodes) * orthonFactor(0); + // degree 1 + for (int node = 0; node < static_cast(nNodes); node++) { + V(node, 1) = nodes[node] * orthonFactor(1); + } + + for (int deg = 2; deg <= static_cast(polyDeg); deg++) { + + for (int node = 0; node < static_cast(nNodes); node++) { + + double orthn_1 = orthonFactor(deg) / orthonFactor(deg - 1); + double orthn_2 = orthonFactor(deg) / orthonFactor(deg - 2); + + double fac_1 = ((2.0 * deg - 1.0) * 2.0 * deg * (2.0 * deg - 2.0) * nodes[node]) / (2.0 * deg * deg * (2.0 * deg - 2.0)); + double fac_2 = (2.0 * (deg - 1.0) * (deg - 1.0) * 2.0 * deg) / (2.0 * deg * deg * (2.0 * deg - 2.0)); + + V(node, deg) = orthn_1 * fac_1 * V(node, deg - 1) - orthn_2 * fac_2 * V(node, deg - 2); + + } + + } + + return V; + } + /** + * @brief calculates mass matrix for exact polynomial integration + * @detail exact polynomial integration leads to a full mass matrix + */ + void invMMatrix() { + invMM = (getVandermonde_LEGENDRE() * (getVandermonde_LEGENDRE().transpose())); + } + /** + * @brief calculates the convection part of the DG jacobian + */ + MatrixXd DGjacobianConvBlock() { + + // Convection block [ d RHS_conv / d c ], additionally depends on upwind flux part from corresponding neighbour cell + MatrixXd convBlock = MatrixXd::Zero(nNodes, nNodes + 1); + + if (velocity >= 0.0) { // forward flow -> Convection block additionally depends on last entry of previous cell + convBlock.block(0, 1, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, 0, nNodes, 1) += invMM.block(0, 0, nNodes, 1); + convBlock.block(0, 1, nNodes, 1) -= invMM.block(0, 0, nNodes, 1); + } + else { + convBlock(0, 0) += invWeights[0]; + convBlock(0, 1) -= invWeights[0]; + } + } + else { // backward flow -> Convection block additionally depends on first entry of subsequent cell + convBlock.block(0, 0, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, nNodes - 1, nNodes, 1) += invMM.block(0, nNodes - 1, nNodes, 1); + convBlock.block(0, nNodes, nNodes, 1) -= invMM.block(0, nNodes - 1, nNodes, 1); + } + else { + convBlock(nNodes - 1, nNodes - 1) += invWeights[nNodes - 1]; + convBlock(nNodes - 1, nNodes) -= invWeights[nNodes - 1]; + } + } + convBlock *= 2 / deltaZ; + + return -convBlock; // *-1 for residual + } + /** + * @brief calculates the DG Jacobian auxiliary block + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd getGBlock(unsigned int cellIdx) { + + // Auxiliary Block [ d g(c) / d c ], additionally depends on boundary entries of neighbouring cells + MatrixXd gBlock = MatrixXd::Zero(nNodes, nNodes + 2); + gBlock.block(0, 1, nNodes, nNodes) = polyDerM; + if (exactInt) { + if (cellIdx != 1 && cellIdx != nCol) { + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == 1) { // left + if (cellIdx == nCol) + return gBlock * 2 / deltaZ; + ; + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == nCol) { // right + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + } + else if (cellIdx == 0 || cellIdx == nCol + 1) { + gBlock.setZero(); + } + gBlock *= 2 / deltaZ; + } + else { + if (cellIdx == 0 || cellIdx == nCol + 1) + return MatrixXd::Zero(nNodes, nNodes + 2); + + gBlock(0, 0) -= 0.5 * invWeights[0]; + gBlock(0, 1) += 0.5 * invWeights[0]; + gBlock(nNodes - 1, nNodes) -= 0.5 * invWeights[nNodes - 1]; + gBlock(nNodes - 1, nNodes + 1) += 0.5 * invWeights[nNodes - 1]; + gBlock *= 2 / deltaZ; + + if (cellIdx == 1) { + // adjust auxiliary Block [ d g(c) / d c ] for left boundary cell + gBlock(0, 1) -= 0.5 * invWeights[0] * 2 / deltaZ; + if (cellIdx == nCol) { // adjust for special case one cell + gBlock(0, 0) += 0.5 * invWeights[0] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes + 1) -= 0.5 * invWeights[nNodes - 1] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + else if (cellIdx == nCol) { + // adjust auxiliary Block [ d g(c) / d c ] for right boundary cell + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + + return gBlock; + } + /** + * @brief calculates the num. flux part of a dispersion DG Jacobian block + * @param [in] cellIdx cell index + * @param [in] leftG left neighbour auxiliary block + * @param [in] middleG neighbour auxiliary block + * @param [in] rightG neighbour auxiliary block + */ + Eigen::MatrixXd auxBlockGstar(unsigned int cellIdx, MatrixXd leftG, MatrixXd middleG, MatrixXd rightG) { + + // auxiliary block [ d g^* / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + MatrixXd gStarDC = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + // NOTE: N = polyDeg + // indices gStarDC : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),... , -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + // auxiliary block [d g^* / d c] + if (cellIdx != 1) { + gStarDC.block(0, nNodes, 1, nNodes + 2) += middleG.block(0, 0, 1, nNodes + 2); + gStarDC.block(0, 0, 1, nNodes + 2) += leftG.block(nNodes - 1, 0, 1, nNodes + 2); + } + if (cellIdx != nCol) { + gStarDC.block(nNodes - 1, nNodes, 1, nNodes + 2) += middleG.block(nNodes - 1, 0, 1, nNodes + 2); + gStarDC.block(nNodes - 1, 2 * nNodes, 1, nNodes + 2) += rightG.block(0, 0, 1, nNodes + 2); + } + gStarDC *= 0.5; + + return gStarDC; + } + + Eigen::MatrixXd getBMatrix() { + + MatrixXd B = MatrixXd::Zero(nNodes, nNodes); + B(0, 0) = -1.0; + B(nNodes - 1, nNodes - 1) = 1.0; + + return B; + } + + /** + * @brief calculates the dispersion part of the DG jacobian + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd DGjacobianDispBlock(unsigned int cellIdx) { + + int offC = 0; // inlet DOFs not included in Jacobian + + MatrixXd dispBlock; + + if (exactInt) { + + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + + MatrixXd B = getBMatrix(); // "Lifting" matrix + MatrixXd gBlock = getGBlock(cellIdx); // current cell auxiliary block matrix + MatrixXd gStarDC = auxBlockGstar(cellIdx, getGBlock(cellIdx - 1), gBlock, getGBlock(cellIdx + 1)); // Numerical flux block + + // indices dispBlock : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),..., -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + dispBlock.block(0, nNodes, nNodes, nNodes + 2) += polyDerM * gBlock - invMM * B * gBlock; + dispBlock += invMM * B * gStarDC; + dispBlock *= 2 / deltaZ; + } + else { // inexact integration collocation DGSEM + + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes); + MatrixXd GBlockLeft = getGBlock(cellIdx - 1); + MatrixXd GBlock = getGBlock(cellIdx); + MatrixXd GBlockRight = getGBlock(cellIdx + 1); + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + // NOTE: N = polyDeg + // cell indices : 0 , ..., nNodes - 1; nNodes, ..., 2 * nNodes - 1; 2 * nNodes, ..., 3 * nNodes - 1 + // j : -N-1, ..., -1 ; 0 , ..., N ; N + 1, ..., 2N + 1 + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) = polyDerM * GBlock; + + if (cellIdx > 1) { + dispBlock(0, nNodes - 1) += -invWeights[0] * (-0.5 * GBlock(0, 0) + 0.5 * GBlockLeft(nNodes - 1, nNodes)); // G_N,N i=0, j=-1 + dispBlock(0, nNodes) += -invWeights[0] * (-0.5 * GBlock(0, 1) + 0.5 * GBlockLeft(nNodes - 1, nNodes + 1)); // G_N,N+1 i=0, j=0 + dispBlock.block(0, nNodes + 1, 1, nNodes) += -invWeights[0] * (-0.5 * GBlock.block(0, 2, 1, nNodes)); // G_i,j i=0, j=1,...,N+1 + dispBlock.block(0, 0, 1, nNodes - 1) += -invWeights[0] * (0.5 * GBlockLeft.block(nNodes - 1, 1, 1, nNodes - 1)); // G_N,j+N+1 i=0, j=-N-1,...,-2 + } + else if (cellIdx == 1) { // left boundary cell + dispBlock.block(0, nNodes - 1, 1, nNodes + 2) += -invWeights[0] * (-GBlock.block(0, 0, 1, nNodes + 2)); // G_N,N i=0, j=-1,...,N+1 + } + if (cellIdx < nCol) { + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock.block(nNodes - 1, 0, 1, nNodes)); // G_i,j+N+1 i=N, j=-1,...,N-1 + dispBlock(nNodes - 1, 2 * nNodes - 1) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes) + 0.5 * GBlockRight(0, 0)); // G_i,j i=N, j=N + dispBlock(nNodes - 1, 2 * nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes + 1) + 0.5 * GBlockRight(0, 1)); // G_i,j i=N, j=N+1 + dispBlock.block(nNodes - 1, 2 * nNodes + 1, 1, nNodes - 1) += invWeights[nNodes - 1] * (0.5 * GBlockRight.block(0, 2, 1, nNodes - 1)); // G_0,j-N-1 i=N, j=N+2,...,2N+1 + } + else if (cellIdx == nCol) { // right boundary cell + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes + 2) += invWeights[nNodes - 1] * (-GBlock.block(nNodes - 1, 0, 1, nNodes + 2)); // G_i,j+N+1 i=N, j=--1,...,N+1 + } + + dispBlock *= 2 / deltaZ; + } + + return -dispBlock; // *-1 for residual + } + }; + + Discretization _disc; //!< Discretization info +// IExternalFunction* _extFun; //!< External function (owned by library user) + + parts::ConvectionDispersionOperatorBase _convDispOp; //!< Convection dispersion operator for interstitial volume transport + IDynamicReactionModel* _dynReactionBulk; //!< Dynamic reactions in the bulk volume + + Eigen::SparseLU> _globalSolver; //!< linear solver for the bulk concentration + //Eigen::BiCGSTAB, Eigen::DiagonalPreconditioner> _globalSolver; + + Eigen::MatrixXd _jacInlet; //!< Jacobian inlet DOF block matrix connects inlet DOFs to first bulk cells + + // for FV the bulk jacobians are defined in the ConvDisp operator. + Eigen::SparseMatrix _globalJac; //!< global Jacobian + Eigen::SparseMatrix _globalJacDisc; //!< global Jacobian with time derivatove from BDF method + //Eigen::MatrixXd _FDjac; //!< test purpose FD jacobian + + active _colPorosity; //!< Column porosity (external porosity) \f$ \varepsilon_c \f$ + std::vector _parGeomSurfToVol; //!< Particle surface to volume ratio factor (i.e., 3.0 for spherical, 2.0 for cylindrical, 1.0 for hexahedral) + std::vector _parRadius; //!< Particle radius \f$ r_p \f$ + bool _singleParRadius; + std::vector _parPorosity; //!< Particle porosity (internal porosity) \f$ \varepsilon_p \f$ + bool _singleParPorosity; + std::vector _parTypeVolFrac; //!< Volume fraction of each particle type + + // Vectorial parameters + std::vector _filmDiffusion; //!< Film diffusion coefficient \f$ k_f \f$ + MultiplexMode _filmDiffusionMode; + std::vector _poreAccessFactor; //!< Pore accessibility factor \f$ F_{\text{acc}} \f$ + MultiplexMode _poreAccessFactorMode; + + bool _axiallyConstantParTypeVolFrac; //!< Determines whether particle type volume fraction is homogeneous across axial coordinate + bool _analyticJac; //!< Determines whether AD or analytic Jacobians are used + unsigned int _jacobianAdDirs; //!< Number of AD seed vectors required for Jacobian computation + + bool _factorizeJacobian; //!< Determines whether the Jacobian needs to be factorized + double* _tempState; //!< Temporary storage with the size of the state vector or larger if binding models require it + + std::vector _initC; //!< Liquid bulk phase initial conditions + std::vector _initCp; //!< Liquid particle phase initial conditions + std::vector _initQ; //!< Solid phase initial conditions + std::vector _initState; //!< Initial conditions for state vector if given + std::vector _initStateDot; //!< Initial conditions for time derivative + + BENCH_TIMER(_timerResidual) + BENCH_TIMER(_timerResidualPar) + BENCH_TIMER(_timerResidualSens) + BENCH_TIMER(_timerResidualSensPar) + BENCH_TIMER(_timerJacobianPar) + BENCH_TIMER(_timerConsistentInit) + BENCH_TIMER(_timerConsistentInitPar) + BENCH_TIMER(_timerLinearSolve) + BENCH_TIMER(_timerFactorize) + BENCH_TIMER(_timerFactorizePar) + BENCH_TIMER(_timerMatVec) + + class Indexer + { + public: + Indexer(const Discretization& disc) : _disc(disc) { } + + // Strides + inline int strideColNode() const CADET_NOEXCEPT { return static_cast(_disc.nComp); } + inline int strideColCell() const CADET_NOEXCEPT { return static_cast(_disc.nNodes * strideColNode()); } + inline int strideColComp() const CADET_NOEXCEPT { return 1; } + + inline int strideParComp() const CADET_NOEXCEPT { return 1; } + inline int strideParLiquid() const CADET_NOEXCEPT { return static_cast(_disc.nComp); } + inline int strideParBound(int parType) const CADET_NOEXCEPT { return static_cast(_disc.strideBound[parType]); } + inline int strideParBlock(int parType) const CADET_NOEXCEPT { return strideParLiquid() + strideParBound(parType); } + + // Offsets + inline int offsetC() const CADET_NOEXCEPT { return _disc.nComp; } + inline int offsetCp() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints + offsetC(); } + inline int offsetCp(ParticleTypeIndex pti) const CADET_NOEXCEPT { return offsetCp() + _disc.parTypeOffset[pti.value]; } + inline int offsetCp(ParticleTypeIndex pti, ParticleIndex pi) const CADET_NOEXCEPT { return offsetCp(pti) + strideParBlock(pti.value) * pi.value; } + inline int offsetBoundComp(ParticleTypeIndex pti, ComponentIndex comp) const CADET_NOEXCEPT { return _disc.boundOffset[pti.value * _disc.nComp + comp.value]; } + + // Return pointer to first element of state variable in state vector + template inline real_t* c(real_t* const data) const { return data + offsetC(); } + template inline real_t const* c(real_t const* const data) const { return data + offsetC(); } + + template inline real_t* cp(real_t* const data) const { return data + offsetCp(); } + template inline real_t const* cp(real_t const* const data) const { return data + offsetCp(); } + + template inline real_t* q(real_t* const data) const { return data + offsetCp() + strideParLiquid(); } + template inline real_t const* q(real_t const* const data) const { return data + offsetCp() + strideParLiquid(); } + + // Return specific variable in state vector + template inline real_t& c(real_t* const data, unsigned int point, unsigned int comp) const { return data[offsetC() + comp + point * strideColNode()]; } + template inline const real_t& c(real_t const* const data, unsigned int point, unsigned int comp) const { return data[offsetC() + comp + point * strideColNode()]; } + + protected: + const Discretization& _disc; + }; + + class Exporter : public ISolutionExporter + { + public: + + Exporter(const Discretization& disc, const LumpedRateModelWithPoresDG& model, double const* data) : _disc(disc), _idx(disc), _model(model), _data(data) { } + Exporter(const Discretization&& disc, const LumpedRateModelWithPoresDG& model, double const* data) = delete; + + virtual bool hasParticleFlux() const CADET_NOEXCEPT { return false; } + virtual bool hasParticleMobilePhase() const CADET_NOEXCEPT { return true; } + virtual bool hasSolidPhase() const CADET_NOEXCEPT { return _disc.strideBound[_disc.nParType] > 0; } + virtual bool hasVolume() const CADET_NOEXCEPT { return false; } + virtual bool isParticleLumped() const CADET_NOEXCEPT { return true; } + virtual bool hasPrimaryExtent() const CADET_NOEXCEPT { return true; } + + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual unsigned int numPrimaryCoordinates() const CADET_NOEXCEPT { return _disc.nPoints; } + virtual unsigned int numSecondaryCoordinates() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numParticleTypes() const CADET_NOEXCEPT { return _disc.nParType; } + virtual unsigned int numParticleShells(unsigned int parType) const CADET_NOEXCEPT { return 1; } + virtual unsigned int numBoundStates(unsigned int parType) const CADET_NOEXCEPT { return _disc.strideBound[parType]; } + virtual unsigned int numMobilePhaseDofs() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints; } + virtual unsigned int numParticleMobilePhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints; } + virtual unsigned int numParticleMobilePhaseDofs() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints * _disc.nParType; } + virtual unsigned int numSolidPhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return _disc.strideBound[parType] * _disc.nPoints; } + virtual unsigned int numSolidPhaseDofs() const CADET_NOEXCEPT { + unsigned int nDofPerParType = 0; + for (unsigned int i = 0; i < _disc.nParType; ++i) + nDofPerParType += _disc.strideBound[i]; + return _disc.nPoints * nDofPerParType; + } + virtual unsigned int numParticleFluxDofs() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numVolumeDofs() const CADET_NOEXCEPT { return 0; } + + virtual int writeMobilePhase(double* buffer) const; + virtual int writeSolidPhase(double* buffer) const; + virtual int writeParticleMobilePhase(double* buffer) const; + virtual int writeSolidPhase(unsigned int parType, double* buffer) const; + virtual int writeParticleMobilePhase(unsigned int parType, double* buffer) const; + virtual int writeParticleFlux(double* buffer) const { return 0; } + virtual int writeParticleFlux(unsigned int parType, double* buffer) const { return 0; } + virtual int writeVolume(double* buffer) const { return 0; } + virtual int writeInlet(unsigned int port, double* buffer) const; + virtual int writeInlet(double* buffer) const; + virtual int writeOutlet(unsigned int port, double* buffer) const; + virtual int writeOutlet(double* buffer) const; + /** + * @brief calculates and writes the physical node coordinates of the DG discretization with double! interface nodes + */ + virtual int writePrimaryCoordinates(double* coords) const { + Eigen::VectorXd x_l = Eigen::VectorXd::LinSpaced(static_cast(_disc.nCol + 1), 0.0, _disc.length_); + for (unsigned int i = 0; i < _disc.nCol; i++) { + for (unsigned int j = 0; j < _disc.nNodes; j++) { + // mapping + coords[i * _disc.nNodes + j] = x_l[i] + 0.5 * (_disc.length_ / static_cast(_disc.nCol)) * (1.0 + _disc.nodes[j]); + } + } + return _disc.nPoints; + } + virtual int writeSecondaryCoordinates(double* coords) const { return 0; } + virtual int writeParticleCoordinates(unsigned int parType, double* coords) const + { + coords[0] = static_cast(_model._parRadius[parType]) * 0.5; + return 1; + } + + protected: + const Discretization& _disc; + const Indexer _idx; + const LumpedRateModelWithPoresDG& _model; + double const* const _data; + }; + + /** + * @brief sets the current section index and section dependend velocity, dispersion + */ + void updateSection(int secIdx) { + + if (cadet_unlikely(_disc.curSection != secIdx)) { + + _disc.curSection = secIdx; + _disc.newStaticJac = true; + + // update velocity and dispersion + _disc.velocity = static_cast(_convDispOp.currentVelocity()); + if (_convDispOp.dispersionCompIndep()) + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOp.currentDispersion(secIdx)[0]); + } + else { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOp.currentDispersion(secIdx)[comp]); + } + } + + } + } + + // ========================================================================================================================================================== // + // ======================================== DG RHS ====================================================== // + // ========================================================================================================================================================== // + + /** + * @brief calculates the volume Integral of the auxiliary equation + * @param [in] current state vector + * @param [in] stateDer vector to be changed + * @param [in] aux true if auxiliary, else main equation + */ + void volumeIntegral(Eigen::Map>& state, Eigen::Map>& stateDer) { + // comp-cell-node state vector: use of Eigen lib performance + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + stateDer.segment(Cell * _disc.nNodes, _disc.nNodes) + -= _disc.polyDerM * state.segment(Cell * _disc.nNodes, _disc.nNodes); + } + } + /* + * @brief calculates the interface fluxes h* of Convection Dispersion equation + */ + void InterfaceFlux(Eigen::Map>& C, const VectorXd& g, unsigned int comp) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // Conv.Disp. flux: h* = h*_conv + h*_disp = numFlux(v c_l, v c_r) + 0.5 sqrt(D_ax) (S_l + S_r) + + if (_disc.velocity >= 0.0) { // forward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell - strideNode]) // left cell (i.e. forward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet (left) boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * _disc.boundary[0]; + + // outlet (right) boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * (C[_disc.nCol * strideCell - strideNode]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[_disc.nCol * strideCell - strideNode] // last cell last node + + _disc.boundary[3]); // right boundary value S + } + else { // backward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell]) // right cell (i.e. backward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * _disc.boundary[0]; + + // outlet boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * (C[0]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[0] // first cell first node + + _disc.boundary[2]); // left boundary value g + } + } + /** + * @brief calculates and fills the surface flux values for auxiliary equation + */ + void InterfaceFluxAuxiliary(Eigen::Map>& C) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // Auxiliary flux: c* = 0.5 (c_l + c_r) + + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + _disc.surfaceFlux[Cell] // left interfaces + = 0.5 * (C[Cell * strideCell - strideNode] + // left node + C[Cell * strideCell]); // right node + } + // calculate boundary interface fluxes + + _disc.surfaceFlux[0] // left boundary interface + = 0.5 * (C[0] + // boundary value + C[0]); // first cell first node + + _disc.surfaceFlux[(_disc.nCol)] // right boundary interface + = 0.5 * (C[_disc.nCol * strideCell - strideNode] + // last cell last node + C[_disc.nCol * strideCell - strideNode]);// // boundary value + } + + /** + * @brief calculates the surface Integral, depending on the approach (exact/inexact integration) + * @param [in] state relevant state vector + * @param [in] stateDer state derivative vector the solution is added to + * @param [in] aux true for auxiliary equation, false for main equation + surfaceIntegral(cPtr, &(disc.g[0]), disc,&(disc.h[0]), resPtrC, 0, secIdx); + */ + void surfaceIntegral(Eigen::Map>& C, Eigen::Map>& state, Eigen::Map>& stateDer, bool aux, unsigned int Comp) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // calc numerical flux values c* or h* depending on equation switch aux + (aux == 1) ? InterfaceFluxAuxiliary(C) : InterfaceFlux(C, _disc.g, Comp); + if (_disc.exactInt) { // modal approach -> dense mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + for (unsigned int Node = 0; Node < _disc.nNodes; Node++) { + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.invMM(Node, 0) * (state[Cell * strideCell] + - _disc.surfaceFlux[Cell]) + - _disc.invMM(Node, _disc.polyDeg) * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux[(Cell + 1u)]); + } + } + } + else { // nodal approach -> diagonal mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + stateDer[Cell * strideCell] // first cell node + -= _disc.invWeights[0] * (state[Cell * strideCell] // first node + - _disc.surfaceFlux(Cell)); + stateDer[Cell * strideCell + _disc.polyDeg * strideNode] // last cell node + += _disc.invWeights[_disc.polyDeg] * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux(Cell + 1u)); + } + } + } + + /** + * @brief calculates the substitute h = vc - sqrt(D_ax) g(c) + */ + void calcH(Eigen::Map>& C, unsigned int Comp) { + _disc.h = _disc.velocity * C - std::sqrt(_disc.dispersion[Comp]) * _disc.g; + } + + /** + * @brief applies the inverse Jacobian of the mapping + */ + void applyMapping(Eigen::Map>& state) { + state *= (2.0 / _disc.deltaZ); + } + /** + * @brief applies the inverse Jacobian of the mapping and auxiliary factor -1 + */ + void applyMapping_Aux(Eigen::Map>& state, unsigned int Comp) { + state *= (-2.0 / _disc.deltaZ) * ((_disc.dispersion[Comp] == 0.0) ? 1.0 : std::sqrt(_disc.dispersion[Comp])); + } + + void ConvDisp_DG(Eigen::Map>& C, Eigen::Map>& resC, double t, unsigned int Comp) { + + // ===================================// + // reset cache // + // ===================================// + + resC.setZero(); + _disc.h.setZero(); + _disc.g.setZero(); + _disc.surfaceFlux.setZero(); + // get Map objects of auxiliary variable memory + Eigen::Map> g(&_disc.g[0], _disc.nPoints, InnerStride<>(1)); + Eigen::Map> h(&_disc.h[0], _disc.nPoints, InnerStride<>(1)); + + // ======================================// + // solve auxiliary system g = d c / d x // + // ======================================// + + volumeIntegral(C, g); // DG volumne integral in strong form + + surfaceIntegral(C, C, g, 1, Comp); // surface integral in strong form + + applyMapping_Aux(g, Comp); // inverse mapping from reference space and auxiliary factor + + _disc.surfaceFlux.setZero(); // reset surface flux storage as it is used twice + + // ======================================// + // solve main equation w_t = d h / d x // + // ======================================// + + calcH(C, Comp); // calculate the substitute h(S(c), c) = sqrt(D_ax) g(c) - v c + + volumeIntegral(h, resC); // DG volumne integral in strong form + + calcBoundaryValues(C);// update boundary values including auxiliary variable g + + surfaceIntegral(C, h, resC, 0, Comp); // DG surface integral in strong form + + applyMapping(resC); // inverse mapping to reference space + + } + /** + * @brief computes ghost nodes used to implement Danckwerts boundary conditions + */ + void calcBoundaryValues(Eigen::Map>& C) { + + //cache.boundary[0] = c_in -> inlet DOF idas suggestion + //_disc.boundary[1] = (_disc.velocity >= 0.0) ? C[_disc.nPoints - 1] : C[0]; // c_r outlet not required + _disc.boundary[2] = -_disc.g[0]; // g_l left boundary (inlet/outlet for forward/backward flow) + _disc.boundary[3] = -_disc.g[_disc.nPoints - 1]; // g_r right boundary (outlet/inlet for forward/backward flow) + } + + // ========================================================================================================================================================== // + // ======================================== DG Jacobian ========================================================= // + // ========================================================================================================================================================== // + + typedef Eigen::Triplet T; + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian + */ + void setGlobalJacPattern(Eigen::SparseMatrix& mat, const bool hasBulkReaction) { + + std::vector tripletList; + + tripletList.reserve(nJacEntries(hasBulkReaction)); + + if (_disc.exactInt) + ConvDispModalPattern(tripletList); + else + ConvDispNodalPattern(tripletList); + + if (hasBulkReaction) + bulkReactionPattern(tripletList); + + particlePattern(tripletList); + + fluxPattern(tripletList); + + mat.setFromTriplets(tripletList.begin(), tripletList.end()); + + } + /** + * @brief sets the sparsity pattern of the bulkreaction Jacobian pattern + */ + int bulkReactionPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + for (unsigned int blk = 0; blk < _disc.nPoints; blk++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int toComp = 0; toComp < _disc.nComp; toComp++) { + tripletList.push_back(T(idxr.offsetC() + blk * idxr.strideColNode() + comp * idxr.strideColComp(), + idxr.offsetC() + blk * idxr.strideColNode() + toComp * idxr.strideColComp(), + 0.0)); + } + } + } + return 1; + } + /** + * @brief sets the sparsity pattern of the particle Jacobian pattern (isotherm, reaction pattern) + */ + int particlePattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + for (unsigned int parType = 0; parType < _disc.nParType; parType++) { + for (unsigned int nCol = 0; nCol < _disc.nPoints; nCol++) { + + int offset = idxr.offsetCp(ParticleTypeIndex{ parType }, ParticleIndex{ nCol }) - idxr.offsetC(); // inlet DOFs not included in Jacobian + + // add dense nComp * nBound blocks, since all solid and liquid entries can be coupled through binding. + for (unsigned int parState = 0; parState < _disc.nComp + _disc.strideBound[parType]; parState++) { + for (unsigned int toParState = 0; toParState < _disc.nComp + _disc.strideBound[parType]; toParState++) { + tripletList.push_back(T(offset + parState, offset + toParState, 0.0)); + } + } + } + } + return 1; + } + + /** + * @brief sets the sparsity pattern of the flux Jacobian pattern + */ + int fluxPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + for (unsigned int parType = 0; parType < _disc.nParType; parType++) { + + int offC = 0; // inlet DOFs not included in Jacobian + int offP = idxr.offsetCp(ParticleTypeIndex{ parType }) - idxr.offsetC(); // inlet DOFs not included in Jacobian + + // add dependency of c^b, c^p and flux on another + for (unsigned int nCol = 0; nCol < _disc.nPoints; nCol++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + // c^b on c^b entry already set + tripletList.push_back(T(offC + nCol * _disc.nComp + comp, offP + nCol * idxr.strideParBlock(parType) + comp, 0.0)); // c^b on c^p + // c^p on c^p entry already set + tripletList.push_back(T(offP + nCol * idxr.strideParBlock(parType) + comp, offC + nCol * _disc.nComp + comp, 0.0)); // c^p on c^b + } + } + } + return 1; + } + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the nodal DG scheme + */ + int ConvDispNodalPattern(std::vector& tripletList) { + + Indexer idx(_disc); + + int sNode = idx.strideColNode(); + int sCell = idx.strideColCell(); + int sComp = idx.strideColComp(); + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int polyDeg = _disc.polyDeg; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Define Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + // special inlet DOF treatment for inlet boundary cell (first cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + /*======================================================*/ + /* Define Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + + // insert Blocks to Jacobian inner cells (only for nCells >= 3) + if (nCells >= 3u) { + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + (cell - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + if (nCells != 1) { // Note: special case nCells = 1 already set by advection block + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - nNodes) * sNode, + 0.0)); + } + } + } + + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1 - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 0; + } + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the exact integration (here: modal) DG scheme + */ + int ConvDispModalPattern(std::vector& tripletList) { + + Indexer idx(_disc); + + int sNode = idx.strideColNode(); + int sCell = idx.strideColCell(); + int sComp = idx.strideColComp(); + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Define Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + /*======================================================*/ + /* Define Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells */ + if (nCells >= 5u) { + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + for (unsigned int cell = 2; cell < nCells - 2; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* boundary cell neighbours */ + + // left boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + else if (nCells == 3u) { // special case: only depends on the two neighbouring cells + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + // right boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2 - 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 2) * sCell + comp * sComp + i * sNode, + offC + (nCells - 2) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + /* boundary cells */ + + // left boundary cell + unsigned int end = 3u * nNodes + 2u; + if (nCells == 1u) end = 2u * nNodes + 1u; + else if (nCells == 2u) end = 3u * nNodes + 1u; + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes + 1; j < end; j++) { + // row: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset, adjust for iterator j (-Nnodes-1) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - (nNodes + 1)) * sNode, + 0.0)); + } + } + } + // right boundary cell + if (nCells >= 3u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + else if (nCells == 2u) { // special case for nCells == 2: depends only on left cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes)*sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 0; + } + + /** + * @brief analytically calculates the static (per section) bulk jacobian (inlet DOFs included!) + * @return 1 if jacobain estimation fits the predefined pattern of the jacobian, 0 if not. + */ + int calcStaticAnaGlobalJacobian(unsigned int secIdx) { + + calcStaticAnaBulkJacobian(secIdx); + + calcFluxJacobians(secIdx); + + return 1; + } + /** + * @brief analytically calculates the static (per section) bulk jacobian (inlet DOFs included!) + * @return 1 if jacobain estimation fits the predefined pattern of the jacobian, 0 if not. + */ + int calcStaticAnaBulkJacobian(unsigned int secIdx) { + + // DG convection dispersion Jacobian + if (_disc.exactInt) + calcConvDispDGSEMJacobian(); + else + calcConvDispCollocationDGSEMJacobian(); + + if (!_globalJac.isCompressed()) // if matrix lost its compressed storage, the pattern did not fit. + return 0; + + return 1; + } + + int calcFluxJacobians(unsigned int secIdx) { + + Indexer idxr(_disc); + + const double invBetaC = 1.0 / static_cast(_colPorosity) - 1.0; + + for (unsigned int type = 0; type < _disc.nParType; type++) { + + const double epsP = static_cast(_parPorosity[type]); + const double radius = static_cast(_parRadius[type]); + const double jacCF_val = invBetaC * _parGeomSurfToVol[type] / radius; + const double jacPF_val = -_parGeomSurfToVol[type] / (radius * epsP); + + // Ordering of diffusion: + // sec0type0comp0, sec0type0comp1, sec0type0comp2, sec0type1comp0, sec0type1comp1, sec0type1comp2, + // sec1type0comp0, sec1type0comp1, sec1type0comp2, sec1type1comp0, sec1type1comp1, sec1type1comp2, ... + active const* const filmDiff = getSectionDependentSlice(_filmDiffusion, _disc.nComp * _disc.nParType, secIdx) + type * _disc.nComp; + active const* const poreAccFactor = _poreAccessFactor.data() + type * _disc.nComp; + + linalg::BandedEigenSparseRowIterator jacC(_globalJac, 0); + linalg::BandedEigenSparseRowIterator jacP(_globalJac, idxr.offsetCp(ParticleTypeIndex{ type }) - idxr.offsetC()); + + for (unsigned int colNode = 0; colNode < _disc.nPoints; colNode++, jacP += _disc.strideBound[type]) + { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jacC, ++jacP) { + + // add Cl on Cl entries (added since already set in bulk jacobian) + // row: already at bulk phase. already at current node and component. + // col: already at bulk phase. already at current node and component. + jacC[0] += jacCF_val * static_cast(filmDiff[comp]) * static_cast(_parTypeVolFrac[type + _disc.nParType * colNode]); + // add Cl on Cp entries + // row: already at bulk phase. already at current node and component. + // col: already at bulk phase. already at current node and component. + jacC[jacP.row() - jacC.row()] = -jacCF_val * static_cast(filmDiff[comp]) * static_cast(_parTypeVolFrac[type + _disc.nParType * colNode]); + + // add Cp on Cp entries + // row: already at particle. already at current node and liquid state. + // col: go to flux of current parType and adjust for offsetC. jump over previous colNodes and add component offset + jacP[0] + = -jacPF_val / static_cast(poreAccFactor[comp]) * static_cast(filmDiff[comp]); + // add Cp on Cl entries + // row: already at particle. already at current node and liquid state. + // col: go to flux of current parType and adjust for offsetC. jump over previous colNodes and add component offset + jacP[jacC.row() - jacP.row()] + = jacPF_val / static_cast(poreAccFactor[comp]) * static_cast(filmDiff[comp]); + } + } + } + return 1; + } + + /** + * @brief calculates the number of entris for the DG convection dispersion jacobian + * @note only dispersion entries are relevant for jacobian NNZ as the convection entries are a subset of these + */ + unsigned int nJacEntries(const bool hasBulkReaction, const bool pureNNZ = false) { + + unsigned int nEntries = 0; + // Convection dispersion + if (_disc.exactInt) { + if (pureNNZ) + nEntries = _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + else + nEntries = _disc.nComp * _disc.nNodes * _disc.nNodes + _disc.nNodes // convection entries + + _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + } + else { + if (pureNNZ) + nEntries = _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + else + nEntries = _disc.nComp * _disc.nNodes * _disc.nNodes + 1u // convection entries + + _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + } + + // Bulk reaction entries + if (hasBulkReaction) + nEntries += _disc.nPoints * _disc.nComp * _disc.nComp; // add nComp entries for every component at each discrete bulk point + + // Particle binding and reaction entries + for (unsigned int type = 0; type < _disc.nParType; type++) + nEntries += _disc.nComp + _disc.nBoundBeforeType[type]; + + return nEntries; + } + /** + * @brief analytically calculates the convection dispersion jacobian for the nodal DG scheme + */ + int calcConvDispCollocationDGSEMJacobian() { + + Indexer idxr(_disc); + + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + if (nCells >= 3u) { + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[1]; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at second cell and component + + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + /* left cell */ + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[0]; + + if (nCells != 1u) { // "standard" case + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + else { // special case + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < nNodes * 2u; j++) { + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /* right cell */ + if (nCells != 1u) { // "standard" case + dispBlock = _disc.DGjacAxDispBlocks[std::min(nCells, 3u) - 1]; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + (nCells - 1) * idxr.strideColCell()); // row iterator starting at last cell + + for (unsigned int i = 0; i < dispBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on first entry of previous cell + MatrixXd convBlock = _disc.DGjacAxConvBlock; + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell and component + + if (_disc.velocity >= 0.0) { // forward flow upwind convection + // special inlet DOF treatment for first cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(0, 0); // only first node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + //jacIt[0] = -convBlock(i, 0); // dependency on inlet DOFs is handled in _jacInlet + for (unsigned int j = 1; j < convBlock.cols(); j++) { + jacIt[((j - 1) - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + // remaining cells + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at previous cells last node and go to node j. + jacIt[-idxr.strideColNode() + (j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + } + else { // backward flow upwind convection + // non-inlet cells + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at current cells first node and go to node j. + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + // special inlet DOF treatment for last cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(convBlock.rows() - 1, convBlock.cols() - 1); // only last node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols() - 1; j++) { + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + return 0; + } + /** + * @brief inserts a liquid state block with different factors for components into the system jacobian + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] offCol column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + * @param [in] Compfactor component dependend factors + */ + void insertCompDepLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offCol, Indexer& idxr, unsigned int nCells, double* Compfactor) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * idxr.strideColNode() + offCol] = block(i, j) * Compfactor[comp]; + } + } + } + } + } + /** + * @brief adds liquid state blocks for all components to the system jacobian + * @param [in] block to be added + * @param [in] jac row iterator at first (i.e. upper left) entry + * @param [in] column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + */ + void addLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offCol, Indexer& idxr, unsigned int nCells) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * idxr.strideColNode() + offCol] += block(i, j); + } + } + } + } + } + /** + * @brief analytically calculates the convection dispersion jacobian for the exact integration (here: modal) DG scheme + */ + int calcConvDispDGSEMJacobian() { + + Indexer idxr(_disc); + + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells (exist only if nCells >= 5) */ + if (nCells >= 5) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell() * 2); // row iterator starting at third cell, first component + // insert all (nCol - 4) inner cell blocks + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[2], jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, _disc.nCol - 4u, &(_disc.dispersion[0])); + } + + /* boundary cell neighbours (exist only if nCells >= 4) */ + if (nCells >= 4) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at second cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes + 1), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + + jacIt += (_disc.nCol - 4) * idxr.strideColCell(); // move iterator to preultimate cell (already at third cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[nCells > 4 ? 3 : 2].block(0, 0, nNodes, 3 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0])); + } + + /* boundary cells (exist only if nCells >= 3) */ + if (nCells >= 3) { + + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes + 1), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + + jacIt += (_disc.nCol - 2) * idxr.strideColCell(); // move iterator to last cell (already at second cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[std::min(nCells, 5u) - 1u].block(0, 0, nNodes, 2 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0])); + } + + /* For special cases nCells = 1, 2, 3, some cells still have to be treated separately*/ + + if (nCells == 1) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + // insert the only block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + } + else if (nCells == 2) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC); // row iterator starting at first cell, first component + // left Bacobian block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + // right Bacobian block, iterator is already moved to second cell + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 2 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + } + else if (nCells == 3) { + linalg::BandedEigenSparseRowIterator jacIt(_globalJac, offC + idxr.strideColCell()); // row iterator starting at first cell, first component + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + int sComp = idxr.strideColComp(); + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + + linalg::BandedEigenSparseRowIterator jac(_globalJac, offC); + + if (_disc.velocity >= 0.0) { // Forward flow + // special inlet DOF treatment for inlet (first) cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(0); // only first cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 1, nNodes, nNodes), jac, 0, idxr, 1); + if (_disc.nCol > 1) // iterator already moved to second cell + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, -idxr.strideColNode(), idxr, _disc.nCol - 1); + } + else { // Backward flow + // non-inlet cells first + if (_disc.nCol > 1) + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, 0, idxr, _disc.nCol - 1); + // special inlet DOF treatment for inlet (last) cell. Iterator already moved to last cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(_disc.DGjacAxConvBlock.cols() - 1); // only last cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 0, nNodes, nNodes), jac, 0, idxr, 1); + } + + return 0; + } + /** + * @brief adds time derivative to the bulk jacobian + * @detail alpha * d Bulk_Residual / d c_t = alpha * I is added to the bulk jacobian + */ + void addTimeDerBulkJacobian(double alpha, Indexer idxr) { + + unsigned int offC = 0; // inlet DOFs not included in Jacobian + + for (linalg::BandedEigenSparseRowIterator jac(_globalJacDisc, offC); jac.row() < _disc.nComp * _disc.nPoints; ++jac) { + + jac[0] += alpha; // main diagonal + + } + } + + // testing purpose + MatrixXd calcFDJacobian(const SimulationTime simTime, util::ThreadLocalStorage& threadLocalMem, double alpha) { + + // create solution vectors + VectorXd y = VectorXd::Zero(numDofs()); + VectorXd yDot = VectorXd::Zero(numDofs()); + VectorXd res = VectorXd::Zero(numDofs()); + const double* yPtr = &y[0]; + const double* yDotPtr = &yDot[0]; + double* resPtr = &res[0]; + // create FD jacobian + MatrixXd Jacobian = MatrixXd::Zero(numDofs(), numDofs()); + // set FD step + double epsilon = 0.01; + + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + + for (int col = 0; col < Jacobian.cols(); col++) { + Jacobian.col(col) = -(1.0 + alpha) * res; + } + /* Residual(y+h) */ + // state DOFs + for (int dof = 0; dof < Jacobian.cols(); dof++) { + y[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + y[dof] -= epsilon; + Jacobian.col(dof) += res; + } + + // state derivative Jacobian + for (int dof = 0; dof < Jacobian.cols(); dof++) { + yDot[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + yDot[dof] -= epsilon; + Jacobian.col(dof) += alpha * res; + } + + ///* exterminate numerical noise and divide by epsilon*/ + //for (int i = 0; i < Jacobian.rows(); i++) { + // for (int j = 0; j < Jacobian.cols(); j++) { + // if (std::abs(Jacobian(i, j)) < 1e-10) Jacobian(i, j) = 0.0; + // } + //} + Jacobian /= epsilon; + + return Jacobian; + } + +}; + +} // namespace model +} // namespace cadet + +#endif // LIBCADET_LUMPEDRATEMODELWITHPORESDG_HPP_ \ No newline at end of file diff --git a/src/libcadet/model/LumpedRateModelWithoutPores.hpp b/src/libcadet/model/LumpedRateModelWithoutPores.hpp index f782f9db5..73761e7cf 100644 --- a/src/libcadet/model/LumpedRateModelWithoutPores.hpp +++ b/src/libcadet/model/LumpedRateModelWithoutPores.hpp @@ -218,8 +218,6 @@ class LumpedRateModelWithoutPores : public UnitOperationBase bool _factorizeJacobian; //!< Determines whether the Jacobian needs to be factorized double* _tempState; //!< Temporary storage with the size of the state vector or larger if binding models require it - linalg::Gmres _gmres; //!< GMRES algorithm for the Schur-complement in linearSolve() - double _schurSafety; //!< Safety factor for Schur-complement solution std::vector _initC; //!< Liquid phase initial conditions std::vector _initQ; //!< Solid phase initial conditions diff --git a/src/libcadet/model/LumpedRateModelWithoutPoresDG.cpp b/src/libcadet/model/LumpedRateModelWithoutPoresDG.cpp new file mode 100644 index 000000000..79cbe8166 --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithoutPoresDG.cpp @@ -0,0 +1,1862 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include "model/LumpedRateModelWithoutPoresDG.hpp" +#include "BindingModelFactory.hpp" +#include "ParamReaderHelper.hpp" +#include "cadet/Exceptions.hpp" +#include "cadet/ExternalFunction.hpp" +#include "cadet/SolutionRecorder.hpp" +#include "ConfigurationHelper.hpp" +#include "model/BindingModel.hpp" +#include "SimulationTypes.hpp" +#include "linalg/DenseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/Norms.hpp" +#include "linalg/Subset.hpp" +#include "model/parts/BindingCellKernel.hpp" + +#include "AdUtils.hpp" + +#include "LoggingUtils.hpp" +#include "Logging.hpp" + +#include +#include + +#include "ParallelSupport.hpp" +#ifdef CADET_PARALLELIZE +#include +#endif + +#define EIGEN_USE_MKL_ALL + +using namespace Eigen; + +namespace cadet +{ + + namespace model + { + + LumpedRateModelWithoutPoresDG::LumpedRateModelWithoutPoresDG(UnitOpIdx unitOpIdx) : UnitOperationBase(unitOpIdx), + /*_jacInlet(),*/ _analyticJac(true), _jacobianAdDirs(0), _factorizeJacobian(false), _tempState(nullptr), _initC(0), + _initQ(0), _initState(0), _initStateDot(0) + { + // Multiple particle types are not supported + _singleBinding = true; + _singleDynReaction = true; + } + + LumpedRateModelWithoutPoresDG::~LumpedRateModelWithoutPoresDG() CADET_NOEXCEPT + { + delete[] _tempState; + + delete[] _disc.nBound; + delete[] _disc.boundOffset; + } + + unsigned int LumpedRateModelWithoutPoresDG::numDofs() const CADET_NOEXCEPT + { + // Column bulk DOFs: nCol * nNodes * nComp mobile phase and nCol * nNodes * (sum boundStates) solid phase + // Inlet DOFs: nComp + return _disc.nPoints * (_disc.nComp + _disc.strideBound) + _disc.nComp; + } + + unsigned int LumpedRateModelWithoutPoresDG::numPureDofs() const CADET_NOEXCEPT + { + // Column bulk DOFs: nCol * nNodes * nComp mobile phase and nCol * nNodes * (sum boundStates) solid phase + return _disc.nPoints * (_disc.nComp + _disc.strideBound); + } + + + bool LumpedRateModelWithoutPoresDG::usesAD() const CADET_NOEXCEPT + { +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + // We always need AD if we want to check the analytical Jacobian + return true; +#else + // We only need AD if we are not computing the Jacobian analytically + return !_analyticJac; +#endif + } + + bool LumpedRateModelWithoutPoresDG::configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper) + { + // Read discretization + _disc.nComp = paramProvider.getInt("NCOMP"); + + paramProvider.pushScope("discretization"); + + if (paramProvider.getInt("NCOL") < 1) + throw InvalidParameterException("Number of column cells must be at least 1!"); + _disc.nCol = paramProvider.getInt("NCOL"); + + if (paramProvider.getInt("POLYDEG") < 1) + throw InvalidParameterException("Polynomial degree must be at least 1!"); + _disc.polyDeg = paramProvider.getInt("POLYDEG"); + + _disc.exactInt = paramProvider.getBool("EXACT_INTEGRATION"); + + const std::vector nBound = paramProvider.getIntArray("NBOUND"); + if (nBound.size() < _disc.nComp) + throw InvalidParameterException("Field NBOUND contains too few elements (NCOMP = " + std::to_string(_disc.nComp) + " required)"); + + _disc.nBound = new unsigned int[_disc.nComp]; + std::copy_n(nBound.begin(), _disc.nComp, _disc.nBound); + + // Compute discretization + _disc.initializeDG(); + + // Precompute offsets and total number of bound states (DOFs in solid phase) + _disc.boundOffset = new unsigned int[_disc.nComp]; + _disc.boundOffset[0] = 0; + for (unsigned int i = 1; i < _disc.nComp; ++i) + { + _disc.boundOffset[i] = _disc.boundOffset[i - 1] + _disc.nBound[i - 1]; + } + _disc.strideBound = _disc.boundOffset[_disc.nComp - 1] + _disc.nBound[_disc.nComp - 1]; + + // Determine whether analytic Jacobian should be used but don't set it right now. + // We need to setup Jacobian matrices first. +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + const bool analyticJac = paramProvider.getBool("USE_ANALYTIC_JACOBIAN"); +#else + const bool analyticJac = false; +#endif + + // Allocate space for initial conditions + _initC.resize(_disc.nCol * _disc.nNodes * _disc.nComp); + _initQ.resize(_disc.nCol * _disc.nNodes * _disc.strideBound); + + // Create nonlinear solver for consistent initialization + configureNonlinearSolver(paramProvider); + + paramProvider.popScope(); + + const unsigned int strideCell = _disc.nNodes; + + const bool transportSuccess = _convDispOp.configureModelDiscretization(paramProvider, _disc.nComp, _disc.nCol, strideCell); + + _disc.dispersion = Eigen::VectorXd::Zero(_disc.nComp); // fill later on with convDispOp (section and component dependent) + + _disc.velocity = static_cast(_convDispOp.currentVelocity()); // updated later on with convDispOp (section dependent) + _disc.curSection = -1; + + _disc.length_ = paramProvider.getDouble("COL_LENGTH"); + + _disc.deltaZ = _disc.length_ / _disc.nCol; + + // Allocate memory + Indexer idxr(_disc); + + if (_disc.exactInt) + _jacInlet.resize(_disc.nNodes, 1); // first cell depends on inlet concentration (same for every component) + else + _jacInlet.resize(1, 1); // first cell depends on inlet concentration (same for every component) + _jac.resize((_disc.nComp + _disc.strideBound) * _disc.nPoints, (_disc.nComp + _disc.strideBound) * _disc.nPoints); + _jacDisc.resize((_disc.nComp + _disc.strideBound) * _disc.nPoints, (_disc.nComp + _disc.strideBound) * _disc.nPoints); + // jacobian pattern is set and analyzed after reactions are configured + + // Set whether analytic Jacobian is used + useAnalyticJacobian(analyticJac); + + // ==== Construct and configure binding model + + clearBindingModels(); + _binding.push_back(nullptr); + + if (paramProvider.exists("ADSORPTION_MODEL")) { + _binding[0] = helper.createBindingModel(paramProvider.getString("ADSORPTION_MODEL")); + if (paramProvider.getString("ADSORPTION_MODEL") != "NONE") { + paramProvider.pushScope("adsorption"); + paramProvider.popScope(); + } + } + else + _binding[0] = helper.createBindingModel("NONE"); + + if (!_binding[0]) + throw InvalidParameterException("Unknown binding model " + paramProvider.getString("ADSORPTION_MODEL")); + + bool bindingConfSuccess = true; + if (_binding[0]->usesParamProviderInDiscretizationConfig()) + paramProvider.pushScope("adsorption"); + + bindingConfSuccess = _binding[0]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound, _disc.boundOffset); + + if (_binding[0]->usesParamProviderInDiscretizationConfig()) + paramProvider.popScope(); + + // ==== Construct and configure dynamic reaction model + bool reactionConfSuccess = true; + clearDynamicReactionModels(); + _dynReaction.push_back(nullptr); + + if (paramProvider.exists("REACTION_MODEL")) + { + _dynReaction[0] = helper.createDynamicReactionModel(paramProvider.getString("REACTION_MODEL")); + if (!_dynReaction[0]) + throw InvalidParameterException("Unknown dynamic reaction model " + paramProvider.getString("REACTION_MODEL")); + + if (_dynReaction[0]->usesParamProviderInDiscretizationConfig()) + paramProvider.pushScope("reaction"); + + reactionConfSuccess = _dynReaction[0]->configureModelDiscretization(paramProvider, _disc.nComp, _disc.nBound, _disc.boundOffset); + + if (_dynReaction[0]->usesParamProviderInDiscretizationConfig()) + paramProvider.popScope(); + } + + // Setup the memory for tempState based on state vector + _tempState = new double[numDofs()]; + + + return bindingConfSuccess && reactionConfSuccess; + } + + bool LumpedRateModelWithoutPoresDG::configure(IParameterProvider& paramProvider) + { + _parameters.clear(); + + _convDispOp.configure(_unitOpIdx, paramProvider, _parameters); + + // Read geometry parameters + _totalPorosity = paramProvider.getDouble("TOTAL_POROSITY"); + _disc.porosity = paramProvider.getDouble("TOTAL_POROSITY"); + + // Add parameters to map + _parameters[makeParamId(hashString("TOTAL_POROSITY"), _unitOpIdx, CompIndep, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = &_totalPorosity; + + // Register initial conditions parameters + for (unsigned int i = 0; i < _disc.nComp; ++i) + _parameters[makeParamId(hashString("INIT_C"), _unitOpIdx, i, ParTypeIndep, BoundStateIndep, ReactionIndep, SectionIndep)] = _initC.data() + i; + + if (_binding[0]) + { + std::vector initParams(_disc.strideBound); + _binding[0]->fillBoundPhaseInitialParameters(initParams.data(), _unitOpIdx, cadet::ParTypeIndep); + + for (unsigned int i = 0; i < _disc.strideBound; ++i) + _parameters[initParams[i]] = _initQ.data() + i; + } + + // Reconfigure binding model + bool bindingConfSuccess = true; + if (_binding[0] && paramProvider.exists("adsorption") && _binding[0]->requiresConfiguration()) + { + paramProvider.pushScope("adsorption"); + bindingConfSuccess = _binding[0]->configure(paramProvider, _unitOpIdx, cadet::ParTypeIndep); + paramProvider.popScope(); + } + + // Reconfigure dynamic reaction model + bool reactionConfSuccess = true; + if (_dynReaction[0] && paramProvider.exists("reaction") && _dynReaction[0]->requiresConfiguration()) + { + paramProvider.pushScope("reaction"); + reactionConfSuccess = _dynReaction[0]->configure(paramProvider, _unitOpIdx, cadet::ParTypeIndep); + paramProvider.popScope(); + } + + //FDjac = MatrixXd::Zero(numDofs(), numDofs()); // todo delete! + + setPattern(_jac, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + setPattern(_jacDisc, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + + // the solver repetitively solves the linear system with a static pattern of the jacobian (set above). + // The goal of analyzePattern() is to reorder the nonzero elements of the matrix, such that the factorization step creates less fill-in + _linSolver.analyzePattern(_jacDisc); + + return bindingConfSuccess && reactionConfSuccess; + } + + unsigned int LumpedRateModelWithoutPoresDG::threadLocalMemorySize() const CADET_NOEXCEPT + { + LinearMemorySizer lms; + + // Memory for parts::cell::residualKernel = residualImpl() + if (_binding[0] && _binding[0]->requiresWorkspace()) + lms.addBlock(_binding[0]->workspaceSize(_disc.nComp, _disc.strideBound, _disc.nBound)); + + if (_dynReaction[0]) + { + lms.addBlock(_dynReaction[0]->workspaceSize(_disc.nComp, _disc.strideBound, _disc.nBound)); + lms.add(_disc.strideBound); + lms.add(_disc.strideBound * (_disc.strideBound + _disc.nComp)); + } + + lms.commit(); + const std::size_t resKernelSize = lms.bufferSize(); + + // Memory for consistentInitialSensitivity() + lms.add(_disc.strideBound); + lms.add(_disc.strideBound); + + lms.commit(); + + // Memory for consistentInitialState() + lms.add(_nonlinearSolver->workspaceSize(_disc.strideBound) * sizeof(double)); + lms.add(_disc.strideBound); + lms.add(_disc.strideBound + _disc.nComp); + lms.add(_disc.strideBound + _disc.nComp); + lms.add(_disc.strideBound * _disc.strideBound); + lms.add(_disc.nComp); + lms.addBlock(_binding[0]->workspaceSize(_disc.nComp, _disc.strideBound, _disc.nBound)); + lms.addBlock(resKernelSize); + + lms.commit(); + + return lms.bufferSize(); + } + + void LumpedRateModelWithoutPoresDG::useAnalyticJacobian(const bool analyticJac) + { +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + _analyticJac = analyticJac; + if (!_analyticJac) + _jacobianAdDirs = _jac.cols(); + else + _jacobianAdDirs = 0; +#else + _analyticJac = false; + _jacobianAdDirs = _jac.cols(); +#endif + } + + void LumpedRateModelWithoutPoresDG::notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac) + { + Indexer idxr(_disc); + + // ConvectionDispersionOperator tells us whether flow direction has changed + if (!_convDispOp.notifyDiscontinuousSectionTransition(t, secIdx) && (secIdx != 0)) { + // (re)compute DG Jaconian blocks + updateSection(secIdx); + _disc.initializeDGjac(); + return; + } + else { + // (re)compute DG Jaconian blocks + updateSection(secIdx); + _disc.initializeDGjac(); + } + + // Setup the matrix connecting inlet DOFs to first column cells + //_jacInlet.clear(); + const double h = static_cast(_convDispOp.columnLength()) / static_cast(_disc.nCol); + const double u = static_cast(_convDispOp.currentVelocity()); + + if (u >= 0.0) + { + // Forwards flow + //_jacInlet = MatrixXd::Identity(_disc.nComp, _disc.nComp); + + // Place entries for inlet DOF to first column cell conversion + //for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + //_jacInlet.addElement(comp * idxr.strideColComp(), comp, -u / h) + //; + + // Repartition Jacobians + // TODO: Reset sparsity pattern for DG? + } + else // @TODO: implement Jacobian permutation for backwards flow + { + + // Backwards flow + + // Place entries for inlet DOF to last column cell conversion + //const unsigned int offset = (_disc.nCol - 1) * idxr.strideColCell(); + //for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + //_jacInlet.addElement(offset + comp * idxr.strideColComp(), comp, u / h) + //; + + // Repartition Jacobians + + } + + //prepareADvectors(adJac); + } + + void LumpedRateModelWithoutPoresDG::setFlowRates(active const* in, active const* out) CADET_NOEXCEPT + { + _convDispOp.setFlowRates(in[0], out[0], _totalPorosity); + } + + void LumpedRateModelWithoutPoresDG::reportSolution(ISolutionRecorder& recorder, double const* const solution) const + { + Exporter expr(_disc, *this, solution); + recorder.beginUnitOperation(_unitOpIdx, *this, expr); + recorder.endUnitOperation(); + } + + void LumpedRateModelWithoutPoresDG::reportSolutionStructure(ISolutionRecorder& recorder) const + { + Exporter expr(_disc, *this, nullptr); + recorder.unitOperationStructure(_unitOpIdx, *this, expr); + } + + + unsigned int LumpedRateModelWithoutPoresDG::requiredADdirs() const CADET_NOEXCEPT + { +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + return _jacobianAdDirs; +#else + // If CADET_CHECK_ANALYTIC_JACOBIAN is active, we always need the AD directions for the Jacobian + return _jac.cols();// _jac.stride();@SAM? +#endif + } + + void LumpedRateModelWithoutPoresDG::prepareADvectors(const AdJacobianParams& adJac) const + { + // Early out if AD is disabled + if (!adJac.adY) + return; + + Indexer idxr(_disc); + + // @TODO + // Get bandwidths + //const unsigned int lowerBandwidth = _jac.lowerBandwidth(); + //const unsigned int upperBandwidth = _jac.upperBandwidth(); + //ad::prepareAdVectorSeedsForBandMatrix(adJac.adY + _disc.nComp, adJac.adDirOffset, _jac.rows(), lowerBandwidth, upperBandwidth, lowerBandwidth); + } + + /** + * @brief Extracts the system Jacobian from band compressed AD seed vectors + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ + void LumpedRateModelWithoutPoresDG::extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset) + { + Indexer idxr(_disc); + // @TODO @SAM Ad returned Dense Jacobian? + //ad::extractBandedJacobianFromAd(adRes + idxr.offsetC(), adDirOffset, _jac.lowerBandwidth(), _jac); + } + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + + /** + * @brief Compares the analytical Jacobian with a Jacobian derived by AD + * @details The analytical Jacobian is assumed to be stored in the corresponding band matrices. + * @param [in] adRes Residual vector of AD datatypes with band compressed seed vectors + * @param [in] adDirOffset Number of AD directions used for non-Jacobian purposes (e.g., parameter sensitivities) + */ + void LumpedRateModelWithoutPoresDG::checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const + { + Indexer idxr(_disc); + + // @TODO @SAM Ad returned Dense Jacobian? + //const double maxDiff = ad::compareBandedJacobianWithAd(adRes + idxr.offsetC(), adDirOffset, _jac.lowerBandwidth(), _jac); + //LOG(Debug) << "AD dir offset: " << adDirOffset << " DiagDirCol: " << _jac.lowerBandwidth() << " MaxDiff: " << maxDiff; + } + +#endif + + int LumpedRateModelWithoutPoresDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerResidual); + + // Evaluate residual do not compute Jacobian or parameter sensitivities + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } + + int LumpedRateModelWithoutPoresDG::residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerResidual); + + //FDjac = calcFDJacobian(static_cast(simState.vecStateY), static_cast(simState.vecStateYdot), simTime, threadLocalMem, 2.0); // todo delete + + // Evaluate residual, use AD for Jacobian if required but do not evaluate parameter derivatives + return residual(simTime, simState, res, adJac, threadLocalMem, true, false); + } + + int LumpedRateModelWithoutPoresDG::residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, + const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity) + { + if (updateJacobian) + { + _factorizeJacobian = true; + +#ifndef CADET_CHECK_ANALYTIC_JACOBIAN + if (_analyticJac) + { + if (paramSensitivity) + { + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } + else + { + // Compute Jacobian via AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; + } +#else + // Compute Jacobian via AD + + // Copy over state vector to AD state vector (without changing directional values to keep seed vectors) + // and initialize residuals with zero (also resetting directional values) + ad::copyToAd(simState.vecStateY, adJac.adY, numDofs()); + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + // Evaluate with AD enabled + int retCode = 0; + if (paramSensitivity) + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + else + retCode = residualImpl(simTime.t, simTime.secIdx, adJac.adY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Only do comparison if we have a residuals vector (which is not always the case) + if (res) + { + // Evaluate with analytical Jacobian which is stored in the band matrices + retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + + // Compare AD with anaytic Jacobian + checkAnalyticJacobianAgainstAd(adJac.adRes, adJac.adDirOffset); + } + + // Extract Jacobian + extractJacobianFromAD(adJac.adRes, adJac.adDirOffset); + + return retCode; +#endif + } + else + { + if (paramSensitivity) + { + // initialize residuals with zero + // @todo Check if this is necessary + ad::resetAd(adJac.adRes, numDofs()); + + const int retCode = residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adJac.adRes, threadLocalMem); + + // Copy AD residuals to original residuals vector + if (res) + ad::copyFromAd(adJac.adRes, res, numDofs()); + + return retCode; + } + else + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, res, threadLocalMem); + } + } + + template + int LumpedRateModelWithoutPoresDG::residualImpl(double t, unsigned int secIdx, StateType const* const y_, double const* const yDot_, ResidualType* const res_, util::ThreadLocalStorage& threadLocalMem) + { + Indexer idxr(_disc); + + // Eigen access to data pointers + const double* yPtr = reinterpret_cast(y_); + const double* const ypPtr = reinterpret_cast(yDot_); + double* const resPtr = reinterpret_cast(res_); + + bool success = 1; + + // determine wether we have a section switch. If so, set velocity, dispersion, newStaticJac + + if (wantJac) { + + if (_disc.newStaticJac) { // static part of jacobian only needs to be updated at new sections + + // TODO: reset pattern every time section? + //setPattern(_jacDisc, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + //setPattern(_jac, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + success = calcStaticAnaJacobian(t, secIdx, yPtr, threadLocalMem); + + _disc.newStaticJac = false; + } + + if (cadet_unlikely(!success)) + LOG(Error) << "Jacobian pattern did not fit the Jacobian estimation"; + + } + + // ==========================================// + // Estimate binding, reactions Residual // + // ==========================================// + +#ifdef CADET_PARALLELIZE + tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints), [&](std::size_t blk) +#else + for (unsigned int blk = 0; blk < _disc.nPoints; ++blk) +#endif + { + linalg::BandedEigenSparseRowIterator jacIt(_jac, blk * idxr.strideColNode()); + StateType const* const localY = y_ + idxr.offsetC() + idxr.strideColNode() * blk; + ResidualType* const localRes = res_ + idxr.offsetC() + idxr.strideColNode() * blk; + double const* const localYdot = yDot_ ? yDot_ + idxr.offsetC() + idxr.strideColNode() * blk : nullptr; + + const parts::cell::CellParameters cellResParams + { + _disc.nComp, + _disc.nBound, + _disc.boundOffset, + _disc.strideBound, + _binding[0]->reactionQuasiStationarity(), + _totalPorosity, + nullptr, + _binding[0], + (_dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)) ? _dynReaction[0] : nullptr + }; + + // position of current column node (z coordinate) - needed in externally dependent adsorption kinetic + double z = _disc.deltaZ * std::floor(blk / _disc.nNodes) + 0.5 * _disc.deltaZ * (1 + _disc.nodes[blk % _disc.nNodes]); + + parts::cell::residualKernel( + t, secIdx, ColumnPosition{ z, 0.0, 0.0 }, localY, localYdot, localRes, jacIt, cellResParams, threadLocalMem.get() + ); + + } CADET_PARFOR_END; + + // ==================================================// + // Estimate Convection Dispersion residual // + // ================================================// + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + + // extract current component mobile phase, mobile phase residual, mobile phase derivative (discontinous memory blocks) + Eigen::Map> C_comp(yPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> cRes_comp(resPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + Eigen::Map> cDot_comp(ypPtr + idxr.offsetC() + comp, _disc.nPoints, InnerStride(idxr.strideColNode())); + + /* convection dispersion RHS */ + + _disc.boundary[0] = yPtr[comp]; // copy inlet DOFs to ghost node + ConvDisp_DG(C_comp, cRes_comp, t, comp); + + /* residual */ + + res_[comp] = y_[comp]; // simply copy the inlet DOFs to the residual (handled in inlet unit operation) + + if (ypPtr) { // NULLpointer for consistent initialization + if (_disc.nBound[comp]) { // either one or null + Eigen::Map> qDot_comp(ypPtr + idxr.offsetC() + idxr.strideColLiquid() + idxr.offsetBoundComp(comp), _disc.nPoints, InnerStride(idxr.strideColNode())); + cRes_comp = cDot_comp + qDot_comp * ((1 - _disc.porosity) / _disc.porosity) - cRes_comp; + } + else + cRes_comp = cDot_comp - cRes_comp; + } + else + cRes_comp *= -1.0; + } + + return 0; + } + + int LumpedRateModelWithoutPoresDG::residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode and at the same time update the + // Jacobian (in one AD run, if analytic Jacobians are disabled) + return residual(simTime, simState, nullptr, adJac, threadLocalMem, true, true); + } + + int LumpedRateModelWithoutPoresDG::residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerResidualSens); + + // Evaluate residual for all parameters using AD in vector mode + return residualImpl(simTime.t, simTime.secIdx, simState.vecStateY, simState.vecStateYdot, adRes, threadLocalMem); + } + + int LumpedRateModelWithoutPoresDG::residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3) + { + BENCH_SCOPE(_timerResidualSens); + + // tmp1 stores result of (dF / dy) * s + // tmp2 stores result of (dF / dyDot) * sDot + + for (std::size_t param = 0; param < yS.size(); ++param) + { + // Directional derivative (dF / dy) * s + multiplyWithJacobian(SimulationTime{ 0.0, 0u }, ConstSimulationState{ nullptr, nullptr }, yS[param], 1.0, 0.0, tmp1); + + // Directional derivative (dF / dyDot) * sDot + multiplyWithDerivativeJacobian(SimulationTime{ 0.0, 0u }, ConstSimulationState{ nullptr, nullptr }, ySdot[param], tmp2); + + double* const ptrResS = resS[param]; + + BENCH_START(_timerResidualSensPar); + + // Complete sens residual is the sum: + // TODO: Chunk TBB loop +#ifdef CADET_PARALLELIZE + tbb::parallel_for(std::size_t(0), static_cast(numDofs()), [&](std::size_t i) +#else + for (unsigned int i = 0; i < numDofs(); ++i) +#endif + { + ptrResS[i] = tmp1[i] + tmp2[i] + adRes[i].getADValue(param); + } CADET_PARFOR_END; + + BENCH_STOP(_timerResidualSensPar); + } + + return 0; + } + + /** + * @brief Multiplies the given vector with the system Jacobian (i.e., @f$ \frac{\partial F}{\partial y}\left(t, y, \dot{y}\right) @f$) + * @details Actually, the operation @f$ z = \alpha \frac{\partial F}{\partial y} x + \beta z @f$ is performed. + * + * Note that residual() or one of its cousins has to be called with the requested point @f$ (t, y, \dot{y}) @f$ once + * before calling multiplyWithJacobian() as this implementation ignores the given @f$ (t, y, \dot{y}) @f$. + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] yS Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] alpha Factor @f$ \alpha @f$ in front of @f$ \frac{\partial F}{\partial y} @f$ + * @param [in] beta Factor @f$ \beta @f$ in front of @f$ z @f$ + * @param [in,out] ret Vector @f$ z @f$ which stores the result of the operation + */ + void LumpedRateModelWithoutPoresDG::multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret) + { + + Eigen::Map _ret(ret, numDofs()); + Eigen::Map _yS(yS, numDofs()); + + _ret = alpha * _jac * _yS + beta * _ret; // NOTE: inlet DOFs are included in DG jacobian + + } + + /** + * @brief Multiplies the time derivative Jacobian @f$ \frac{\partial F}{\partial \dot{y}}\left(t, y, \dot{y}\right) @f$ with a given vector + * @details The operation @f$ z = \frac{\partial F}{\partial \dot{y}} x @f$ is performed. + * The matrix-vector multiplication is performed matrix-free (i.e., no matrix is explicitly formed). + * @param [in] simTime Current simulation time point + * @param [in] simState Simulation state vectors + * @param [in] sDot Vector @f$ x @f$ that is transformed by the Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ + * @param [out] ret Vector @f$ z @f$ which stores the result of the operation + */ + void LumpedRateModelWithoutPoresDG::multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret) + { + Indexer idxr(_disc); + const double invBeta = (1.0 / static_cast(_totalPorosity) - 1.0); + + _convDispOp.multiplyWithDerivativeJacobian(simTime, sDot, ret); + for (unsigned int col = 0; col < _disc.nCol; ++col) + { + const unsigned int localOffset = idxr.offsetC() + col * idxr.strideColCell(); + double const* const localSdot = sDot + localOffset; + double* const localRet = ret + localOffset; + + parts::cell::multiplyWithDerivativeJacobianKernel(localSdot, localRet, _disc.nComp, _disc.nBound, _disc.boundOffset, _disc.strideBound, _binding[0]->reactionQuasiStationarity(), 1.0, invBeta); + } + + // Handle inlet DOFs (all algebraic) + std::fill_n(ret, _disc.nComp, 0.0); + } + + void LumpedRateModelWithoutPoresDG::setExternalFunctions(IExternalFunction** extFuns, unsigned int size) + { + if (_binding[0]) + _binding[0]->setExternalFunctions(extFuns, size); + } + + unsigned int LumpedRateModelWithoutPoresDG::localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT + { + // Inlets are duplicated so need to be accounted for + if (static_cast(_convDispOp.currentVelocity()) >= 0.0) + // Forward Flow: outlet is last cell + return _disc.nComp + (_disc.nPoints - 1) * (_disc.nComp + _disc.strideBound); + else + // Backward flow: Outlet is first cell + return _disc.nComp; + } + + unsigned int LumpedRateModelWithoutPoresDG::localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT + { + return 0; + } + + unsigned int LumpedRateModelWithoutPoresDG::localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT + { + return 1; + } + + unsigned int LumpedRateModelWithoutPoresDG::localInletComponentStride(unsigned int port) const CADET_NOEXCEPT + { + return 1; + } + + void LumpedRateModelWithoutPoresDG::expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut) + { + // @todo Write this function + } + + /** + * @brief Computes the solution of the linear system involving the system Jacobian + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The right hand side \f$ b \f$ is given by @p rhs, the Jacobians are evaluated at the + * point \f$(y, \dot{y})\f$ given by @p y and @p yDot. The residual @p res at this point, \f$ F(t, y, \dot{y}) \f$, + * may help with this. Error weights (see IDAS guide) are given in @p weight. The solution is returned in @p rhs. + * + * @param [in] t Current time point + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] outerTol Error tolerance for the solution of the linear system from outer Newton iteration + * @param [in,out] rhs On entry the right hand side of the linear equation system, on exit the solution + * @param [in] weight Vector with error weights + * @param [in] simState State of the simulation (state vector and its time derivatives) at which the Jacobian is evaluated + * @return @c 0 on success, @c -1 on non-recoverable error, and @c +1 on recoverable error + */ + int LumpedRateModelWithoutPoresDG::linearSolve(double t, double alpha, double outerTol, double* const rhs, double const* const weight, + const ConstSimulationState& simState) + { + BENCH_SCOPE(_timerLinearSolve); + + Indexer idxr(_disc); + + bool success = true; + bool result = true; + + // Assemble Jacobian + assembleDiscretizedJacobian(alpha, idxr); + + // solve J x = rhs + Eigen::Map r(rhs, numDofs()); + + // Factorize Jacobian only if required + if (_factorizeJacobian) + { + _linSolver.factorize(_jacDisc); + + if (_linSolver.info() != Success) { + LOG(Error) << "factorization failed"; + success = false; + } + } + + // Use the factors to solve the linear system + r.segment(idxr.offsetC(), numPureDofs()) = _linSolver.solve(r.segment(idxr.offsetC(), numPureDofs())); + + if (_linSolver.info() != Success) { + LOG(Error) << "solve() failed"; + result = false; + } + + // Handle inlet DOFs: + // Inlet at z = 0 for forward flow, at z = L for backward flow. + unsigned int offInlet = (_disc.velocity >= 0.0) ? 0 : (_disc.nCol - 1u) * idxr.strideColCell(); + + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + for (unsigned int node = 0; node < (_disc.exactInt ? _disc.nNodes : 1); node++) { + r[idxr.offsetC() + offInlet + comp * idxr.strideColComp() + node * idxr.strideColNode()] += _jacInlet(node, 0) * r[comp]; + } + } + + return (success && result) ? 0 : 1; + } + + /** + * @brief Assembles the Jacobian of the time-discretized equations + * @details The system \f[ \left( \frac{\partial F}{\partial y} + \alpha \frac{\partial F}{\partial \dot{y}} \right) x = b \f] + * has to be solved. The system Jacobian of the original equations, + * \f[ \frac{\partial F}{\partial y}, \f] + * is already computed (by AD or manually in residualImpl() with @c wantJac = true). This function is responsible + * for adding + * \f[ \alpha \frac{\partial F}{\partial \dot{y}} \f] + * to the system Jacobian, which yields the Jacobian of the time-discretized equations + * \f[ F\left(t, y_0, \sum_{k=0}^N \alpha_k y_k \right) = 0 \f] + * when a BDF method is used. The time integrator needs to solve this equation for @f$ y_0 @f$, which requires + * the solution of the linear system mentioned above (@f$ \alpha_0 = \alpha @f$ given in @p alpha). + * + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] idxr Indexer + */ + void LumpedRateModelWithoutPoresDG::assembleDiscretizedJacobian(double alpha, const Indexer& idxr) + { + // set to static jacobian entries + _jacDisc = _jac; + + // add time derivative jacobian entries + addTimederJacobian(alpha); + } + + /** + * @brief Adds Jacobian @f$ \frac{\partial F}{\partial \dot{y}} @f$ to cell of system Jacobian + * @details Actually adds @f$ \alpha \frac{\partial F}{\partial \dot{y}} @f$, which is useful + * for constructing the linear system in BDF time discretization. + * @param [in,out] jac On entry, RowIterator pointing to the beginning of a cell; + * on exit, the iterator points to the end of the cell + * @param [in] idxr Indexer + * @param [in] alpha Value of \f$ \alpha \f$ (arises from BDF time discretization) + * @param [in] invBeta Inverse porosity term @f$\frac{1}{\beta}@f$ + */ + void LumpedRateModelWithoutPoresDG::addTimeDerivativeToJacobianNode(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, double invBeta) const + { + + // Mobile phase + for (int comp = 0; comp < static_cast(_disc.nComp); ++comp, ++jac) + { + // dc / dt is handled by convection dispersion operator + + // Add derivative with respect to dq / dt to Jacobian + for (int i = 0; i < static_cast(_disc.nBound[comp]); ++i) + { + // Index explanation: + // -comp -> go back to beginning of liquid phase + // + strideColLiquid() skip to solid phase + // + offsetBoundComp() jump to component (skips all bound states of previous components) + // + i go to current bound state + jac[idxr.strideColLiquid() - comp + idxr.offsetBoundComp(comp) + i] += alpha * invBeta; + } + } + + // Solid phase + int const* const qsReaction = _binding[0]->reactionQuasiStationarity(); + for (unsigned int bnd = 0; bnd < _disc.strideBound; ++bnd, ++jac) + { + // Add derivative with respect to dynamic states to Jacobian + if (qsReaction[bnd]) + continue; + + // Add derivative with respect to dq / dt to Jacobian + jac[0] += alpha; + } + } + + void LumpedRateModelWithoutPoresDG::applyInitialCondition(const SimulationState& simState) const + { + Indexer idxr(_disc); + + // Check whether full state vector is available as initial condition + if (!_initState.empty()) + { + std::fill(simState.vecStateY, simState.vecStateY + idxr.offsetC(), 0.0); + std::copy(_initState.data(), _initState.data() + numPureDofs(), simState.vecStateY + idxr.offsetC()); + + if (!_initStateDot.empty()) + { + std::fill(simState.vecStateYdot, simState.vecStateYdot + idxr.offsetC(), 0.0); + std::copy(_initStateDot.data(), _initStateDot.data() + numPureDofs(), simState.vecStateYdot + idxr.offsetC()); + } + else + std::fill(simState.vecStateYdot, simState.vecStateYdot + numDofs(), 0.0); + + return; + } + + double* const stateYbulk = simState.vecStateY + idxr.offsetC(); + + // Loop over column cells + for (unsigned int point = 0; point < _disc.nPoints; ++point) + { + const unsigned int localOffset = point * idxr.strideColNode(); + + // Loop over components in cell + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[localOffset + comp * idxr.strideColComp()] = static_cast(_initC[comp]); + + // Initialize q + for (unsigned int bnd = 0; bnd < _disc.strideBound; ++bnd) + stateYbulk[localOffset + idxr.strideColLiquid() + bnd] = static_cast(_initQ[bnd]); + } + } + + void LumpedRateModelWithoutPoresDG::readInitialCondition(IParameterProvider& paramProvider) + { + _initState.clear(); + _initStateDot.clear(); + + // Check if INIT_STATE is present + if (paramProvider.exists("INIT_STATE")) + { + const std::vector initState = paramProvider.getDoubleArray("INIT_STATE"); + _initState = std::vector(initState.begin(), initState.begin() + numPureDofs()); + + // Check if INIT_STATE contains the full state and its time derivative + if (initState.size() >= 2 * numPureDofs()) + _initStateDot = std::vector(initState.begin() + numPureDofs(), initState.begin() + 2 * numPureDofs()); + return; + } + + const std::vector initC = paramProvider.getDoubleArray("INIT_C"); + std::vector initQ; + + if (paramProvider.exists("INIT_Q")) + initQ = paramProvider.getDoubleArray("INIT_Q"); + + if (initC.size() < _disc.nComp) + throw InvalidParameterException("INIT_C does not contain enough values for all components"); + + if ((_disc.strideBound > 0) && (initQ.size() < _disc.strideBound)) + throw InvalidParameterException("INIT_Q does not contain enough values for all bound states"); + + ad::copyToAd(initC.data(), _initC.data(), _disc.nComp); + if (!initQ.empty()) + ad::copyToAd(initQ.data(), _initQ.data(), _disc.strideBound); + } + + /** + * @brief Computes consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise).
  4. + *
+ * + * This function performs step 1. See consistentInitialTimeDerivative() for step 2. + * + * This function is to be used with consistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithoutPoresDG::consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // Step 1: Solve algebraic equations + if (!_binding[0]->hasQuasiStationaryReactions()) + return; + + // Copy quasi-stationary binding mask to a local array that also includes the mobile phase + std::vector qsMask(_disc.nComp + _disc.strideBound, false); + int const* const qsMaskSrc = _binding[0]->reactionQuasiStationarity(); + std::copy_n(qsMaskSrc, _disc.strideBound, qsMask.data() + _disc.nComp); + + // Activate mobile phase components that have at least one active bound state + unsigned int bndStartIdx = 0; + unsigned int numActiveComp = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + for (unsigned int bnd = 0; bnd < _disc.nBound[comp]; ++bnd) + { + if (qsMaskSrc[bndStartIdx + bnd]) + { + ++numActiveComp; + qsMask[comp] = true; + break; + } + } + + bndStartIdx += _disc.nBound[comp]; + } + + const linalg::ConstMaskArray mask{ qsMask.data(), static_cast(_disc.nComp + _disc.strideBound) }; + const int probSize = linalg::numMaskActive(mask); + + //Problem capturing variables here +#ifdef CADET_PARALLELIZE + BENCH_SCOPE(_timerConsistentInitPar); + tbb::parallel_for(std::size_t(0), static_cast(_disc.nPoints), [&](std::size_t point) +#else + for (unsigned int point = 0; point < _disc.nPoints; point++) +#endif + { + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + // Reuse memory of band matrix for dense matrix + linalg::DenseMatrixView fullJacobianMatrix(_jacDisc.valuePtr() + point * _disc.strideBound * _disc.strideBound, nullptr, mask.len, mask.len); + + // z coordinate (column length normed to 1) of current node - needed in externally dependent adsorption kinetic + const double z = (_disc.deltaZ * std::floor(point / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[point % _disc.nNodes])) / _disc.length_; + + // Get workspace memory + BufferedArray nonlinMemBuffer = tlmAlloc.array(_nonlinearSolver->workspaceSize(probSize)); + double* const nonlinMem = static_cast(nonlinMemBuffer); + + BufferedArray solutionBuffer = tlmAlloc.array(probSize); + double* const solution = static_cast(solutionBuffer); + + BufferedArray fullResidualBuffer = tlmAlloc.array(mask.len); + double* const fullResidual = static_cast(fullResidualBuffer); + + BufferedArray fullXBuffer = tlmAlloc.array(mask.len); + double* const fullX = static_cast(fullXBuffer); + + BufferedArray jacobianMemBuffer = tlmAlloc.array(probSize * probSize); + double* const jacobianMem = static_cast(jacobianMemBuffer); + + BufferedArray conservedQuantsBuffer = tlmAlloc.array(numActiveComp); + double* const conservedQuants = static_cast(conservedQuantsBuffer); + + linalg::DenseMatrixView jacobianMatrix(jacobianMem, _jacDisc.innerIndexPtr() + point * _disc.strideBound, probSize, probSize); + const parts::cell::CellParameters cellResParams + { + _disc.nComp, + _disc.nBound, + _disc.boundOffset, + _disc.strideBound, + _binding[0]->reactionQuasiStationarity(), + _totalPorosity, + nullptr, + _binding[0], + (_dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)) ? _dynReaction[0] : nullptr + }; + + const int localOffsetToCell = idxr.offsetC() + point * idxr.strideColNode(); + const int localOffsetInCell = idxr.strideColLiquid(); + + // Get pointer to q variables in cell + double* const qShell = vecStateY + localOffsetToCell + localOffsetInCell; + active* const localAdRes = adJac.adRes ? adJac.adRes + localOffsetToCell : nullptr; + active* const localAdY = adJac.adY ? adJac.adY + localOffsetToCell : nullptr; + + const ColumnPosition colPos{ z, 0.0, 0.0 }; + + // Determine whether nonlinear solver is required + if (!_binding[0]->preConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - localOffsetInCell, tlmAlloc)) + CADET_PAR_CONTINUE; + + // Extract initial values from current state + linalg::selectVectorSubset(qShell - _disc.nComp, mask, solution); + + // Save values of conserved moieties + const double epsQ = 1.0 - static_cast(_totalPorosity); + linalg::conservedMoietiesFromPartitionedMask(mask, _disc.nBound, _disc.nComp, qShell - _disc.nComp, conservedQuants, static_cast(_totalPorosity), epsQ); + + std::function jacFunc; + + jacFunc = [&](double const* const x, linalg::detail::DenseMatrixBase& mat) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract Jacobian from full Jacobian + mat.setAll(0.0); + linalg::copyMatrixSubset(fullJacobianMatrix, mask, mask, mat); + + // Replace upper part with conservation relations + mat.submatrixSetAll(0.0, 0, 0, numActiveComp, probSize); + + unsigned int bndIdx = 0; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[comp]; + continue; + } + + mat.native(rIdx, rIdx) = static_cast(_totalPorosity); + + for (unsigned int bnd = 0; bnd < _disc.nBound[comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + mat.native(rIdx, bIdx + numActiveComp) = epsQ; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }; + + // Apply nonlinear solver + _nonlinearSolver->solve( + [&](double const* const x, double* const r) + { + // Prepare input vector by overwriting masked items + std::copy_n(qShell - _disc.nComp, mask.len, fullX); + linalg::applyVectorSubset(x, mask, fullX); + + // Call residual function + parts::cell::residualKernel( + simTime.t, simTime.secIdx, colPos, fullX, nullptr, fullResidual, fullJacobianMatrix.row(0), cellResParams, tlmAlloc + ); + + // Extract values from residual + linalg::selectVectorSubset(fullResidual, mask, r); + + // Calculate residual of conserved moieties + std::fill_n(r, numActiveComp, 0.0); + unsigned int bndIdx = _disc.nComp; + unsigned int rIdx = 0; + unsigned int bIdx = 0; + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + if (!mask.mask[comp]) + { + bndIdx += _disc.nBound[comp]; + continue; + } + + r[rIdx] = static_cast(_totalPorosity) * x[rIdx] - conservedQuants[rIdx]; + + for (unsigned int bnd = 0; bnd < _disc.nBound[comp]; ++bnd, ++bndIdx) + { + if (mask.mask[bndIdx]) + { + r[rIdx] += epsQ * x[bIdx + numActiveComp]; + ++bIdx; + } + } + + ++rIdx; + } + + return true; + }, + jacFunc, errorTol, solution, nonlinMem, jacobianMatrix, probSize); + + // Apply solution + linalg::applyVectorSubset(solution, mask, qShell - idxr.strideColLiquid()); + + // Refine / correct solution + _binding[0]->postConsistentInitialState(simTime.t, simTime.secIdx, colPos, qShell, qShell - idxr.strideColLiquid(), tlmAlloc); + } CADET_PARFOR_END; + + // restore _jacDisc pattern + setPattern(_jacDisc, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + + } + + /** + * @brief Computes consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * The process works in two steps: + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{y}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the state vector @f$ y @f$ is fixed). + * + * The right hand side of the linear system is given by the negative residual without contribution + * of @f$ \dot{y} @f$ for differential equations and 0 for algebraic equations + * (@f$ -\frac{\partial F}{\partial t}@f$, to be more precise).
  4. + *
+ * + * This function performs step 2. See consistentInitialState() for step 1. + * + * This function is to be used with consistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] vecStateY Consistently initialized state vector + * @param [in,out] vecStateYdot On entry, residual without taking time derivatives into account. On exit, consistent state time derivatives. + */ + void LumpedRateModelWithoutPoresDG::consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + //// Step 2: Compute the correct time derivative of the state vector + + //// Note that the residual has not been negated, yet. We will do that now. + for (unsigned int i = 0; i < numDofs(); ++i) + vecStateYdot[i] = -vecStateYdot[i]; + + double* entries = _jacDisc.valuePtr(); + for (unsigned int nz = 0; nz < _jacDisc.nonZeros(); nz++) + entries[nz] = 0.0; + + // Handle transport equations (dc_i / dt terms) + const int gapNode = idxr.strideColNode() - static_cast(_disc.nComp) * idxr.strideColComp(); + linalg::BandedEigenSparseRowIterator jac(_jacDisc, 0); + for (unsigned int i = 0; i < _disc.nPoints; ++i, jac += gapNode) + { + for (unsigned int j = 0; j < _disc.nComp; ++j, ++jac) + { + // Add time derivative to liquid states (on main diagonal) + jac[0] += 1.0; + } + } + + const double invBeta = 1.0 / static_cast(_totalPorosity) - 1.0; + double* const dFluxDt = _tempState + idxr.offsetC(); + LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + for (unsigned int col = 0; col < _disc.nPoints; ++col) + { + // Assemble + linalg::BandedEigenSparseRowIterator jac(_jacDisc, idxr.strideColNode() * col); + + // Mobile and solid phase (advances jac accordingly) + addTimeDerivativeToJacobianNode(jac, idxr, 1.0, invBeta); + + // Stationary phase + if (!_binding[0]->hasQuasiStationaryReactions()) + continue; + + // Midpoint of current column node (z coordinate) - needed in externally dependent adsorption kinetic + double z = _disc.deltaZ * std::floor(col / _disc.nNodes) + 0.5 * _disc.deltaZ * (1 + _disc.nodes[col % _disc.nNodes]); + + // Get iterators to beginning of solid phase + linalg::BandedEigenSparseRowIterator jacSolidOrig(_jac, idxr.strideColNode() * col + idxr.strideColLiquid()); + linalg::BandedEigenSparseRowIterator jacSolid = jac - idxr.strideColBound(); + + int const* const mask = _binding[0]->reactionQuasiStationarity(); + double* const qNodeDot = vecStateYdot + idxr.offsetC() + col * idxr.strideColNode() + idxr.strideColLiquid(); + + // Obtain derivative of fluxes wrt. time + std::fill_n(dFluxDt, _disc.strideBound, 0.0); + if (_binding[0]->dependsOnTime()) + { + _binding[0]->timeDerivativeQuasiStationaryFluxes(simTime.t, simTime.secIdx, ColumnPosition{ z, 0.0, 0.0 }, + qNodeDot - _disc.nComp, qNodeDot, dFluxDt, tlmAlloc); + } + + // Copy row from original Jacobian and set right hand side + for (unsigned int i = 0; i < _disc.strideBound; ++i, ++jacSolid, ++jacSolidOrig) + { + if (!mask[i]) + continue; + + jacSolid.copyRowFrom(jacSolidOrig); + qNodeDot[i] = -dFluxDt[i]; + } + } + + _linSolver.factorize(_jacDisc); + + if (_linSolver.info() != Success) { + LOG(Error) << "factorization failed in consistent initialization"; + } + + Eigen::Map yp(vecStateYdot + idxr.offsetC(), numPureDofs()); + + yp = _linSolver.solve(yp); + + if (_linSolver.info() != Success) { + LOG(Error) << "Solve failed in consistent initialization"; + } + + // reset jacobian pattern + setPattern(_jacDisc, true, _dynReaction[0] && (_dynReaction[0]->numReactionsCombined() > 0)); + + } + + /** + * @brief Computes approximately / partially consistent initial values (state variables without their time derivatives) + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialState(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 for the + * mobile phase variables.
  4. + *
+ * This function performs step 1. See leanConsistentInitialTimeDerivative() for step 2. + * + * This function is to be used with leanConsistentInitialTimeDerivative(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in,out] vecStateY State vector with initial values that are to be updated for consistency + * @param [in,out] adJac Jacobian information for AD (AD vectors for residual and state, direction offset) + * @param [in] errorTol Error tolerance for algebraic equations + */ + void LumpedRateModelWithoutPoresDG::leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem) + { + } + + /** + * @brief Computes approximately / partially consistent initial time derivatives + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] the initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ have + * to be consistent. This functions updates the initial state \f$ y_0 \f$ and overwrites the time + * derivative \f$ \dot{y}_0 \f$ such that they are consistent. + * + * This function performs a relaxed consistent initialization: Only parts of the vectors are updated + * and, hence, consistency is not guaranteed. Since there is less work to do, it is probably faster than + * the standard process represented by consistentInitialState(). + * + * The process works in two steps: + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 for the + * mobile phase variables.
  4. + *
+ * This function performs step 2. See leanConsistentInitialState() for step 1. + * + * This function is to be used with leanConsistentInitialState(). Do not mix normal and lean + * consistent initialization! + * + * @param [in] t Current time point + * @param [in] vecStateY (Lean) consistently initialized state vector + * @param [in,out] vecStateYdot On entry, inconsistent state time derivatives. On exit, partially consistent state time derivatives. + * @param [in] res On entry, residual without taking time derivatives into account. The data is overwritten during execution of the function. + */ + void LumpedRateModelWithoutPoresDG::leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + // Step 2: Compute the correct time derivative of the state vector (only mobile phase DOFs) + + const double invBeta = (1.0 / static_cast(_totalPorosity) - 1.0); + for (unsigned int col = 0; col < _disc.nCol; ++col) + { + // Offset to current cell's c and q variables + const unsigned int localOffset = idxr.offsetC() + col * idxr.strideColCell(); + const unsigned int localOffsetQ = localOffset + idxr.strideColLiquid(); + + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + // dq_{i,j} / dt is assumed to be fixed, so bring it on the right hand side + for (unsigned int i = 0; i < _disc.nBound[comp]; ++i) + { + res[localOffset + comp] += invBeta * vecStateYdot[localOffsetQ + _disc.boundOffset[comp] + i]; + } + + vecStateYdot[localOffset + comp] = -res[localOffset + comp]; + } + } + } + + void LumpedRateModelWithoutPoresDG::initializeSensitivityStates(const std::vector& vecSensY) const + { + Indexer idxr(_disc); + for (std::size_t param = 0; param < vecSensY.size(); ++param) + { + double* const stateYbulk = vecSensY[param] + idxr.offsetC(); + + // Loop over column cells + for (unsigned int col = 0; col < _disc.nCol; ++col) + { + const unsigned int localOffset = col * idxr.strideColCell(); + + // Loop over components in cell + for (unsigned comp = 0; comp < _disc.nComp; ++comp) + stateYbulk[localOffset + comp * idxr.strideColComp()] = _initC[comp].getADValue(param); + + // Initialize q + for (unsigned int bnd = 0; bnd < _disc.strideBound; ++bnd) + stateYbulk[localOffset + idxr.strideColLiquid() + bnd] = _initQ[bnd].getADValue(param); + } + } + } + + /** + * @brief Computes consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of consistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Solve all algebraic equations in the model (e.g., quasi-stationary isotherms, reaction equilibria). + * Let @f$ \mathcal{I}_a @f$ be the index set of algebraic equations, then, at this point, we have + * \f[ \left( \frac{\partial F}{\partial y}(t, y_0, \dot{y}_0) s + \frac{\partial F}{\partial p}(t, y_0, \dot{y}_0) \right)_{\mathcal{I}_a} = 0. \f]
  2. + *
  3. Compute the time derivatives of the sensitivity @f$ \dot{s} @f$ such that the differential equations hold. + * However, because of the algebraic equations, we need additional conditions to fully determine + * @f$ \dot{s}@f$. By differentiating the algebraic equations with respect to time, we get the + * missing linear equations (recall that the sensitivity vector @f$ s @f$ is fixed). + * + * Let @f$ \mathcal{I}_d @f$ denote the index set of differential equations. + * The right hand side of the linear system is given by @f[ -\frac{\partial F}{\partial y}(t, y, \dot{y}) s - \frac{\partial F}{\partial p}(t, y, \dot{y}), @f] + * which is 0 for algebraic equations (@f$ -\frac{\partial^2 F}{\partial t \partial p}@f$, to be more precise).
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithoutPoresDG::consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + // TODOD ? + // for (std::size_t param = 0; param < vecSensY.size(); ++param) + // { + // double* const sensY = vecSensY[param]; + // double* const sensYdot = vecSensYdot[param]; + // + // // Copy parameter derivative dF / dp from AD and negate it + // for (unsigned int i = _disc.nComp; i < numDofs(); ++i) + // sensYdot[i] = -adRes[i].getADValue(param); + // + // // Step 1: Solve algebraic equations + // + // if (_binding[0]->hasQuasiStationaryReactions()) + // { + // int const* const qsMask = _binding[0]->reactionQuasiStationarity(); + // const linalg::ConstMaskArray mask{qsMask, static_cast(_disc.strideBound)}; + // const int probSize = linalg::numMaskActive(mask); + // + //#ifdef CADET_PARALLELIZE + // BENCH_SCOPE(_timerConsistentInitPar); + // tbb::parallel_for(std::size_t(0), static_cast(_disc.nCol), [&](std::size_t col) + //#else + // for (unsigned int col = 0; col < _disc.nCol; ++col) + //#endif + // { + // const unsigned int jacRowOffset = idxr.strideColCell() * col + static_cast(idxr.strideColLiquid()); + // const int localQOffset = idxr.offsetC() + col * idxr.strideColCell() + idxr.strideColLiquid(); + // + // // Reuse memory of band matrix for dense matrix + // linalg::DenseMatrixView jacobianMatrix(_jacDisc.data() + col * _disc.strideBound * _disc.strideBound, _jacDisc.pivot() + col * _disc.strideBound, probSize, probSize); + // + // // Get workspace memory + // LinearBufferAllocator tlmAlloc = threadLocalMem.get(); + // + // BufferedArray rhsBuffer = tlmAlloc.array(probSize); + // double* const rhs = static_cast(rhsBuffer); + // + // BufferedArray rhsUnmaskedBuffer = tlmAlloc.array(_disc.strideBound); + // double* const rhsUnmasked = static_cast(rhsUnmaskedBuffer); + // + // double* const maskedMultiplier = _tempState + idxr.offsetC() + col * idxr.strideColCell(); + // + // // Extract subproblem Jacobian from full Jacobian + // jacobianMatrix.setAll(0.0); + // linalg::copyMatrixSubset(_jac, mask, mask, jacRowOffset, 0, jacobianMatrix); + // + // // Construct right hand side + // linalg::selectVectorSubset(sensYdot + localQOffset, mask, rhs); + // + // // Zero out masked elements + // std::copy_n(sensY + localQOffset - idxr.strideColLiquid(), idxr.strideColCell(), maskedMultiplier); + // linalg::fillVectorSubset(maskedMultiplier + _disc.nComp, mask, 0.0); + // + // // Assemble right hand side + // _jac.submatrixMultiplyVector(maskedMultiplier, jacRowOffset, -static_cast(_disc.nComp), _disc.strideBound, idxr.strideColCell(), rhsUnmasked); + // linalg::vectorSubsetAdd(rhsUnmasked, mask, -1.0, 1.0, rhs); + // + // // Precondition + // double* const scaleFactors = _tempState + idxr.offsetC() + col * idxr.strideColCell(); + // jacobianMatrix.rowScaleFactors(scaleFactors); + // jacobianMatrix.scaleRows(scaleFactors); + // + // // Solve + // jacobianMatrix.factorize(); + // jacobianMatrix.solve(scaleFactors, rhs); + // + // // Write back + // linalg::applyVectorSubset(rhs, mask, sensY + localQOffset); + // } CADET_PARFOR_END; + // } + // + // // Step 2: Compute the correct time derivative of the state vector + // + // // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in sensYdot + // multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, sensYdot); + // + // // Note that we have correctly negated the right hand side + // + // //_jacDisc.setAll(0.0); + // _jacDisc.setZero(); + // + // // Handle transport equations (dc_i / dt terms) + // // TODO ! + // //_convDispOp.addTimeDerivativeToJacobian(1.0, _jacDisc); + // + // const double invBeta = 1.0 / static_cast(_totalPorosity) - 1.0; + // for (unsigned int col = 0; col < _disc.nCol; ++col) + // { + // // Assemble + // //linalg::FactorizableBandMatrix::RowIterator jac = _jacDisc.row(idxr.strideColCell() * col); + // + // // Mobile and solid phase (advances jac accordingly) + // addTimeDerivativeToJacobianNode(jac, idxr, 1.0, invBeta); + // + // // Iterator jac has already been advanced to next shell + // + // // Overwrite rows corresponding to algebraic equations with the Jacobian and set right hand side to 0 + // if (_binding[0]->hasQuasiStationaryReactions()) + // { + // // Get iterators to beginning of solid phase + // linalg::BandMatrix::RowIterator jacSolidOrig = _jac.row(idxr.strideColCell() * col + idxr.strideColLiquid()); + // linalg::FactorizableBandMatrix::RowIterator jacSolid = jac - idxr.strideColLiquid(); + // + // int const* const mask = _binding[0]->reactionQuasiStationarity(); + // double* const qShellDot = sensYdot + idxr.offsetC() + idxr.strideColCell() * col + idxr.strideColLiquid(); + // + // // Copy row from original Jacobian and set right hand side + // for (unsigned int i = 0; i < _disc.strideBound; ++i, ++jacSolid, ++jacSolidOrig) + // { + // if (!mask[i]) + // continue; + // + // jacSolid.copyRowFrom(jacSolidOrig); + // + // // Right hand side is -\frac{\partial^2 res(t, y, \dot{y})}{\partial p \partial t} + // // If the residual is not explicitly depending on time, this expression is 0 + // // @todo This is wrong if external functions are used. Take that into account! + // qShellDot[i] = 0.0; + // } + // } + // } + // + // // Precondition + // double* const scaleFactors = _tempState + idxr.offsetC(); + // _jacDisc.rowScaleFactors(scaleFactors); + // _jacDisc.scaleRows(scaleFactors); + // + // // Factorize + // const bool result = _jacDisc.factorize(); + // if (!result) + // { + // LOG(Error) << "Factorize() failed for par block"; + // } + // + // const bool result2 = _jacDisc.solve(scaleFactors, sensYdot + idxr.offsetC()); + // if (!result2) + // { + // LOG(Error) << "Solve() failed for par block"; + // } + // } + } + + /** + * @brief Computes approximately / partially consistent initial values and time derivatives of sensitivity subsystems + * @details Given the DAE \f[ F(t, y, \dot{y}) = 0, \f] and initial values \f$ y_0 \f$ and \f$ \dot{y}_0 \f$, + * the sensitivity system for a parameter @f$ p @f$ reads + * \f[ \frac{\partial F}{\partial y}(t, y, \dot{y}) s + \frac{\partial F}{\partial \dot{y}}(t, y, \dot{y}) \dot{s} + \frac{\partial F}{\partial p}(t, y, \dot{y}) = 0. \f] + * The initial values of this linear DAE, @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p} @f$ + * have to be consistent with the sensitivity DAE. This functions updates the initial sensitivity\f$ s_0 \f$ and overwrites the time + * derivative \f$ \dot{s}_0 \f$ such that they are consistent. + * + * The process follows closely the one of leanConsistentInitialConditions() and, in fact, is a linearized version of it. + * This is necessary because the initial conditions of the sensitivity system \f$ s_0 \f$ and \f$ \dot{s}_0 \f$ are + * related to the initial conditions \f$ y_0 \f$ and \f$ \dot{y}_0 \f$ of the original DAE by differentiating them + * with respect to @f$ p @f$: @f$ s_0 = \frac{\partial y_0}{\partial p} @f$ and @f$ \dot{s}_0 = \frac{\partial \dot{y}_0}{\partial p}. @f$ + *
    + *
  1. Keep state and time derivative vectors as they are (i.e., do not solve algebraic equations).
  2. + *
  3. Compute the time derivatives of the state @f$ \dot{y} @f$ such that the residual is 0 for the + * mobile phase variables.
  4. + *
+ * This function requires the parameter sensitivities to be computed beforehand and up-to-date Jacobians. + * @param [in] simTime Simulation time information (time point, section index, pre-factor of time derivatives) + * @param [in] simState Consistent state of the simulation (state vector and its time derivative) + * @param [in,out] vecSensY Sensitivity subsystem state vectors + * @param [in,out] vecSensYdot Time derivative state vectors of the sensitivity subsystems to be initialized + * @param [in] adRes Pointer to residual vector of AD datatypes with parameter sensitivities + * @todo Decrease amount of allocated memory by partially using temporary vectors (state and Schur complement) + */ + void LumpedRateModelWithoutPoresDG::leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem) + { + BENCH_SCOPE(_timerConsistentInit); + + Indexer idxr(_disc); + + for (std::size_t param = 0; param < vecSensY.size(); ++param) + { + double* const sensY = vecSensY[param]; + double* const sensYdot = vecSensYdot[param]; + + // Copy parameter derivative from AD to tempState and negate it + for (unsigned int col = 0; col < _disc.nCol; ++col) + { + const unsigned int localOffset = idxr.offsetC() + col * idxr.strideColCell(); + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + _tempState[localOffset + comp] = -adRes[localOffset + comp].getADValue(param); + } + } + + // Step 2: Compute the correct time derivative of the state vector + + // Compute right hand side by adding -dF / dy * s = -J * s to -dF / dp which is already stored in _tempState + multiplyWithJacobian(simTime, simState, sensY, -1.0, 1.0, _tempState); + + const double invBeta = (1.0 / static_cast(_totalPorosity) - 1.0); + for (unsigned int col = 0; col < _disc.nCol; ++col) + { + // Offset to current cell's c and q variables + const unsigned int localOffset = idxr.offsetC() + col * idxr.strideColCell(); + const unsigned int localOffsetQ = localOffset + idxr.strideColLiquid(); + + for (unsigned int comp = 0; comp < _disc.nComp; ++comp) + { + // dq_{i,j} / dt is assumed to be fixed, so bring it on the right hand side + for (unsigned int i = 0; i < _disc.nBound[comp]; ++i) + { + _tempState[localOffset + comp] -= invBeta * sensYdot[localOffsetQ + _disc.boundOffset[comp] + i]; + } + + sensYdot[localOffset + comp] = _tempState[localOffset + comp]; + } + } + } + } + + bool LumpedRateModelWithoutPoresDG::setParameter(const ParameterId& pId, double value) + { + if (_convDispOp.setParameter(pId, value)) + return true; + + return UnitOperationBase::setParameter(pId, value); + } + + void LumpedRateModelWithoutPoresDG::setSensitiveParameterValue(const ParameterId& pId, double value) + { + if (_convDispOp.setSensitiveParameterValue(_sensParams, pId, value)) + return; + + UnitOperationBase::setSensitiveParameterValue(pId, value); + } + + bool LumpedRateModelWithoutPoresDG::setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue) + { + if (_convDispOp.setSensitiveParameter(_sensParams, pId, adDirection, adValue)) + { + LOG(Debug) << "Found parameter " << pId << ": Dir " << adDirection << " is set to " << adValue; + return true; + } + + return UnitOperationBase::setSensitiveParameter(pId, adDirection, adValue); + } + + + int LumpedRateModelWithoutPoresDG::Exporter::writeMobilePhase(double* buffer) const + { + const int stride = _idx.strideColNode(); + double const* ptr = _data + _idx.offsetC(); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + std::copy_n(ptr, _disc.nComp, buffer); + buffer += _disc.nComp; + ptr += stride; + } + return _disc.nPoints * _disc.nComp; + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeSolidPhase(double* buffer) const + { + const int stride = _idx.strideColNode(); + double const* ptr = _data + _idx.offsetC() + _idx.strideColLiquid(); + for (unsigned int i = 0; i < _disc.nPoints; ++i) + { + std::copy_n(ptr, _disc.strideBound, buffer); + buffer += _disc.strideBound; + ptr += stride; + } + return _disc.nPoints * _disc.strideBound; + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeSolidPhase(unsigned int parType, double* buffer) const + { + cadet_assert(parType == 0); + return writeSolidPhase(buffer); + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeInlet(unsigned int port, double* buffer) const + { + cadet_assert(port == 0); + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeInlet(double* buffer) const + { + std::copy_n(_data, _disc.nComp, buffer); + return _disc.nComp; + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeOutlet(unsigned int port, double* buffer) const + { + cadet_assert(port == 0); + + if (_model._convDispOp.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; + } + + int LumpedRateModelWithoutPoresDG::Exporter::writeOutlet(double* buffer) const + { + if (_model._convDispOp.currentVelocity() >= 0) + std::copy_n(&_idx.c(_data, _disc.nPoints - 1, 0), _disc.nComp, buffer); + else + std::copy_n(&_idx.c(_data, 0, 0), _disc.nComp, buffer); + + return _disc.nComp; + } + + void registerLumpedRateModelWithoutPoresDG(std::unordered_map>& models) + { + models[LumpedRateModelWithoutPoresDG::identifier()] = [](UnitOpIdx uoId) { return new LumpedRateModelWithoutPoresDG(uoId); }; + models["LRM_DG"] = [](UnitOpIdx uoId) { return new LumpedRateModelWithoutPoresDG(uoId); }; + models["DPFR_DG"] = [](UnitOpIdx uoId) { return new LumpedRateModelWithoutPoresDG(uoId); }; + } + + } // namespace model + +} // namespace cadet diff --git a/src/libcadet/model/LumpedRateModelWithoutPoresDG.hpp b/src/libcadet/model/LumpedRateModelWithoutPoresDG.hpp new file mode 100644 index 000000000..5a76428bd --- /dev/null +++ b/src/libcadet/model/LumpedRateModelWithoutPoresDG.hpp @@ -0,0 +1,1932 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +/** + * @file + * Defines the lumped rate model without pores (LRM) using a Discontinous Galerkin (DG) scheme. + */ + +#ifndef LIBCADET_LUMPEDRATEMODELWITHOUTPORESDG_HPP_ +#define LIBCADET_LUMPEDRATEMODELWITHOUTPORESDG_HPP_ + +#include "BindingModel.hpp" +#include "ParallelSupport.hpp" + +#include "UnitOperationBase.hpp" +#include "cadet/SolutionExporter.hpp" +#include "model/parts/ConvectionDispersionOperator.hpp" +#include "AutoDiff.hpp" +#include "linalg/SparseMatrix.hpp" +#include "linalg/BandMatrix.hpp" +#include "linalg/BandedEigenSparseRowIterator.hpp" +#include "linalg/Gmres.hpp" +#include "Memory.hpp" +#include "model/ModelUtils.hpp" + +#include +#include + +#include "Benchmark.hpp" +#include +#include + +using namespace Eigen; + +namespace cadet +{ + + namespace model + { + + /** + * @brief Lumped rate model of liquid column chromatography without pores + * @details See @cite Guiochon2006, @cite Gu1995, @cite Felinger2004 + * + * @f[\begin{align} + \frac{\partial c_i}{\partial t} + \frac{1 - \varepsilon_t}{\varepsilon_t} \frac{\partial q_{i}}{\partial t} &= - u \frac{\partial c_i}{\partial z} + D_{\text{ax},i} \frac{\partial^2 c_i}{\partial z^2} \\ + a \frac{\partial q_i}{\partial t} &= f_{\text{iso}}(c, q) + \end{align} @f] + * Danckwerts boundary conditions (see @cite Danckwerts1953) + @f[ \begin{align} + u c_{\text{in},i}(t) &= u c_i(t,0) - D_{\text{ax},i} \frac{\partial c_i}{\partial z}(t,0) \\ + \frac{\partial c_i}{\partial z}(t,L) &= 0 + \end{align} @f] + * Methods are described in @cite Breuer2023 (DGSEM discretization), @cite Puttmann2013 @cite Puttmann2016 (forward sensitivities, AD, band compression) + */ + class LumpedRateModelWithoutPoresDG : public UnitOperationBase + { + public: + + LumpedRateModelWithoutPoresDG(UnitOpIdx unitOpIdx); + virtual ~LumpedRateModelWithoutPoresDG() CADET_NOEXCEPT; + + virtual unsigned int numDofs() const CADET_NOEXCEPT; + virtual unsigned int numPureDofs() const CADET_NOEXCEPT; + virtual bool usesAD() const CADET_NOEXCEPT; + virtual unsigned int requiredADdirs() const CADET_NOEXCEPT; + + virtual UnitOpIdx unitOperationId() const CADET_NOEXCEPT { return _unitOpIdx; } + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual void setFlowRates(active const* in, active const* out) CADET_NOEXCEPT; + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual bool canAccumulate() const CADET_NOEXCEPT { return false; } + + static const char* identifier() { return "LUMPED_RATE_MODEL_WITHOUT_PORES_DG"; } + virtual const char* unitOperationName() const CADET_NOEXCEPT { return identifier(); } + + virtual bool configureModelDiscretization(IParameterProvider& paramProvider, IConfigHelper& helper); + virtual bool configure(IParameterProvider& paramProvider); + virtual void notifyDiscontinuousSectionTransition(double t, unsigned int secIdx, const ConstSimulationState& simState, const AdJacobianParams& adJac); + + virtual void useAnalyticJacobian(const bool analyticJac); + + virtual void reportSolution(ISolutionRecorder& recorder, double const* const solution) const; + virtual void reportSolutionStructure(ISolutionRecorder& recorder) const; + + virtual int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdAdOnly(const SimulationTime& simTime, const ConstSimulationState& simState, active* const adRes, util::ThreadLocalStorage& threadLocalMem); + virtual int residualSensFwdWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem); + + virtual int residualSensFwdCombine(const SimulationTime& simTime, const ConstSimulationState& simState, + const std::vector& yS, const std::vector& ySdot, const std::vector& resS, active const* adRes, + double* const tmp1, double* const tmp2, double* const tmp3); + + virtual int linearSolve(double t, double alpha, double tol, double* const rhs, double const* const weight, + const ConstSimulationState& simState); + + virtual void prepareADvectors(const AdJacobianParams& adJac) const; + + virtual void applyInitialCondition(const SimulationState& simState) const; + virtual void readInitialCondition(IParameterProvider& paramProvider); + + virtual void consistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void consistentInitialTimeDerivative(const SimulationTime& simTime, double const* vecStateY, double* const vecStateYdot, util::ThreadLocalStorage& threadLocalMem); + + virtual void initializeSensitivityStates(const std::vector& vecSensY) const; + virtual void consistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialState(const SimulationTime& simTime, double* const vecStateY, const AdJacobianParams& adJac, double errorTol, util::ThreadLocalStorage& threadLocalMem); + virtual void leanConsistentInitialTimeDerivative(double t, double const* const vecStateY, double* const vecStateYdot, double* const res, util::ThreadLocalStorage& threadLocalMem); + + virtual void leanConsistentInitialSensitivity(const SimulationTime& simTime, const ConstSimulationState& simState, + std::vector& vecSensY, std::vector& vecSensYdot, active const* const adRes, util::ThreadLocalStorage& threadLocalMem); + + virtual bool hasInlet() const CADET_NOEXCEPT { return true; } + virtual bool hasOutlet() const CADET_NOEXCEPT { return true; } + + virtual unsigned int localOutletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localOutletComponentStride(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentIndex(unsigned int port) const CADET_NOEXCEPT; + virtual unsigned int localInletComponentStride(unsigned int port) const CADET_NOEXCEPT; + + virtual void setExternalFunctions(IExternalFunction** extFuns, unsigned int size); + virtual void setSectionTimes(double const* secTimes, bool const* secContinuity, unsigned int nSections) { } + + virtual void expandErrorTol(double const* errorSpec, unsigned int errorSpecSize, double* expandOut); + + virtual void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double alpha, double beta, double* ret); + virtual void multiplyWithDerivativeJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* sDot, double* ret); + + inline void multiplyWithJacobian(const SimulationTime& simTime, const ConstSimulationState& simState, double const* yS, double* ret) + { + multiplyWithJacobian(simTime, simState, yS, 1.0, 0.0, ret); + } + + virtual bool setParameter(const ParameterId& pId, double value); + virtual bool setSensitiveParameter(const ParameterId& pId, unsigned int adDirection, double adValue); + virtual void setSensitiveParameterValue(const ParameterId& id, double value); + + virtual unsigned int threadLocalMemorySize() const CADET_NOEXCEPT; + + +#ifdef CADET_BENCHMARK_MODE + virtual std::vector benchmarkTimings() const + { + return std::vector({ + static_cast(numDofs()), + _timerResidual.totalElapsedTime(), + _timerResidualPar.totalElapsedTime(), + _timerResidualSens.totalElapsedTime(), + _timerResidualSensPar.totalElapsedTime(), + _timerConsistentInit.totalElapsedTime(), + _timerConsistentInitPar.totalElapsedTime(), + _timerLinearSolve.totalElapsedTime() + }); + } + + virtual char const* const* benchmarkDescriptions() const + { + static const char* const desc[] = { + "DOFs", + "Residual", + "ResidualPar", + "ResidualSens", + "ResidualSensPar", + "ConsistentInit", + "ConsistentInitPar", + "LinearSolve" + }; + return desc; + } +#endif + + protected: + + class Indexer; + + int residual(const SimulationTime& simTime, const ConstSimulationState& simState, double* const res, const AdJacobianParams& adJac, util::ThreadLocalStorage& threadLocalMem, bool updateJacobian, bool paramSensitivity); + + template + int residualImpl(double t, unsigned int secIdx, StateType const* const y, double const* const yDot, ResidualType* const res, util::ThreadLocalStorage& threadLocalMem); + + void extractJacobianFromAD(active const* const adRes, unsigned int adDirOffset); + + void assembleDiscretizedJacobian(double alpha, const Indexer& idxr); + void addTimeDerivativeToJacobianNode(linalg::BandedEigenSparseRowIterator& jac, const Indexer& idxr, double alpha, double invBetaP) const; + +#ifdef CADET_CHECK_ANALYTIC_JACOBIAN + void checkAnalyticJacobianAgainstAd(active const* const adRes, unsigned int adDirOffset) const; +#endif + + class Discretization + { + public: + unsigned int nComp; //!< Number of components + unsigned int nCol; //!< Number of column cells + unsigned int polyDeg; //!< polynomial degree + unsigned int nNodes; //!< Number of nodes per cell + unsigned int nPoints; //!< Number of discrete Points + double deltaZ; //!< cell spacing + bool exactInt; //!< 1 for exact integration, 0 for LGL quadrature + Eigen::VectorXd nodes; //!< Array with positions of nodes in reference element + Eigen::MatrixXd polyDerM; //!< Array with polynomial derivative Matrix + Eigen::VectorXd invWeights; //!< Array with weights for numerical quadrature of size nNodes + Eigen::MatrixXd invMM; //!< dense inverse mass matrix for exact integration + + Eigen::MatrixXd* DGjacAxDispBlocks; //!< axial dispersion blocks of DG jacobian (unique blocks only) + Eigen::MatrixXd DGjacAxConvBlock; //!< axial convection block of DG jacobian + + unsigned int* nBound; //!< Array with number of bound states for each component + unsigned int* boundOffset; //!< Array with offset to the first bound state of each component in the solid phase + unsigned int strideBound; //!< Total number of bound states + + // vgl. convDispOperator + Eigen::VectorXd dispersion; //!< Column dispersion (may be section and component dependent) + bool _dispersionCompIndep; //!< Determines whether dispersion is component independent + double velocity; //!< Interstitial velocity (may be section dependent) \f$ u \f$ + int curSection; //!< current section index + + double length_; + double porosity; + + Eigen::VectorXd g; //!< auxiliary variable + Eigen::VectorXd h; //!< auxiliary substitute + Eigen::VectorXd surfaceFlux; //!< stores the surface flux values + Eigen::Vector4d boundary; //!< stores the boundary values from Danckwert boundary conditions + + bool newStaticJac; //!< determines wether static analytical jacobian needs to be computed (every section) + + /** + * @brief computes LGL nodes, integration weights, polynomial derivative matrix + */ + void initializeDG() { + + nNodes = polyDeg + 1; + nPoints = nNodes * nCol; + // Allocate space for DG discretization + nodes.resize(nNodes); + nodes.setZero(); + invWeights.resize(nNodes); + invWeights.setZero(); + polyDerM.resize(nNodes, nNodes); + polyDerM.setZero(); + invMM.resize(nNodes, nNodes); + invMM.setZero(); + + g.resize(nPoints); + g.setZero(); + h.resize(nPoints); + h.setZero(); + boundary.setZero(); + surfaceFlux.resize(nCol + 1); + surfaceFlux.setZero(); + + newStaticJac = true; + + lglNodesWeights(); + invMMatrix(); + derivativeMatrix(); + } + + void initializeDGjac() { + DGjacAxDispBlocks = new MatrixXd[(exactInt ? std::min(nCol, 5u) : std::min(nCol, 3u))]; + // we only need unique dispersion blocks, which are given by cells 1, 2, nCol for inexact integration DG and by cells 1, 2, 3, nCol-1, nCol for eaxct integration DG + DGjacAxDispBlocks[0] = DGjacobianDispBlock(1); + if (nCol > 1) + DGjacAxDispBlocks[1] = DGjacobianDispBlock(2); + if (nCol > 2 && exactInt) + DGjacAxDispBlocks[2] = DGjacobianDispBlock(3); + else if (nCol > 2 && !exactInt) + DGjacAxDispBlocks[2] = DGjacobianDispBlock(nCol); + if (exactInt && nCol > 3) + DGjacAxDispBlocks[3] = DGjacobianDispBlock(std::max(4u, nCol - 1u)); + if (exactInt && nCol > 4) + DGjacAxDispBlocks[4] = DGjacobianDispBlock(nCol); + + DGjacAxConvBlock = DGjacobianConvBlock(); + } + + private: + + /* =================================================================================== + * Polynomial Basis operators and auxiliary functions + * =================================================================================== */ + + /** + * @brief computes the Legendre polynomial L_N and q = L_N+1 - L_N-2 and q' at point x + * @param [in] polyDeg polynomial degree of spatial Discretization + * @param [in] x evaluation point + * @param [in] L <- L(x) + * @param [in] q <- q(x) = L_N+1 (x) - L_N-2(x) + * @param [in] qder <- q'(x) = [L_N+1 (x) - L_N-2(x)]' + */ + void qAndL(const double x, double& L, double& q, double& qder) { + // auxiliary variables (Legendre polynomials) + double L_2 = 1.0; + double L_1 = x; + double Lder_2 = 0.0; + double Lder_1 = 1.0; + double Lder = 0.0; + for (double k = 2; k <= polyDeg; k++) { // note that this function is only called for polyDeg >= 2. + L = ((2 * k - 1) * x * L_1 - (k - 1) * L_2) / k; + Lder = Lder_2 + (2 * k - 1) * L_1; + L_2 = L_1; + L_1 = L; + Lder_2 = Lder_1; + Lder_1 = Lder; + } + q = ((2.0 * polyDeg + 1) * x * L - polyDeg * L_2) / (polyDeg + 1.0) - L_2; + qder = Lder_1 + (2.0 * polyDeg + 1) * L_1 - Lder_2; + } + + /** + * @brief computes the Legendre-Gauss-Lobatto nodes and (inverse) quadrature weights + * @detail inexact LGL-quadrature leads to a diagonal mass matrix (mass lumping), defined by the quadrature weights + */ + void lglNodesWeights() { + + const double pi = 3.1415926535897932384626434; + + // tolerance and max #iterations for Newton iteration + int nIterations = 10; + double tolerance = 1e-15; + // Legendre polynomial and derivative + double L = 0; + double q = 0; + double qder = 0; + switch (polyDeg) { + case 0: + throw std::invalid_argument("Polynomial degree must be at least 1 !"); + break; + case 1: + nodes[0] = -1; + invWeights[0] = 1; + nodes[1] = 1; + invWeights[1] = 1; + break; + default: + nodes[0] = -1; + nodes[polyDeg] = 1; + invWeights[0] = 2.0 / (polyDeg * (polyDeg + 1.0)); + invWeights[polyDeg] = invWeights[0]; + // use symmetrie, only compute half of points and weights + for (unsigned int j = 1; j <= floor((polyDeg + 1) / 2) - 1; j++) { + // first guess for Newton iteration + nodes[j] = -cos(pi * (j + 0.25) / polyDeg - 3 / (8.0 * polyDeg * pi * (j + 0.25))); + // Newton iteration to find roots of Legendre Polynomial + for (unsigned int k = 0; k <= nIterations; k++) { + qAndL(nodes[j], L, q, qder); + nodes[j] = nodes[j] - q / qder; + if (abs(q / qder) <= tolerance * abs(nodes[j])) { + break; + } + } + // calculate weights + qAndL(nodes[j], L, q, qder); + invWeights[j] = 2.0 / (polyDeg * (polyDeg + 1.0) * pow(L, 2.0)); + nodes[polyDeg - j] = -nodes[j]; // copy to second half of points and weights + invWeights[polyDeg - j] = invWeights[j]; + } + } + if (polyDeg % 2 == 0) { // for even polyDeg we have an odd number of points which include 0.0 + qAndL(0.0, L, q, qder); + nodes[polyDeg / 2] = 0; + invWeights[polyDeg / 2] = 2.0 / (polyDeg * (polyDeg + 1.0) * pow(L, 2.0)); + } + // inverse the weights + invWeights = invWeights.cwiseInverse(); + } + + /** + * @brief computation of barycentric weights for fast polynomial evaluation + * @param [in] baryWeights vector to store barycentric weights. Must already be initialized with ones! + */ + void barycentricWeights(Eigen::VectorXd& baryWeights) { + for (unsigned int j = 1; j <= polyDeg; j++) { + for (unsigned int k = 0; k <= j - 1; k++) { + baryWeights[k] = baryWeights[k] * (nodes[k] - nodes[j]) * 1.0; + baryWeights[j] = baryWeights[j] * (nodes[j] - nodes[k]) * 1.0; + } + } + for (unsigned int j = 0; j <= polyDeg; j++) { + baryWeights[j] = 1 / baryWeights[j]; + } + } + + /** + * @brief computation of nodal (lagrange) polynomial derivative matrix + */ + void derivativeMatrix() { + Eigen::VectorXd baryWeights = Eigen::VectorXd::Ones(polyDeg + 1u); + barycentricWeights(baryWeights); + for (unsigned int i = 0; i <= polyDeg; i++) { + for (unsigned int j = 0; j <= polyDeg; j++) { + if (i != j) { + polyDerM(i, j) = baryWeights[j] / (baryWeights[i] * (nodes[i] - nodes[j])); + polyDerM(i, i) += -polyDerM(i, j); + } + } + } + } + + /** + * @brief factor to normalize legendre polynomials + */ + double orthonFactor(int polyDeg) { + + double n = static_cast (polyDeg); + // alpha = beta = 0 to get legendre polynomials as special case from jacobi polynomials. + double a = 0.0; + double b = 0.0; + return std::sqrt(((2.0 * n + a + b + 1.0) * std::tgamma(n + 1.0) * std::tgamma(n + a + b + 1.0)) + / (std::pow(2.0, a + b + 1.0) * std::tgamma(n + a + 1.0) * std::tgamma(n + b + 1.0))); + } + + /** + * @brief calculates the Vandermonde matrix of the normalized legendre polynomials + */ + Eigen::MatrixXd getVandermonde_LEGENDRE() { + + Eigen::MatrixXd V(nodes.size(), nodes.size()); + + double alpha = 0.0; + double beta = 0.0; + + // degree 0 + V.block(0, 0, nNodes, 1) = VectorXd::Ones(nNodes) * orthonFactor(0); + // degree 1 + for (int node = 0; node < static_cast(nNodes); node++) { + V(node, 1) = nodes[node] * orthonFactor(1); + } + + for (int deg = 2; deg <= static_cast(polyDeg); deg++) { + + for (int node = 0; node < static_cast(nNodes); node++) { + + double orthn_1 = orthonFactor(deg) / orthonFactor(deg - 1); + double orthn_2 = orthonFactor(deg) / orthonFactor(deg - 2); + + double fac_1 = ((2.0 * deg - 1.0) * 2.0 * deg * (2.0 * deg - 2.0) * nodes[node]) / (2.0 * deg * deg * (2.0 * deg - 2.0)); + double fac_2 = (2.0 * (deg - 1.0) * (deg - 1.0) * 2.0 * deg) / (2.0 * deg * deg * (2.0 * deg - 2.0)); + + V(node, deg) = orthn_1 * fac_1 * V(node, deg - 1) - orthn_2 * fac_2 * V(node, deg - 2); + + } + + } + + return V; + } + /** + * @brief calculates mass matrix for exact polynomial integration + * @detail exact polynomial integration leads to a full mass matrix + */ + void invMMatrix() { + invMM = (getVandermonde_LEGENDRE() * (getVandermonde_LEGENDRE().transpose())); + } + /** + * @brief calculates the convection part of the DG jacobian + */ + MatrixXd DGjacobianConvBlock() { + + // Convection block [ d RHS_conv / d c ], additionally depends on upwind flux part from corresponding neighbour cell + MatrixXd convBlock = MatrixXd::Zero(nNodes, nNodes + 1); + + if (velocity >= 0.0) { // forward flow -> Convection block additionally depends on last entry of previous cell + convBlock.block(0, 1, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, 0, nNodes, 1) += invMM.block(0, 0, nNodes, 1); + convBlock.block(0, 1, nNodes, 1) -= invMM.block(0, 0, nNodes, 1); + } + else { + convBlock(0, 0) += invWeights[0]; + convBlock(0, 1) -= invWeights[0]; + } + } + else { // backward flow -> Convection block additionally depends on first entry of subsequent cell + convBlock.block(0, 0, nNodes, nNodes) -= polyDerM; + + if (exactInt) { + convBlock.block(0, nNodes - 1, nNodes, 1) += invMM.block(0, nNodes - 1, nNodes, 1); + convBlock.block(0, nNodes, nNodes, 1) -= invMM.block(0, nNodes - 1, nNodes, 1); + } + else { + convBlock(nNodes - 1, nNodes - 1) += invWeights[nNodes - 1]; + convBlock(nNodes - 1, nNodes) -= invWeights[nNodes - 1]; + } + } + convBlock *= 2 / deltaZ; + + return -convBlock; // *-1 for residual + } + /** + * @brief calculates the DG Jacobian auxiliary block + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd getGBlock(unsigned int cellIdx) { + + // Auxiliary Block [ d g(c) / d c ], additionally depends on boundary entries of neighbouring cells + MatrixXd gBlock = MatrixXd::Zero(nNodes, nNodes + 2); + gBlock.block(0, 1, nNodes, nNodes) = polyDerM; + if (exactInt) { + if (cellIdx != 1 && cellIdx != nCol) { + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == 1) { // left + if (cellIdx == nCol) + return gBlock * 2 / deltaZ; + ; + gBlock.block(0, nNodes, nNodes, 1) -= 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + gBlock.block(0, nNodes + 1, nNodes, 1) += 0.5 * invMM.block(0, nNodes - 1, nNodes, 1); + } + else if (cellIdx == nCol) { // right + gBlock.block(0, 0, nNodes, 1) -= 0.5 * invMM.block(0, 0, nNodes, 1); + gBlock.block(0, 1, nNodes, 1) += 0.5 * invMM.block(0, 0, nNodes, 1); + } + else if (cellIdx == 0 || cellIdx == nCol + 1) { + gBlock.setZero(); + } + gBlock *= 2 / deltaZ; + } + else { + if (cellIdx == 0 || cellIdx == nCol + 1) + return MatrixXd::Zero(nNodes, nNodes + 2); + + gBlock(0, 0) -= 0.5 * invWeights[0]; + gBlock(0, 1) += 0.5 * invWeights[0]; + gBlock(nNodes - 1, nNodes) -= 0.5 * invWeights[nNodes - 1]; + gBlock(nNodes - 1, nNodes + 1) += 0.5 * invWeights[nNodes - 1]; + gBlock *= 2 / deltaZ; + + if (cellIdx == 1) { + // adjust auxiliary Block [ d g(c) / d c ] for left boundary cell + gBlock(0, 1) -= 0.5 * invWeights[0] * 2 / deltaZ; + if (cellIdx == nCol) { // adjust for special case one cell + gBlock(0, 0) += 0.5 * invWeights[0] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes + 1) -= 0.5 * invWeights[nNodes - 1] * 2 / deltaZ; + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + else if (cellIdx == nCol) { + // adjust auxiliary Block [ d g(c) / d c ] for right boundary cell + gBlock(nNodes - 1, nNodes) += 0.5 * invWeights[polyDeg] * 2 / deltaZ; + } + } + + return gBlock; + } + /** + * @brief calculates the num. flux part of a dispersion DG Jacobian block + * @param [in] cellIdx cell index + * @param [in] leftG left neighbour auxiliary block + * @param [in] middleG neighbour auxiliary block + * @param [in] rightG neighbour auxiliary block + */ + Eigen::MatrixXd auxBlockGstar(unsigned int cellIdx, MatrixXd leftG, MatrixXd middleG, MatrixXd rightG) { + + // auxiliary block [ d g^* / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + MatrixXd gStarDC = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + // NOTE: N = polyDeg + // indices gStarDC : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),... , -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + // auxiliary block [d g^* / d c] + if (cellIdx != 1) { + gStarDC.block(0, nNodes, 1, nNodes + 2) += middleG.block(0, 0, 1, nNodes + 2); + gStarDC.block(0, 0, 1, nNodes + 2) += leftG.block(nNodes - 1, 0, 1, nNodes + 2); + } + if (cellIdx != nCol) { + gStarDC.block(nNodes - 1, nNodes, 1, nNodes + 2) += middleG.block(nNodes - 1, 0, 1, nNodes + 2); + gStarDC.block(nNodes - 1, 2 * nNodes, 1, nNodes + 2) += rightG.block(0, 0, 1, nNodes + 2); + } + gStarDC *= 0.5; + + return gStarDC; + } + + Eigen::MatrixXd getBMatrix() { + + MatrixXd B = MatrixXd::Zero(nNodes, nNodes); + B(0, 0) = -1.0; + B(nNodes - 1, nNodes - 1) = 1.0; + + return B; + } + + /** + * @brief calculates the dispersion part of the DG jacobian + * @param [in] exInt true if exact integration DG scheme + * @param [in] cellIdx cell index + */ + MatrixXd DGjacobianDispBlock(unsigned int cellIdx) { + + int offC = 0; // inlet DOFs not included in Jacobian + + MatrixXd dispBlock; + + if (exactInt) { + + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes + 2); + + MatrixXd B = getBMatrix(); // "Lifting" matrix + MatrixXd gBlock = getGBlock(cellIdx); // current cell auxiliary block matrix + MatrixXd gStarDC = auxBlockGstar(cellIdx, getGBlock(cellIdx - 1), gBlock, getGBlock(cellIdx + 1)); // Numerical flux block + + // indices dispBlock : 0 , 1 , ..., nNodes; nNodes+1, ..., 2 * nNodes; 2*nNodes+1, ..., 3 * nNodes; 3*nNodes+1 + // derivative index j : -(N+1)-1, -(N+1),..., -1 ; 0 , ..., N ; N + 1 , ..., 2N + 2 ; 2(N+1) +1 + dispBlock.block(0, nNodes, nNodes, nNodes + 2) += polyDerM * gBlock - invMM * B * gBlock; + dispBlock += invMM * B * gStarDC; + dispBlock *= 2 / deltaZ; + } + else { // inexact integration collocation DGSEM + + dispBlock = MatrixXd::Zero(nNodes, 3 * nNodes); + MatrixXd GBlockLeft = getGBlock(cellIdx - 1); + MatrixXd GBlock = getGBlock(cellIdx); + MatrixXd GBlockRight = getGBlock(cellIdx + 1); + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + // NOTE: N = polyDeg + // cell indices : 0 , ..., nNodes - 1; nNodes, ..., 2 * nNodes - 1; 2 * nNodes, ..., 3 * nNodes - 1 + // j : -N-1, ..., -1 ; 0 , ..., N ; N + 1, ..., 2N + 1 + dispBlock.block(0, nNodes - 1, nNodes, nNodes + 2) = polyDerM * GBlock; + + if (cellIdx > 1) { + dispBlock(0, nNodes - 1) += -invWeights[0] * (-0.5 * GBlock(0, 0) + 0.5 * GBlockLeft(nNodes - 1, nNodes)); // G_N,N i=0, j=-1 + dispBlock(0, nNodes) += -invWeights[0] * (-0.5 * GBlock(0, 1) + 0.5 * GBlockLeft(nNodes - 1, nNodes + 1)); // G_N,N+1 i=0, j=0 + dispBlock.block(0, nNodes + 1, 1, nNodes) += -invWeights[0] * (-0.5 * GBlock.block(0, 2, 1, nNodes)); // G_i,j i=0, j=1,...,N+1 + dispBlock.block(0, 0, 1, nNodes - 1) += -invWeights[0] * (0.5 * GBlockLeft.block(nNodes - 1, 1, 1, nNodes - 1)); // G_N,j+N+1 i=0, j=-N-1,...,-2 + } + else if (cellIdx == 1) { // left boundary cell + dispBlock.block(0, nNodes - 1, 1, nNodes + 2) += -invWeights[0] * (-GBlock.block(0, 0, 1, nNodes + 2)); // G_N,N i=0, j=-1,...,N+1 + } + if (cellIdx < nCol) { + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock.block(nNodes - 1, 0, 1, nNodes)); // G_i,j+N+1 i=N, j=-1,...,N-1 + dispBlock(nNodes - 1, 2 * nNodes - 1) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes) + 0.5 * GBlockRight(0, 0)); // G_i,j i=N, j=N + dispBlock(nNodes - 1, 2 * nNodes) += invWeights[nNodes - 1] * (-0.5 * GBlock(nNodes - 1, nNodes + 1) + 0.5 * GBlockRight(0, 1)); // G_i,j i=N, j=N+1 + dispBlock.block(nNodes - 1, 2 * nNodes + 1, 1, nNodes - 1) += invWeights[nNodes - 1] * (0.5 * GBlockRight.block(0, 2, 1, nNodes - 1)); // G_0,j-N-1 i=N, j=N+2,...,2N+1 + } + else if (cellIdx == nCol) { // right boundary cell + dispBlock.block(nNodes - 1, nNodes - 1, 1, nNodes + 2) += invWeights[nNodes - 1] * (-GBlock.block(nNodes - 1, 0, 1, nNodes + 2)); // G_i,j+N+1 i=N, j=--1,...,N+1 + } + + dispBlock *= 2 / deltaZ; + } + + return -dispBlock; // *-1 for residual + } + }; + + Discretization _disc; //!< Discretization info + + // IExternalFunction* _extFun; //!< External function (owned by library user) + + // used as auxiliary supplier + parts::ConvectionDispersionOperatorBase _convDispOp; //!< Convection dispersion operator for interstitial volume transport + + // linear solver (Eigen lib) + Eigen::SparseLU> _linSolver; + //Eigen::BiCGSTAB, Eigen::DiagonalPreconditioner> solver; (needs _tempState, cant solve inplace) + + Eigen::SparseMatrix _jac; //!< Jacobian + Eigen::SparseMatrix _jacDisc; //!< Jacobian with time derivatives from BDF method + Eigen::MatrixXd _jacInlet; //!< Jacobian inlet DOF block matrix connects inlet DOFs to first bulk cells + //MatrixXd FDjac; // test purpose FD Jacobian + + active _totalPorosity; //!< Total porosity \f$ \varepsilon_t \f$ + + bool _analyticJac; //!< Determines whether AD or analytic Jacobians are used + unsigned int _jacobianAdDirs; //!< Number of AD seed vectors required for Jacobian computation + bool _factorizeJacobian; //!< Determines whether the Jacobian needs to be factorized + double* _tempState; //!< Temporary storage with the size of the state vector or larger if binding models require it + + std::vector _initC; //!< Liquid phase initial conditions + std::vector _initQ; //!< Solid phase initial conditions + std::vector _initState; //!< Initial conditions for state vector if given + std::vector _initStateDot; //!< Initial conditions for time derivative + + BENCH_TIMER(_timerResidual) + BENCH_TIMER(_timerResidualPar) + BENCH_TIMER(_timerResidualSens) + BENCH_TIMER(_timerResidualSensPar) + BENCH_TIMER(_timerConsistentInit) + BENCH_TIMER(_timerConsistentInitPar) + BENCH_TIMER(_timerLinearSolve) + + class Indexer + { + public: + Indexer(const Discretization& disc) : _disc(disc) { } + + // Strides + inline int strideColNode() const CADET_NOEXCEPT { return static_cast(_disc.nComp + _disc.strideBound); } + inline int strideColCell() const CADET_NOEXCEPT { return static_cast(_disc.nNodes * strideColNode()); } + inline int strideColComp() const CADET_NOEXCEPT { return 1; } + + inline int strideColLiquid() const CADET_NOEXCEPT { return static_cast(_disc.nComp); } + inline int strideColBound() const CADET_NOEXCEPT { return static_cast(_disc.strideBound); } + + // Offsets + inline int offsetC() const CADET_NOEXCEPT { return _disc.nComp; } + inline int offsetBoundComp(unsigned int comp) const CADET_NOEXCEPT { return _disc.boundOffset[comp]; } + + // Return pointer to first element of state variable in state vector + template inline real_t* c(real_t* const data) const { return data + offsetC(); } + template inline real_t const* c(real_t const* const data) const { return data + offsetC(); } + + template inline real_t* q(real_t* const data) const { return data + offsetC() + strideColLiquid(); } + template inline real_t const* q(real_t const* const data) const { return data + offsetC() + strideColLiquid(); } + + // Return specific variable in state vector + template inline real_t& c(real_t* const data, unsigned int node, unsigned int comp) const { return data[offsetC() + comp * strideColComp() + node * strideColNode()]; } + template inline const real_t& c(real_t const* const data, unsigned int node, unsigned int comp) const { return data[offsetC() + comp * strideColComp() + node * strideColNode()]; } + + protected: + const Discretization& _disc; + }; + + class Exporter : public ISolutionExporter + { + public: + + Exporter(const Discretization& disc, const LumpedRateModelWithoutPoresDG& model, double const* data) : _disc(disc), _idx(disc), _model(model), _data(data) { } + Exporter(const Discretization&& disc, const LumpedRateModelWithoutPoresDG& model, double const* data) = delete; + + virtual bool hasParticleFlux() const CADET_NOEXCEPT { return false; } + virtual bool hasParticleMobilePhase() const CADET_NOEXCEPT { return false; } + virtual bool hasSolidPhase() const CADET_NOEXCEPT { return _disc.strideBound > 0; } + virtual bool hasVolume() const CADET_NOEXCEPT { return false; } + virtual bool isParticleLumped() const CADET_NOEXCEPT { return false; } + virtual bool hasPrimaryExtent() const CADET_NOEXCEPT { return true; } + + virtual unsigned int numComponents() const CADET_NOEXCEPT { return _disc.nComp; } + virtual unsigned int numPrimaryCoordinates() const CADET_NOEXCEPT { return _disc.nPoints; } + virtual unsigned int numSecondaryCoordinates() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numInletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numOutletPorts() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numParticleTypes() const CADET_NOEXCEPT { return 1; } + virtual unsigned int numParticleShells(unsigned int parType) const CADET_NOEXCEPT { return 0; } + virtual unsigned int numBoundStates(unsigned int parType) const CADET_NOEXCEPT { return _disc.strideBound; } + virtual unsigned int numMobilePhaseDofs() const CADET_NOEXCEPT { return _disc.nComp * _disc.nPoints; } + virtual unsigned int numParticleMobilePhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return 0; } + virtual unsigned int numParticleMobilePhaseDofs() const CADET_NOEXCEPT { return 0; } + virtual unsigned int numSolidPhaseDofs(unsigned int parType) const CADET_NOEXCEPT { return _disc.strideBound * _disc.nPoints; } + virtual unsigned int numSolidPhaseDofs() const CADET_NOEXCEPT { return _disc.strideBound * _disc.nPoints; } + virtual unsigned int numParticleFluxDofs() const CADET_NOEXCEPT { return 0u; } + virtual unsigned int numVolumeDofs() const CADET_NOEXCEPT { return 0; } + + virtual int writeMobilePhase(double* buffer) const; + virtual int writeSolidPhase(double* buffer) const; + virtual int writeParticleMobilePhase(double* buffer) const { return 0; } + virtual int writeSolidPhase(unsigned int parType, double* buffer) const; + virtual int writeParticleMobilePhase(unsigned int parType, double* buffer) const { return 0; } + virtual int writeParticleFlux(double* buffer) const { return 0; } + virtual int writeParticleFlux(unsigned int parType, double* buffer) const { return 0; } + virtual int writeVolume(double* buffer) const { return 0; } + virtual int writeInlet(unsigned int port, double* buffer) const; + virtual int writeInlet(double* buffer) const; + virtual int writeOutlet(unsigned int port, double* buffer) const; + virtual int writeOutlet(double* buffer) const; + /** + * @brief calculates the physical node coordinates of the DG discretization with double! interface nodes + */ + virtual int writePrimaryCoordinates(double* coords) const + { + VectorXd x_l = VectorXd::LinSpaced(static_cast(_disc.nCol + 1u), 0.0, static_cast(_disc.length_)); + for (unsigned int i = 0; i < _disc.nCol; i++) { + for (unsigned int j = 0; j < _disc.nNodes; j++) { + // mapping + coords[i * _disc.nNodes + j] = x_l[i] + 0.5 * (_disc.length_ / static_cast(_disc.nCol)) * (1.0 + _disc.nodes[j]); + } + } + return _disc.nPoints; + } + + virtual int writeSecondaryCoordinates(double* coords) const { return 0; } + virtual int writeParticleCoordinates(unsigned int parType, double* coords) const { return 0; } + + protected: + const Discretization& _disc; + const Indexer _idx; + const LumpedRateModelWithoutPoresDG& _model; + double const* const _data; + }; + + /** + * @brief sets the current section index and section dependend velocity, dispersion + */ + void updateSection(int secIdx) { + + if (_disc.curSection != secIdx) { + + _disc.curSection = secIdx; + _disc.newStaticJac = true; + + // update velocity and dispersion + _disc.velocity = static_cast(_convDispOp.currentVelocity()); + if (_convDispOp.dispersionCompIndep()) + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOp.currentDispersion(secIdx)[0]); + } + else { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + _disc.dispersion[comp] = static_cast(_convDispOp.currentDispersion(secIdx)[comp]); + } + } + + } + } + + // ========================================================================================================================================================== // + // ======================================== DG Residual, RHS ====================================================== // + // ========================================================================================================================================================== // + + /** + * @brief calculates the volume Integral of the auxiliary equation + * @param [in] current state vector + * @param [in] stateDer vector to be changed + * @param [in] aux true if auxiliary, else main equation + */ + void volumeIntegral(Eigen::Map>& state, Eigen::Map>& stateDer) { + // comp-cell-node state vector: use of Eigen lib performance + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + stateDer.segment(Cell * _disc.nNodes, _disc.nNodes) + -= _disc.polyDerM * state.segment(Cell * _disc.nNodes, _disc.nNodes); + } + } + + /* + * @brief calculates the interface fluxes h* of Convection Dispersion equation + */ + void InterfaceFlux(Eigen::Map>& C, const VectorXd& g, unsigned int comp) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // Conv.Disp. flux: h* = h*_conv + h*_disp = numFlux(v c_l, v c_r) + 0.5 sqrt(D_ax) (S_l + S_r) + + if (_disc.velocity >= 0.0) { // forward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell - strideNode]) // left cell (i.e. forward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet (left) boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * _disc.boundary[0]; + + // outlet (right) boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * (C[_disc.nCol * strideCell - strideNode]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[_disc.nCol * strideCell - strideNode] // last cell last node + + _disc.boundary[3]); // right boundary value S + } + else { // backward flow (upwind num. flux) + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + // h* = h*_conv + h*_disp + _disc.surfaceFlux[Cell] // inner interfaces + = _disc.velocity * (C[Cell * strideCell]) // right cell (i.e. backward flow upwind) + - 0.5 * std::sqrt(_disc.dispersion[comp]) * (g[Cell * strideCell - strideNode] // left cell + + g[Cell * strideCell]); // right cell + } + + // boundary fluxes + // inlet boundary interface + _disc.surfaceFlux[_disc.nCol] + = _disc.velocity * _disc.boundary[0]; + + // outlet boundary interface + _disc.surfaceFlux[0] + = _disc.velocity * (C[0]) + - std::sqrt(_disc.dispersion[comp]) * 0.5 * (g[0] // first cell first node + + _disc.boundary[2]); // left boundary value g + } + } + /** + * @brief calculates and fills the surface flux values for auxiliary equation + */ + void InterfaceFluxAuxiliary(Eigen::Map>& C) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // Auxiliary flux: c* = 0.5 (c_l + c_r) + + // calculate inner interface fluxes + for (unsigned int Cell = 1; Cell < _disc.nCol; Cell++) { + _disc.surfaceFlux[Cell] // left interfaces + = 0.5 * (C[Cell * strideCell - strideNode] + // left node + C[Cell * strideCell]); // right node + } + // calculate boundary interface fluxes + + _disc.surfaceFlux[0] // left boundary interface + = 0.5 * (C[0] + // boundary value + C[0]); // first cell first node + + _disc.surfaceFlux[(_disc.nCol)] // right boundary interface + = 0.5 * (C[_disc.nCol * strideCell - strideNode] + // last cell last node + C[_disc.nCol * strideCell - strideNode]);// // boundary value + } + + /** + * @brief calculates the surface Integral, depending on the approach (exact/inexact integration) + * @param [in] state relevant state vector + * @param [in] stateDer state derivative vector the solution is added to + * @param [in] aux true for auxiliary equation, false for main equation + surfaceIntegral(cPtr, &(disc.g[0]), disc,&(disc.h[0]), resPtrC, 0, secIdx); + */ + void surfaceIntegral(Eigen::Map>& C, Eigen::Map>& state, Eigen::Map>& stateDer, bool aux, unsigned int Comp) { + + // component-wise strides + unsigned int strideCell = _disc.nNodes; + unsigned int strideNode = 1u; + + // calc numerical flux values c* or h* depending on auxiliary equation switch aux + (aux == 1) ? InterfaceFluxAuxiliary(C) : InterfaceFlux(C, _disc.g, Comp); + if (_disc.exactInt) { // modal approach -> dense mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + for (unsigned int Node = 0; Node < _disc.nNodes; Node++) { + stateDer[Cell * strideCell + Node * strideNode] + -= _disc.invMM(Node, 0) * (state[Cell * strideCell] + - _disc.surfaceFlux[Cell]) + - _disc.invMM(Node, _disc.polyDeg) * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux[(Cell + 1)]); + } + } + } + else { // nodal approach -> diagonal mass matrix + for (unsigned int Cell = 0; Cell < _disc.nCol; Cell++) { + // strong surface integral -> M^-1 B [state - state*] + stateDer[Cell * strideCell] // first cell node + -= _disc.invWeights[0] * (state[Cell * strideCell] // first node + - _disc.surfaceFlux(Cell)); + stateDer[Cell * strideCell + _disc.polyDeg * strideNode] // last cell node + += _disc.invWeights[_disc.polyDeg] * (state[Cell * strideCell + _disc.polyDeg * strideNode] + - _disc.surfaceFlux(Cell + 1)); + } + } + } + + /** + * @brief calculates the substitute h = vc - sqrt(D_ax) g(c) + */ + void calcH(Eigen::Map>& C, unsigned int Comp) { + _disc.h = _disc.velocity * C - std::sqrt(_disc.dispersion[Comp]) * _disc.g; + } + + /** + * @brief calculates the isotherm right hand side + */ + void calcRHSq_DG(double t, unsigned int secIdx, const double* yPtr, double* const resPtr, util::ThreadLocalStorage& threadLocalMem) { + + Indexer idx(_disc); + + const double* localC = yPtr + idx.offsetC(); + const double* localQ = yPtr + idx.offsetC() + idx.strideColLiquid() + idx.offsetBoundComp(0); + double* localQRes = resPtr + idx.offsetC() + idx.strideColLiquid() + idx.offsetBoundComp(0); + + for (unsigned int point = 0; point < _disc.nPoints; point++) { + + double z = _disc.deltaZ * std::floor(point / _disc.nNodes) + + 0.5 * _disc.deltaZ * (1 + _disc.nodes[point % _disc.nNodes]); + + _binding[0]->flux(t, secIdx, ColumnPosition{ z, 0.0, 0.0 }, localQ, localC, localQRes, threadLocalMem.get()); + + localC += idx.strideColNode(); // next solid concentration + localQ += idx.strideColNode(); // next liquid concentration + localQRes += idx.strideColNode(); // next liquid concentration + + } + } + + /** + * @brief applies the inverse Jacobian of the mapping + */ + void applyMapping(Eigen::Map>& state) { + state *= (2.0 / _disc.deltaZ); + } + /** + * @brief applies the inverse Jacobian of the mapping and auxiliary factor -1 + */ + void applyMapping_Aux(Eigen::Map>& state, unsigned int Comp) { + state *= (-2.0 / _disc.deltaZ) * ((_disc.dispersion[Comp] == 0.0) ? 1.0 : std::sqrt(_disc.dispersion[Comp])); + } + + void ConvDisp_DG(Eigen::Map>& C, Eigen::Map>& resC, double t, unsigned int Comp) { + + // ===================================// + // reset cache // + // ===================================// + + resC.setZero(); + _disc.h.setZero(); + _disc.g.setZero(); + _disc.surfaceFlux.setZero(); + // get Map objects of auxiliary variable memory + Eigen::Map> g(&_disc.g[0], _disc.nPoints, InnerStride<>(1)); + Eigen::Map> h(&_disc.h[0], _disc.nPoints, InnerStride<>(1)); + + // ======================================// + // solve auxiliary system g = d c / d x // + // ======================================// + + volumeIntegral(C, g); // DG volumne integral in strong form + + surfaceIntegral(C, C, g, 1, Comp); // surface integral in strong form + + applyMapping_Aux(g, Comp); // inverse mapping from reference space and auxiliary factor + + _disc.surfaceFlux.setZero(); // reset surface flux storage as it is used twice + + // ======================================// + // solve main equation w_t = d h / d x // + // ======================================// + + calcH(C, Comp); // calculate the substitute h(S(c), c) = - (sqrt(D_ax) g(c) - v c) + + volumeIntegral(h, resC); // DG volumne integral in strong form + + calcBoundaryValues(C);// update boundary values including auxiliary variable g + + surfaceIntegral(C, h, resC, 0, Comp); // DG surface integral in strong form + + applyMapping(resC); // inverse mapping to reference space + + } + /** + * @brief computes ghost nodes used to implement Danckwerts boundary conditions + */ + void calcBoundaryValues(Eigen::Map>& C) { + + //cache.boundary[0] = c_in -> inlet DOF idas suggestion + //_disc.boundary[1] = (_disc.velocity >= 0.0) ? C[_disc.nPoints - 1] : C[0]; // c_r outlet not required + _disc.boundary[2] = -_disc.g[0]; // g_l left boundary (inlet/outlet for forward/backward flow) + _disc.boundary[3] = -_disc.g[_disc.nPoints - 1]; // g_r right boundary (outlet/inlet for forward/backward flow) + } + + // ========================================================================================================================================================== // + // ======================================== DG Jacobian ========================================================= // + // ========================================================================================================================================================== // + + typedef Eigen::Triplet T; + + /** + * @brief sets the sparsity pattern of the static Jacobian + * @detail DG ConvDisp pattern and isotherm pattern. Independent of the isotherm, + all liquid and solid entries at a discrete point are set. + * @param [in] stateDer bool if state derivative pattern should be added (for _jacDisc) + */ + void setPattern(Eigen::SparseMatrix& mat, bool stateDer, bool has_reaction) { + + std::vector tripletList; + + unsigned int isotherm_entries = _disc.nPoints * _disc.strideBound * (_disc.strideBound + _disc.nComp); + unsigned int reaction_entries = has_reaction ? _disc.nPoints * _disc.nComp * (_disc.strideBound + _disc.nComp) : 0; + + tripletList.reserve(nConvDispEntries(false) + isotherm_entries + reaction_entries); + + if (_disc.exactInt) + ConvDispModalPattern(tripletList); + else + ConvDispNodalPattern(tripletList); + + bindingAndReactionPattern(tripletList, has_reaction); + + if (stateDer) + stateDerPattern(tripletList); // only adds [ d convDisp / d q_t ] because main diagonal is already included ! + + mat.setFromTriplets(tripletList.begin(), tripletList.end()); + + } + + /** + * @brief computes the convection dispersion part of the state derivative pattern of the jacobian. + * @detail the main also diagonal belongs to the state derivative pattern but is not set here, so it should be set previously. + */ + void stateDerPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + unsigned int offC = 0; // inlet DOFs not included in Jacobian + + for (unsigned int point = 0; point < _disc.nPoints; point++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + if (_disc.nBound[comp]) // either one or null + // row: jump over inlet, go node and comp strides to find all liquid states + // column: jump over inlet and liquid states, add component offset and go node strides to find corresponding solid states + tripletList.push_back(T(offC + point * idxr.strideColNode() + comp * idxr.strideColComp(), + offC + idxr.strideColLiquid() + idxr.offsetBoundComp(comp) + point * idxr.strideColNode(), + 0.0)); + } + } + + } + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the nodal DG scheme + */ + int ConvDispNodalPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + int sComp = idxr.strideColComp(); + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Define Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + // special inlet DOF treatment for inlet boundary cell (first cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /*======================================================*/ + /* Define Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + // Dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell + + // insert Blocks to Jacobian inner cells (only for nCells >= 3) + if (nCells >= 3u) { + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + (cell - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + if (nCells != 1) { // Note: special case nCells = 1 already set by advection block + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes; j < 3 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - nNodes) * sNode, + 0.0)); + } + } + } + + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1 - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 0; + } + + /** + * @brief sets the sparsity pattern of the convection dispersion Jacobian for the exact integration (her: modal) DG scheme + */ + int ConvDispModalPattern(std::vector& tripletList) { + + Indexer idxr(_disc); + + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + int sComp = idxr.strideColComp(); + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Define Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on upwind entry + + if (_disc.velocity >= 0.0) { // forward flow upwind entry -> last node of previous cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + //tripletList.push_back(T(offC + comp * sComp + i * sNode, comp * sComp, 0.0)); // inlet DOFs not included in Jacobian + for (unsigned int j = 1; j < nNodes + 1; j++) { + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, go back one node, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + else { // backward flow upwind entry -> first node of subsequent cell + // special inlet DOF treatment for inlet boundary cell (last cell) + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + // inlet DOFs not included in Jacobian + for (unsigned int j = 0; j < nNodes; j++) { + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + // col: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each convection block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /*======================================================*/ + /* Define Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells */ + if (nCells >= 5u) { + // Inner dispersion block [ d RHS_disp / d c ], depends on whole previous and subsequent cell plus first entries of subsubsequent cells + for (unsigned int cell = 2; cell < nCells - 2; cell++) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry + tripletList.push_back(T(offC + cell * sCell + comp * sComp + i * sNode, + offC + cell * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + } + + /* boundary cell neighbours */ + + // left boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 2; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + else if (nCells == 3u) { // special case: only depends on the two neighbouring cells + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 1; j < 3 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cell, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry. Also adjust for iterator j (-1) + tripletList.push_back(T(offC + nNodes * sNode + comp * sComp + i * sNode, + offC + comp * sComp + (j - 1) * sNode, + 0.0)); + } + } + } + } + // right boundary cell neighbour + if (nCells >= 4u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 3 * nNodes + 2 - 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 2) * sCell + comp * sComp + i * sNode, + offC + (nCells - 2) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + /* boundary cells */ + + // left boundary cell + unsigned int end = 3u * nNodes + 2u; + if (nCells == 1u) end = 2u * nNodes + 1u; + else if (nCells == 2u) end = 3u * nNodes + 1u; + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = nNodes + 1; j < end; j++) { + // row: jump over inlet DOFs, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs, add component offset, adjust for iterator j (-Nnodes-1) and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + comp * sComp + i * sNode, + offC + comp * sComp + (j - (nNodes + 1)) * sNode, + 0.0)); + } + } + } + // right boundary cell + if (nCells >= 3u) { + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes + 1; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell and one node, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes + 1) * sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + else if (nCells == 2u) { // special case for nCells == 2: depends only on left cell + for (unsigned int comp = 0; comp < nComp; comp++) { + for (unsigned int i = 0; i < nNodes; i++) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // row: jump over inlet DOFs and previous cells, add component offset and go node strides from there for each dispersion block entry + // col: jump over inlet DOFs and previous cells, go back one cell, add component offset and go node strides from there for each dispersion block entry. + tripletList.push_back(T(offC + (nCells - 1) * sCell + comp * sComp + i * sNode, + offC + (nCells - 1) * sCell - (nNodes)*sNode + comp * sComp + j * sNode, + 0.0)); + } + } + } + } + + return 0; + } + + /** + * @brief sets the sparsity pattern of the isotherm and reaction Jacobian + * @detail Independent of the isotherm, all liquid and solid entries (so all entries, the isotherm could theoretically depend on) at a discrete point are set. + */ + void bindingAndReactionPattern(std::vector& tripletList, bool has_reaction) { + + Indexer idxr(_disc); + int offC = 0; // inlet DOFs not included in Jacobian + + // isotherm pattern: loop over all discrete points and solid states and add all liquid plus solid entries at that solid state at that discrete point + for (unsigned int point = 0; point < _disc.nPoints; point++) { + for (unsigned int solid = 0; solid < _disc.strideBound; solid++) { + for (unsigned int conc = 0; conc < _disc.nComp + _disc.strideBound; conc++) { + // row: jump over inlet and previous discrete points, liquid concentration and add the offset of the current bound state + // column: jump over inlet and previous discrete points. add entries for all liquid and bound concentrations (conc) + tripletList.push_back(T(offC + idxr.strideColNode() * point + idxr.strideColLiquid() + solid, + offC + idxr.strideColNode() * point + conc, + 0.0)); + } + } + } + + // reaction pattern: loop over all discrete points and liquid states and add all liquid plus solid entries at that liquid state at that discrete point + if (has_reaction) { + for (unsigned int point = 0; point < _disc.nPoints; point++) { + for (unsigned int liquid = 0; liquid < _disc.nComp; liquid++) { + for (unsigned int conc = 0; conc < _disc.nComp + _disc.strideBound; conc++) { + // row: jump over inlet and previous discrete points, liquid concentration and add the offset of the current bound state + // column: jump over inlet and previous discrete points. add entries for all liquid and bound concentrations (conc) + tripletList.push_back(T(offC + idxr.strideColNode() * point + liquid, + offC + idxr.strideColNode() * point + conc, + 0.0)); + } + } + } + } + } + + /** + * @brief analytically calculates the (static) state jacobian + * @return 1 if jacobain estimation fits the predefined pattern of the jacobian, 0 if not. + */ + int calcStaticAnaJacobian(double t, unsigned int secIdx, const double* const y, util::ThreadLocalStorage& threadLocalMem) { + + // DG convection dispersion Jacobian + if (_disc.exactInt) + calcConvDispDGSEMJacobian(); + else + calcConvDispCollocationDGSEMJacobian(); + + if (!_jac.isCompressed()) // if matrix lost its compressed storage, the pattern did not fit. + return 0; + + return 1; + } + + /** + * @brief calculates the number of entris for the DG convection dispersion jacobian + * @note only dispersion entries are relevant for jacobian NNZ as the convection entries are a subset of these + */ + unsigned int nConvDispEntries(bool pureNNZ = false) { + + if (_disc.exactInt) { + if (pureNNZ) { + return _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + } + return _disc.nComp * _disc.nNodes * _disc.nNodes + _disc.nNodes // convection entries + + _disc.nComp * ((3u * _disc.nCol - 2u) * _disc.nNodes * _disc.nNodes + (2u * _disc.nCol - 3u) * _disc.nNodes); // dispersion entries + } + else { + if (pureNNZ) { + return _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + } + return _disc.nComp * _disc.nNodes * _disc.nNodes + 1u // convection entries + + _disc.nComp * (_disc.nCol * _disc.nNodes * _disc.nNodes + 8u * _disc.nNodes); // dispersion entries + } + } + + /** + * @brief analytically calculates the convection dispersion jacobian for the nodal DG scheme + */ + int calcConvDispCollocationDGSEMJacobian() { + + Indexer idxr(_disc); + + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cell dispersion blocks */ + + if (nCells >= 3u) { + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[1]; + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC + idxr.strideColCell()); // row iterator starting at second cell and component + + for (unsigned int cell = 1; cell < nCells - 1; cell++) { + for (unsigned int i = 0; i < dispBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 3*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + } + + /* Boundary cell Dispersion blocks */ + + /* left cell */ + MatrixXd dispBlock = _disc.DGjacAxDispBlocks[0]; + + if (nCells != 1u) { // "standard" case + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell and component + + for (unsigned int i = 0; i < dispBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < dispBlock.cols(); j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + else { // special case + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell and component + for (unsigned int i = 0; i < dispBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = nNodes; j < nNodes * 2u; j++) { + // row: iterator is at current node i and current component comp + // col: jump to node j + jacIt[((j - nNodes) - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /* right cell */ + if (nCells != 1u) { // "standard" case + dispBlock = _disc.DGjacAxDispBlocks[std::min(nCells, 3u) - 1]; + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC + (nCells - 1) * idxr.strideColCell()); // row iterator starting at last cell + + for (unsigned int i = 0; i < dispBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < 2 * nNodes; j++) { + // pattern is more sparse than a nNodes x 2*nNodes block. + if ((j >= nNodes - 1 && j <= 2 * nNodes) || + (i == 0 && j <= 2 * nNodes) || + (i == nNodes - 1 && j >= nNodes - 1)) + // row: iterator is at current node i and current component comp + // col: start at previous cell and jump to node j + jacIt[-idxr.strideColCell() + (j - i) * idxr.strideColNode()] = dispBlock(i, j) * _disc.dispersion[comp]; + } + } + } + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + // Convection block [ d RHS_conv / d c ], also depends on first entry of previous cell + MatrixXd convBlock = _disc.DGjacAxConvBlock; + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell and component + + if (_disc.velocity >= 0.0) { // forward flow upwind convection + // special inlet DOF treatment for first cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(0, 0); // only first node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + //jacIt[0] = -convBlock(i, 0); // dependency on inlet DOFs is handled in _jacInlet + for (unsigned int j = 1; j < convBlock.cols(); j++) { + jacIt[((j - 1) - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + // remaining cells + for (unsigned int cell = 1; cell < nCells; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at previous cells last node and go to node j. + jacIt[-idxr.strideColNode() + (j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + } + else { // backward flow upwind convection + // non-inlet cells + for (unsigned int cell = 0; cell < nCells - 1u; cell++) { + for (unsigned int i = 0; i < convBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols(); j++) { + // row: iterator is at current cell and component + // col: start at current cells first node and go to node j. + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + // special inlet DOF treatment for last cell (inlet boundary cell) + _jacInlet(0, 0) = _disc.velocity * convBlock(convBlock.rows() - 1, convBlock.cols() - 1); // only last node depends on inlet concentration + for (unsigned int i = 0; i < convBlock.rows(); i++, jacIt += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < nComp; comp++, ++jacIt) { + for (unsigned int j = 0; j < convBlock.cols() - 1; j++) { + jacIt[(j - i) * idxr.strideColNode()] += _disc.velocity * convBlock(i, j); + } + } + } + } + + return 0; + } + /** + * @brief inserts a liquid state block with different factors for components into the system jacobian + * @param [in] block (sub)block to be added + * @param [in] jac row iterator at first (i.e. upper) entry + * @param [in] offCol column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + * @param [in] Compfactor component dependend factors + */ + void insertCompDepLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offCol, Indexer& idxr, unsigned int nCells, double* Compfactor) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++, jac += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * idxr.strideColNode() + offCol] = block(i, j) * Compfactor[comp]; + } + } + } + } + } + /** + * @brief adds liquid state blocks for all components to the system jacobian + * @param [in] block to be added + * @param [in] jac row iterator at first (i.e. upper left) entry + * @param [in] column to row offset (i.e. start at upper left corner of block) + * @param [in] idxr Indexer + * @param [in] nCells determines how often the block is added (diagonally) + */ + void addLiquidJacBlock(Eigen::MatrixXd block, linalg::BandedEigenSparseRowIterator& jac, int offCol, Indexer& idxr, unsigned int nCells) { + + for (unsigned int cell = 0; cell < nCells; cell++) { + for (unsigned int i = 0; i < block.rows(); i++, jac += idxr.strideColBound()) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++, ++jac) { + for (unsigned int j = 0; j < block.cols(); j++) { + // row: at current node component + // col: jump to node j + jac[(j - i) * idxr.strideColNode() + offCol] += block(i, j); + } + } + } + } + } + /** + * @brief analytically calculates the convection dispersion jacobian for the exact integration (here: modal) DG scheme + */ + int calcConvDispDGSEMJacobian() { + + Indexer idxr(_disc); + + int offC = 0; // inlet DOFs not included in Jacobian + + unsigned int nNodes = _disc.nNodes; + unsigned int nCells = _disc.nCol; + unsigned int nComp = _disc.nComp; + + /*======================================================*/ + /* Compute Dispersion Jacobian Block */ + /*======================================================*/ + + /* Inner cells (exist only if nCells >= 5) */ + if (nCells >= 5) { + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC + idxr.strideColCell() * 2); // row iterator starting at third cell, first component + // insert all (nCol - 4) inner cell blocks + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[2], jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, _disc.nCol - 4u, &(_disc.dispersion[0])); + } + + /* boundary cell neighbours (exist only if nCells >= 4) */ + if (nCells >= 4) { + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC + idxr.strideColCell()); // row iterator starting at second cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes + 1), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + + jacIt += (_disc.nCol - 4) * idxr.strideColCell(); // move iterator to preultimate cell (already at third cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[nCells > 4 ? 3 : 2].block(0, 0, nNodes, 3 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0])); + } + + /* boundary cells (exist only if nCells >= 3) */ + if (nCells >= 3) { + + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell, first component + + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes + 1), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + + jacIt += (_disc.nCol - 2) * idxr.strideColCell(); // move iterator to last cell (already at second cell) + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[std::min(nCells, 5u) - 1u].block(0, 0, nNodes, 2 * nNodes + 1), jacIt, -(idxr.strideColCell() + idxr.strideColNode()), idxr, 1u, &(_disc.dispersion[0])); + } + + /* For special cases nCells = 1, 2, 3, some cells still have to be treated separately*/ + + if (nCells == 1) { + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell, first component + // insert the only block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + } + else if (nCells == 2) { + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC); // row iterator starting at first cell, first component + // left Bacobian block + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[0].block(0, nNodes + 1, nNodes, 2 * nNodes), jacIt, 0, idxr, 1u, &(_disc.dispersion[0])); + // right Bacobian block, iterator is already moved to second cell + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 2 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + } + else if (nCells == 3) { + linalg::BandedEigenSparseRowIterator jacIt(_jac, offC + idxr.strideColCell()); // row iterator starting at first cell, first component + insertCompDepLiquidJacBlock(_disc.DGjacAxDispBlocks[1].block(0, 1, nNodes, 3 * nNodes), jacIt, -idxr.strideColCell(), idxr, 1u, &(_disc.dispersion[0])); + } + + /*======================================================*/ + /* Compute Convection Jacobian Block */ + /*======================================================*/ + + int sComp = idxr.strideColComp(); + int sNode = idxr.strideColNode(); + int sCell = idxr.strideColCell(); + + linalg::BandedEigenSparseRowIterator jac(_jac, offC); + + if (_disc.velocity >= 0.0) { // Forward flow + // special inlet DOF treatment for inlet (first) cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(0); // only first cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 1, nNodes, nNodes), jac, 0, idxr, 1); + if (_disc.nCol > 1) // iterator already moved to second cell + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, -idxr.strideColNode(), idxr, _disc.nCol - 1); + } + else { // Backward flow + // non-inlet cells first + if (_disc.nCol > 1) + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock, jac, 0, idxr, _disc.nCol - 1); + // special inlet DOF treatment for inlet (last) cell. Iterator already moved to last cell + _jacInlet = _disc.velocity * _disc.DGjacAxConvBlock.col(_disc.DGjacAxConvBlock.cols() - 1); // only last cell depends on inlet concentration + addLiquidJacBlock(_disc.velocity * _disc.DGjacAxConvBlock.block(0, 0, nNodes, nNodes), jac, 0, idxr, 1); + } + + return 0; + } + /** + * @brief adds time derivative to the jacobian + */ + void addTimederJacobian(double alpha) { + + Indexer idxr(_disc); + + int sNode = idxr.strideColNode(); + unsigned int offC = 0; // inlet DOFs not included in Jacobian + + // =================================================================================================== // + // Time derivative Jacobian: d Residual / d y_t // + // =================================================================================================== // + + linalg::BandedEigenSparseRowIterator jac(_jacDisc, offC); + + double Beta = (1.0 - _disc.porosity) / _disc.porosity; + + for (unsigned int point = 0; point < _disc.nPoints; point++) { + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + // d convDispRHS / d c_t + jac[0] += alpha; + + if (_disc.nBound[comp]) { // either one or null; no loop necessary + // d convDispRHS / d q_t + jac[idxr.strideColLiquid() - comp + idxr.offsetBoundComp(comp)] += alpha * Beta; + } + ++jac; + } + for (unsigned int comp = 0; comp < _disc.nComp; comp++) { + if (_disc.nBound[comp]) { // either one or null; no loop over bound states necessary + if (!_binding[0]->hasQuasiStationaryReactions()) { + // d isotherm / d q_t + jac[0] += alpha; + } + ++jac; + } + } + } + + } + + /** + * @brief computes the jacobian via finite differences (testing purpose) + */ + MatrixXd calcFDJacobian(const double* y_, const double* yDot_, const SimulationTime simTime, util::ThreadLocalStorage& threadLocalMem, double alpha) { + + ////// create solution vectors + Eigen::Map hmpf(y_, numDofs()); + VectorXd y = hmpf; + VectorXd yDot; + if (yDot_) { + Eigen::Map hmpf2(yDot_, numDofs()); + yDot = hmpf2; + + //// set to LWE initial conditions + //Indexer idxr(_disc); + //for(unsigned int blk=0;blk<_disc.nPoints;blk++){ + // y[idxr.offsetC() + blk * idxr.strideColNode()] = 50.0; + // y[idxr.offsetC() + blk * idxr.strideColNode() + idxr.strideColLiquid()] = 1200.0; + //} + //yDot.setZero(); + } + else { + return MatrixXd::Zero(numDofs(), numDofs()); + } + //VectorXd y = VectorXd::Zero(numDofs()); + //VectorXd yDot = VectorXd::Zero(numDofs()); + + VectorXd res = VectorXd::Zero(numDofs()); + const double* yPtr = &y[0]; + const double* yDotPtr = &yDot[0]; + double* resPtr = &res[0]; + // create FD jacobian + MatrixXd Jacobian = MatrixXd::Zero(numDofs(), numDofs()); + // set FD step + double epsilon = 0.01; + + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + + for (int col = 0; col < Jacobian.cols(); col++) { + Jacobian.col(col) = -(1.0 + alpha) * res; + } + /* Residual(y+h) */ + // state DOFs + for (int dof = 0; dof < Jacobian.cols(); dof++) { + y[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + y[dof] -= epsilon; + Jacobian.col(dof) += res; + } + + // state derivative Jacobian + for (int dof = 0; dof < Jacobian.cols(); dof++) { + yDot[dof] += epsilon; + residualImpl(simTime.t, simTime.secIdx, yPtr, yDotPtr, resPtr, threadLocalMem); + yDot[dof] -= epsilon; + Jacobian.col(dof) += alpha * res; + } + + ////* exterminate numerical noise and divide by epsilon*/ + for (int i = 0; i < Jacobian.rows(); i++) { + for (int j = 0; j < Jacobian.cols(); j++) { + if (std::abs(Jacobian(i, j)) < 1e-10) Jacobian(i, j) = 0.0; + } + } + Jacobian /= epsilon; + + return Jacobian; + } + + }; + + } // namespace model +} // namespace cadet + +#endif // LIBCADET_LUMPEDRATEMODELWITHOUTPORESDG_HPP_ diff --git a/src/libcadet/model/ReactionModel.hpp b/src/libcadet/model/ReactionModel.hpp index bc3a1b5a1..eeadf9442 100644 --- a/src/libcadet/model/ReactionModel.hpp +++ b/src/libcadet/model/ReactionModel.hpp @@ -20,10 +20,12 @@ #include +#include "CompileTimeConfig.hpp" #include "cadet/ParameterProvider.hpp" #include "cadet/ParameterId.hpp" #include "linalg/DenseMatrix.hpp" #include "linalg/BandMatrix.hpp" +#include "linalg/BandedEigenSparseRowIterator.hpp" #include "linalg/CompressedSparseMatrix.hpp" #include "AutoDiff.hpp" #include "Memory.hpp" @@ -233,6 +235,9 @@ class IDynamicReactionModel virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const = 0; virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const = 0; virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandedSparseRowIterator jac, LinearBufferAllocator workSpace) const = 0; + #ifdef ENABLE_DG + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const = 0; + #endif /** * @brief Evaluates the residual for one combined phase cell @@ -292,6 +297,9 @@ class IDynamicReactionModel virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::BandMatrix::RowIterator jacSolid, LinearBufferAllocator workSpace) const = 0; virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::DenseBandedRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const = 0; virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const = 0; + #ifdef ENABLE_DG + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandedEigenSparseRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const = 0; + #endif /** * @brief Returns the number of reactions for a liquid phase cell diff --git a/src/libcadet/model/binding/BindingModelMacros.hpp b/src/libcadet/model/binding/BindingModelMacros.hpp index 601a21b40..fd33a06b1 100644 --- a/src/libcadet/model/binding/BindingModelMacros.hpp +++ b/src/libcadet/model/binding/BindingModelMacros.hpp @@ -61,17 +61,38 @@ * * The implementation is inserted inline in the class declaration. */ -#define CADET_BINDINGMODEL_JACOBIAN_BOILERPLATE \ - virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ - int offsetCp, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const \ - { \ - jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ - } \ - \ - virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ - int offsetCp, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const \ - { \ - jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ - } +#ifdef ENABLE_DG + #define CADET_BINDINGMODEL_JACOBIAN_BOILERPLATE \ + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + int offsetCp, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ + } \ + \ + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + int offsetCp, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ + } \ + \ + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + int offsetCp, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ + } +#else + #define CADET_BINDINGMODEL_JACOBIAN_BOILERPLATE \ + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + int offsetCp, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ + } \ + \ + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + int offsetCp, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianImpl(t, secIdx, colPos, y, y - offsetCp, offsetCp, jac, workSpace); \ + } +#endif #endif // LIBCADET_BINDINGMODELMACROS_HPP_ diff --git a/src/libcadet/model/binding/DummyBinding.cpp b/src/libcadet/model/binding/DummyBinding.cpp index e4faba6d0..b6acfad37 100644 --- a/src/libcadet/model/binding/DummyBinding.cpp +++ b/src/libcadet/model/binding/DummyBinding.cpp @@ -131,6 +131,10 @@ class DummyBinding : public IBindingModel { } + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, int offsetCp, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const + { + } + virtual void timeDerivativeQuasiStationaryFluxes(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yCp, double const* y, double* dResDt, LinearBufferAllocator workSpace) const { } virtual bool hasSalt() const CADET_NOEXCEPT { return false; } diff --git a/src/libcadet/model/binding/LinearBinding.cpp b/src/libcadet/model/binding/LinearBinding.cpp index 87e114a04..0aad87f97 100644 --- a/src/libcadet/model/binding/LinearBinding.cpp +++ b/src/libcadet/model/binding/LinearBinding.cpp @@ -557,6 +557,11 @@ class LinearBindingBase : public IBindingModel jacobianImpl(t, secIdx, colPos, y, offsetCp, jac, workSpace); } + virtual void analyticJacobian(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, int offsetCp, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const + { + jacobianImpl(t, secIdx, colPos, y, offsetCp, jac, workSpace); + } + virtual void timeDerivativeQuasiStationaryFluxes(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yCp, double const* y, double* dResDt, LinearBufferAllocator workSpace) const { if (!hasQuasiStationaryReactions()) diff --git a/src/libcadet/model/parts/ConvectionDispersionOperator.hpp b/src/libcadet/model/parts/ConvectionDispersionOperator.hpp index a4a8cdcd7..76d0c50ae 100644 --- a/src/libcadet/model/parts/ConvectionDispersionOperator.hpp +++ b/src/libcadet/model/parts/ConvectionDispersionOperator.hpp @@ -24,6 +24,7 @@ #include "Memory.hpp" #include "Weno.hpp" #include "SimulationTypes.hpp" +#include // todo delete once DG has its own operator #include #include @@ -85,7 +86,10 @@ class ConvectionDispersionOperatorBase inline const active& columnLength() const CADET_NOEXCEPT { return _colLength; } inline const active& crossSectionArea() const CADET_NOEXCEPT { return _crossSection; } + inline const active& currentVelocity() const CADET_NOEXCEPT { return _curVelocity; } + inline const active* currentDispersion(const int secIdx) const CADET_NOEXCEPT { return getSectionDependentSlice(_colDispersion, _nComp, secIdx); } // todo delete once DG has its own operator + inline const bool dispersionCompIndep() const CADET_NOEXCEPT { return _dispersionCompIndep; } // todo delete once DG has its own operator inline unsigned int nComp() const CADET_NOEXCEPT { return _nComp; } inline unsigned int nCol() const CADET_NOEXCEPT { return _nCol; } diff --git a/src/libcadet/model/reaction/DummyReaction.cpp b/src/libcadet/model/reaction/DummyReaction.cpp index 6e6241453..9fa7be1e7 100644 --- a/src/libcadet/model/reaction/DummyReaction.cpp +++ b/src/libcadet/model/reaction/DummyReaction.cpp @@ -12,6 +12,7 @@ #include "model/ReactionModel.hpp" #include "ParamIdUtil.hpp" +#include "CompileTimeConfig.hpp" namespace cadet { @@ -76,6 +77,9 @@ class DummyDynamicReaction : public IDynamicReactionModel virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const { } virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const { } virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandedSparseRowIterator jac, LinearBufferAllocator workSpace) const { } + #ifdef ENABLE_DG + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, double factor, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const { } + #endif virtual int residualCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, active const* yLiquid, active const* ySolid, active* resLiquid, active* resSolid, double factor, LinearBufferAllocator workSpace) const { return 0; } @@ -89,6 +93,9 @@ class DummyDynamicReaction : public IDynamicReactionModel virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::BandMatrix::RowIterator jacSolid, LinearBufferAllocator workSpace) const { } virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::DenseBandedRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const { } virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const { } + #ifdef ENABLE_DG + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, double factor, linalg::BandedEigenSparseRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const { } + #endif virtual unsigned int numReactionsLiquid() const CADET_NOEXCEPT { return 0; } virtual unsigned int numReactionsCombined() const CADET_NOEXCEPT { return 0; } diff --git a/src/libcadet/model/reaction/ReactionModelBase.hpp b/src/libcadet/model/reaction/ReactionModelBase.hpp index 35a716478..783fc82b5 100644 --- a/src/libcadet/model/reaction/ReactionModelBase.hpp +++ b/src/libcadet/model/reaction/ReactionModelBase.hpp @@ -90,6 +90,7 @@ class DynamicReactionModelBase : public IDynamicReactionModel * * The implementation is inserted inline in the class declaration. */ +#ifdef ENABLE_DG #define CADET_DYNAMICREACTIONMODEL_BOILERPLATE \ virtual int residualLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, active const* y, \ active* res, const active& factor, LinearBufferAllocator workSpace) const \ @@ -151,6 +152,18 @@ class DynamicReactionModelBase : public IDynamicReactionModel jacobianLiquidImpl(t, secIdx, colPos, y, factor, jac, workSpace); \ } \ \ + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + double factor, linalg::BandedEigenSparseRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianLiquidImpl(t, secIdx, colPos, y, factor, jac, workSpace); \ + } \ + \ + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, \ + double factor, linalg::BandedEigenSparseRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const \ + { \ + jacobianCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, factor, jacLiquid, jacSolid, workSpace); \ + } \ + \ virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, \ double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::BandMatrix::RowIterator jacSolid, LinearBufferAllocator workSpace) const \ { \ @@ -168,6 +181,88 @@ class DynamicReactionModelBase : public IDynamicReactionModel { \ jacobianCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, factor, jacLiquid, jacSolid, workSpace); \ } +#else +#define CADET_DYNAMICREACTIONMODEL_BOILERPLATE \ + virtual int residualLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, active const* y, \ + active* res, const active& factor, LinearBufferAllocator workSpace) const \ + { \ + return residualLiquidImpl(t, secIdx, colPos, y, res, factor, workSpace); \ + } \ + \ + virtual int residualLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, active const* y, \ + active* res, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualLiquidImpl(t, secIdx, colPos, y, res, factor, workSpace); \ + } \ + \ + virtual int residualLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + active* res, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualLiquidImpl(t, secIdx, colPos, y, res, factor, workSpace); \ + } \ + \ + virtual int residualLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + double* res, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualLiquidImpl(t, secIdx, colPos, y, res, factor, workSpace); \ + } \ + \ + virtual int residualCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, active const* yLiquid, \ + active const* ySolid, active* resLiquid, active* resSolid, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, resLiquid, resSolid, factor, workSpace); \ + } \ + \ + virtual int residualCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, \ + double const* ySolid, active* resLiquid, active* resSolid, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, resLiquid, resSolid, factor, workSpace); \ + } \ + \ + virtual int residualCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, \ + double const* ySolid, double* resLiquid, double* resSolid, double factor, LinearBufferAllocator workSpace) const \ + { \ + return residualCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, resLiquid, resSolid, factor, workSpace); \ + } \ + \ + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + double factor, linalg::BandMatrix::RowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianLiquidImpl(t, secIdx, colPos, y, factor, jac, workSpace); \ + } \ + \ + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + double factor, linalg::DenseBandedRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianLiquidImpl(t, secIdx, colPos, y, factor, jac, workSpace); \ + } \ + \ + virtual void analyticJacobianLiquidAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* y, \ + double factor, linalg::BandedSparseRowIterator jac, LinearBufferAllocator workSpace) const \ + { \ + jacobianLiquidImpl(t, secIdx, colPos, y, factor, jac, workSpace); \ + } \ + \ + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, \ + double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::BandMatrix::RowIterator jacSolid, LinearBufferAllocator workSpace) const \ + { \ + jacobianCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, factor, jacLiquid, jacSolid, workSpace); \ + } \ + \ + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, \ + double factor, linalg::DenseBandedRowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const \ + { \ + jacobianCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, factor, jacLiquid, jacSolid, workSpace); \ + } \ + \ + virtual void analyticJacobianCombinedAdd(double t, unsigned int secIdx, const ColumnPosition& colPos, double const* yLiquid, double const* ySolid, \ + double factor, linalg::BandMatrix::RowIterator jacLiquid, linalg::DenseBandedRowIterator jacSolid, LinearBufferAllocator workSpace) const \ + { \ + jacobianCombinedImpl(t, secIdx, colPos, yLiquid, ySolid, factor, jacLiquid, jacSolid, workSpace); \ + } +#endif + + } // namespace model } // namespace cadet diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5a89fd8bd..0a24bd8c7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -43,7 +43,7 @@ if (ENABLE_GRM_2D) endif() add_executable(testRunner testRunner.cpp JsonTestModels.cpp ColumnTests.cpp UnitOperationTests.cpp SimHelper.cpp ParticleHelper.cpp - GeneralRateModel.cpp GeneralRateModel2D.cpp LumpedRateModelWithPores.cpp LumpedRateModelWithoutPores.cpp + GeneralRateModel.cpp GeneralRateModel2D.cpp LumpedRateModelWithPores.cpp LumpedRateModelWithoutPores.cpp LumpedRateModelWithoutPoresDG.cpp CSTR-Residual.cpp CSTR-Simulation.cpp ConvectionDispersionOperator.cpp CellKernelTests.cpp @@ -56,7 +56,9 @@ add_executable(testRunner testRunner.cpp JsonTestModels.cpp ColumnTests.cpp Unit ${TEST_ADDITIONAL_SOURCES} $) -target_link_libraries(testRunner PRIVATE CADET::CompileOptions CADET::AD SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET}) +target_include_directories(testRunner PRIVATE ${CMAKE_BINARY_DIR}/src/libcadet) +target_link_libraries(testRunner PRIVATE CADET::CompileOptions CADET::AD SUNDIALS::sundials_idas ${SUNDIALS_NVEC_TARGET} ${TBB_TARGET} ${EIGEN_TARGET}) + if (ENABLE_GRM_2D) if (SUPERLU_FOUND) target_link_libraries(testRunner PRIVATE SuperLU::SuperLU) diff --git a/test/ColumnTests.cpp b/test/ColumnTests.cpp index 009d3ccd4..b7e9d3990 100644 --- a/test/ColumnTests.cpp +++ b/test/ColumnTests.cpp @@ -203,6 +203,34 @@ namespace column jpp.popScope(); } + void setDG(cadet::JsonParameterProvider& jpp, std::string basis, unsigned int polyDeg, unsigned int nCol) + { + int level = 0; + + if (jpp.exists("model")) + { + jpp.pushScope("model"); + ++level; + } + if (jpp.exists("unit_000")) + { + jpp.pushScope("unit_000"); + ++level; + } + + jpp.pushScope("discretization"); + + // Set discretization parameters + jpp.set("NCOL", static_cast(nCol)); + jpp.set("NNODES", static_cast(polyDeg + 1)); + jpp.set("POLYNOMIAL_BASIS", basis); + + jpp.popScope(); + + for (int l = 0; l < level; ++l) + jpp.popScope(); + } + void setNumParCells(cadet::JsonParameterProvider& jpp, unsigned int nPar, std::string unitID) { int level = 0; @@ -585,6 +613,44 @@ namespace column } } + void testAnalyticBenchmark_DG(const char* uoType, const char* refFileRelPath, bool forwardFlow, bool dynamicBinding, std::string basis, unsigned int polyDeg, unsigned int nCol, double absTol, double relTol) + { + const std::string fwdStr = (forwardFlow ? "forward" : "backward"); + SECTION("Analytic " + fwdStr + " flow with " + (dynamicBinding ? "dynamic" : "quasi-stationary") + " binding") + { + // Setup simulation + cadet::JsonParameterProvider jpp = createLinearBenchmark(dynamicBinding, false, uoType); + setDG(jpp, basis, polyDeg, nCol); + if (!forwardFlow) + reverseFlow(jpp); + + // Run simulation + cadet::Driver drv; + drv.configure(jpp); + drv.run(); + + // Read reference data from test file + const std::string refFile = std::string(getTestDirectory()) + std::string(refFileRelPath); + ReferenceDataReader rd(refFile.c_str()); + const std::vector time = rd.time(); + const std::vector ref = (dynamicBinding ? rd.analyticDynamic() : rd.analyticQuasiStationary()); + + // Get data from simulation + cadet::InternalStorageUnitOpRecorder const* const simData = drv.solution()->unitOperation(0); + double const* outlet = (forwardFlow ? simData->outlet() : simData->inlet()); + + // Compare + for (unsigned int i = 0; i < simData->numDataPoints() * simData->numComponents() * simData->numInletPorts(); ++i, ++outlet) + { + // Note that the simulation only saves the chromatogram at multiples of 2 (i.e., 0s, 2s, 4s, ...) + // whereas the reference solution is given at every second (0s, 1s, 2s, 3s, ...) + // Thus, we only take the even indices of the reference array + CAPTURE(time[2 * i]); + CHECK((*outlet) == makeApprox(ref[2 * i], relTol, absTol)); + } + } + } + void testAnalyticNonBindingBenchmark(const char* uoType, const char* refFileRelPath, bool forwardFlow, unsigned int nCol, double absTol, double relTol) { const std::string fwdStr = (forwardFlow ? "forward" : "backward"); diff --git a/test/ColumnTests.hpp b/test/ColumnTests.hpp index 88f91c93f..4a811d41a 100644 --- a/test/ColumnTests.hpp +++ b/test/ColumnTests.hpp @@ -42,6 +42,16 @@ namespace column */ void setNumAxialCells(cadet::JsonParameterProvider& jpp, unsigned int nCol, std::string unitID="000"); + /** + * @brief Sets the discrete points, polynomial basis in a configuration of a column-like unit operation + * @details Overwrites the POLYNOMIAL_BASIS, POLYDEG, NCOL fields in the discretization group of the given ParameterProvider. + * @param [in,out] jpp ParameterProvider to change the number of axial cells in + * @param [in] basis type of polynomial basis functions + * @param [in] polyDeg Number of axial nodes per cell + * @param [in] nCol Number of axial cells + */ + void setDG(cadet::JsonParameterProvider& jpp, std::string basis, unsigned int polyDeg, unsigned int nCol); + /** * @brief Sets the WENO order in a configuration of a column-like unit operation * @details Overwrites the WENO_ORDER field in the weno group of the given ParameterProvider. @@ -88,6 +98,7 @@ namespace column * @param [in] relTol Relative error tolerance */ void testAnalyticBenchmark(const char* uoType, const char* refFileRelPath, bool forwardFlow, bool dynamicBinding, unsigned int nCol, double absTol, double relTol); + void testAnalyticBenchmark_DG(const char* uoType, const char* refFileRelPath, bool forwardFlow, bool dynamicBinding, std::string basis, unsigned int polyDeg, unsigned int nCol, double absTol, double relTol); /** * @brief Runs a simulation test comparing against (semi-)analytic single component pulse injection reference data diff --git a/test/LumpedRateModelWithoutPoresDG.cpp b/test/LumpedRateModelWithoutPoresDG.cpp new file mode 100644 index 000000000..7ab0dd466 --- /dev/null +++ b/test/LumpedRateModelWithoutPoresDG.cpp @@ -0,0 +1,112 @@ +// ============================================================================= +// CADET +// +// Copyright © 2008-2021: The CADET Authors +// Please see the AUTHORS and CONTRIBUTORS file. +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the GNU Public License v3.0 (or, at +// your option, any later version) which accompanies this distribution, and +// is available at http://www.gnu.org/licenses/gpl.html +// ============================================================================= + +#include +#include "Approx.hpp" +#include "cadet/cadet.hpp" +#include "Logging.hpp" +#include "ColumnTests.hpp" +#include "Utils.hpp" +#include "SimHelper.hpp" +#include "ModelBuilderImpl.hpp" +#include "common/Driver.hpp" +#include "Weno.hpp" +#include "linalg/Norms.hpp" +#include "SimulationTypes.hpp" +#include "ParallelSupport.hpp" + +#include "JsonTestModels.hpp" +#include "JacobianHelper.hpp" +#include "UnitOperationTests.hpp" + +#include +#include +#include + + +namespace +{ + void setParameters(cadet::JsonParameterProvider& jpp, unsigned int nCol, unsigned int nNodes, std::string basis) + { + int level = 0; + + if (jpp.exists("model")) + { + jpp.pushScope("model"); + ++level; + } + if (jpp.exists("unit_000")) + { + jpp.pushScope("unit_000"); + ++level; + } + + jpp.pushScope("discretization"); + + // Set discretization parameters + jpp.set("NCOL", static_cast(nCol)); + jpp.set("NNODES", static_cast(nNodes)); + jpp.set("POLYNOMIAL_BASIS", basis); + + jpp.popScope(); + + for (int l = 0; l < level; ++l) + jpp.popScope(); + } + + + +} + +TEST_CASE("LRM_DG linear pulse vs analytic solution", "[LRM],[Simulation],[Analytic]") +{ + cadet::IModelBuilder* const mb = cadet::createModelBuilder(); + REQUIRE(nullptr != mb); + + // Setup simulation + bool dynamicBinding = true; + cadet::JsonParameterProvider jpp = createColumnWithTwoCompLinearBinding("LUMPED_RATE_MODEL_WITHOUT_PORES_DG"); + setParameters(jpp, 10, 2, "LAGRANGE"); + cadet::IUnitOperation* const unit = cadet::test::unitoperation::createAndConfigureUnit(jpp, *mb); + + // Disable AD + unit->useAnalyticJacobian(true); + + // Obtain memory for state, etc. + std::vector y(unit->numDofs(), 0.0); + std::vector yDot(unit->numDofs(), 0.0); + std::vector res(unit->numDofs(), 0.0); + + // Fill state vector with some values + cadet::test::util::populate(y.data(), [](unsigned int idx) { return std::abs(std::sin(idx * 0.13)) + 1e-4; }, unit->numDofs()); + cadet::test::util::populate(yDot.data(), [](unsigned int idx) { return std::abs(std::sin(idx * 0.11)) + 2e-4; }, unit->numDofs()); + + const cadet::AdJacobianParams noAdParams{nullptr, nullptr, 0u}; + const cadet::ConstSimulationState simState{y.data(), yDot.data()}; + + // Evaluate residual + const cadet::SimulationTime simTime{0.0, 0u}; + cadet::util::ThreadLocalStorage tls; + tls.resize(unit->threadLocalMemorySize()); + + unit->residualWithJacobian(simTime, simState, res.data(), noAdParams, tls); + + mb->destroyUnitOperation(unit); + destroyModelBuilder(mb); +} + +TEST_CASE("LRM_DG test", "[LRM_DG],[UnitOp]") +{ + + cadet::test::column::testAnalyticBenchmark_DG("LUMPED_RATE_MODEL_WITHOUT_PORES_DG", "/data/lrm-pulseBenchmark.data", true, true, "LAGRANGE", 3, 50, 2e-5, 1e-7); + +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index f8e1d8bd9..187456b60 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "dependencies": [ "hdf5", "suitesparse", - "superlu" + "superlu", + "eigen3" ] }