From 20e7be3389b0a0a8867eb0faae59b6e5d2116a31 Mon Sep 17 00:00:00 2001 From: Timo Koch <timo.koch@iws.uni-stuttgart.de> Date: Sat, 17 Oct 2020 23:43:06 +0200 Subject: [PATCH] [md][embedded][kernel] Replace couplmanager with a recent kernel coupling scheme Coupling methox from Koch et al 2020 JCP (https://doi.org/10.1016/j.jcp.2020.109370) --- CHANGELOG.md | 5 +- .../embedded/couplingmanager1d3d_kernel.hh | 489 +++++++++++------- .../embedded/1d3d/1p_1p/params.input | 2 +- .../embedded/1d3d/1p_1p/problem_bloodflow.hh | 57 +- .../embedded/1d3d/1p_1p/problem_tissue.hh | 140 +++-- 5 files changed, 373 insertions(+), 320 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd382bc5e8..78095cfff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,10 @@ Differences Between DuMu<sup>x</sup> 3.3 and DuMu<sup>x</sup> 3.2 - __Quadmath__: Dumux::Quad has been removed without deprecation. Use Dune::Float128 instead. - Within the RANS group, two additional runtime parameters have been included 'IsFlatWallBounded' and 'WriteFlatWallBoundedFields'. For both the K-Epsilon and Zero-eq RANS models the 'IsFlatWallBounded' runtime parameter should be set as True, -as wall topology is not supported for these models with our geometric contraints. If not set as true, the geometry -will be checked before the model is run. If either the runtime parameter or the geometry check indicate non-flat walls, +as wall topology is not supported for these models with our geometric contraints. If not set as true, the geometry +will be checked before the model is run. If either the runtime parameter or the geometry check indicate non-flat walls, the model will terminate. To add FlatWallBounded specific output to the vtk output, WriteFlatWallBoundedFields can be set as True. +- __1d3d coupling__: The kernel coupling manager has been replaced with the one from Koch et al (2020) JCP https://doi.org/10.1016/j.jcp.2020.109370 ### Deprecated properties/classes/functions/files, to be removed after 3.3: diff --git a/dumux/multidomain/embedded/couplingmanager1d3d_kernel.hh b/dumux/multidomain/embedded/couplingmanager1d3d_kernel.hh index 43932daee6..d0706a8224 100644 --- a/dumux/multidomain/embedded/couplingmanager1d3d_kernel.hh +++ b/dumux/multidomain/embedded/couplingmanager1d3d_kernel.hh @@ -36,6 +36,7 @@ #include <dumux/geometry/distance.hh> #include <dumux/multidomain/embedded/couplingmanagerbase.hh> +#include <dumux/multidomain/embedded/cylinderintegration.hh> #include <dumux/multidomain/embedded/extendedsourcestencil.hh> namespace Dumux { @@ -66,7 +67,6 @@ class Embedded1d3dCouplingManager; * \brief Manages the coupling between bulk elements and lower dimensional elements * Point sources on each integration point are computed by an AABB tree. * \note Specialization for coupling method using a distributed kernel source with 3d quantities evaluated on the line - * \note the kernel per point source is isotropic and it's integral over the domain is one */ template<class MDTraits> class Embedded1d3dCouplingManager<MDTraits, Embedded1d3dCouplingMode::Kernel> @@ -95,11 +95,23 @@ class Embedded1d3dCouplingManager<MDTraits, Embedded1d3dCouplingMode::Kernel> static constexpr bool isBox() { return GridGeometry<id>::discMethod == DiscretizationMethod::box; } + static_assert(!isBox<bulkIdx>() && !isBox<lowDimIdx>(), "The kernel coupling method is only implemented for the tpfa method"); + static_assert(Dune::Capabilities::isCartesian<typename GridView<bulkIdx>::Grid>::v, "The kernel coupling method is only implemented for structured grids"); + enum { bulkDim = GridView<bulkIdx>::dimension, lowDimDim = GridView<lowDimIdx>::dimension, dimWorld = GridView<bulkIdx>::dimensionworld }; + + // detect if a class (the spatial params) has a kernelWidthFactor() function + template <typename T, typename ...Ts> + using VariableKernelWidthDetector = decltype(std::declval<T>().kernelWidthFactor(std::declval<Ts>()...)); + + template<class T, typename ...Args> + static constexpr bool hasKernelWidthFactor() + { return Dune::Std::is_detected<VariableKernelWidthDetector, T, Args...>::value; } + public: static constexpr Embedded1d3dCouplingMode::Kernel couplingMode{}; @@ -111,6 +123,10 @@ public: { ParentType::init(bulkProblem, lowDimProblem, curSol); computeLowDimVolumeFractions(); + + const auto refinement = getParamFromGroup<int>(bulkProblem->paramGroup(), "Grid.Refinement", 0); + if (refinement > 0) + DUNE_THROW(Dune::NotImplemented, "The current intersection detection may likely fail for refined grids."); } /*! @@ -156,153 +172,128 @@ public: // initialize the maps // do some logging and profiling Dune::Timer watch; - std::cout << "Initializing the point sources..." << std::endl; - - // clear all internal members like pointsource vectors and stencils - // initializes the point source id counter - this->clear(); - bulkSourceIds_.clear(); - bulkSourceWeights_.clear(); - extendedSourceStencil_.clear(); + std::cout << "[coupling] Initializing the integration point source data structures..." << std::endl; - // precompute the vertex indices for efficiency for the box method - this->precomputeVertexIndices(bulkIdx); - this->precomputeVertexIndices(lowDimIdx); + // prepare the internal data structures + prepareDataStructures_(); + std::cout << "[coupling] Resized data structures." << std::endl; const auto& bulkGridGeometry = this->problem(bulkIdx).gridGeometry(); const auto& lowDimGridGeometry = this->problem(lowDimIdx).gridGeometry(); - bulkSourceIds_.resize(this->gridView(bulkIdx).size(0)); - bulkSourceWeights_.resize(this->gridView(bulkIdx).size(0)); + // generate a bunch of random vectors and values for + // Monte-carlo integration on the cylinder defined by line and radius + static const double characteristicRelativeLength = getParam<double>("MixedDimension.KernelIntegrationCRL", 0.1); + EmbeddedCoupling::CylinderIntegration<Scalar> cylIntegration(characteristicRelativeLength, 1); - // intersect the bounding box trees - this->glueGrids(); + static const bool writeIntegrationPointsToFile = getParam<bool>("MixedDimension.WriteIntegrationPointsToFile", false); + if (writeIntegrationPointsToFile) + { + std::ofstream ipPointFile("kernel_points.log", std::ios::trunc); + ipPointFile << "x,y,z\n"; + std::cout << "[coupling] Initialized kernel_points.log." << std::endl; + } - this->pointSourceData().reserve(this->glue().size()); - this->averageDistanceToBulkCell().reserve(this->glue().size()); - const Scalar kernelWidth = getParam<Scalar>("MixedDimension.KernelWidth"); for (const auto& is : intersections(this->glue())) { // all inside elements are identical... const auto& inside = is.targetEntity(0); // get the intersection geometry for integrating over it const auto intersectionGeometry = is.geometry(); - - // get the Gaussian quadrature rule for the local intersection - const auto& quad = Dune::QuadratureRules<Scalar, lowDimDim>::rule(intersectionGeometry.type(), order); const auto lowDimElementIdx = lowDimGridGeometry.elementMapper().index(inside); - // iterate over all quadrature points and place a source - // for 1d: make a new point source - // for 3d: make a new kernel volume source - for (auto&& qp : quad) + // for each intersection integrate kernel and add: + // * 1d: a new point source + // * 3d: a new kernel volume source + const auto radius = this->problem(lowDimIdx).spatialParams().radius(lowDimElementIdx); + const auto kernelWidthFactor = kernelWidthFactor_(this->problem(lowDimIdx).spatialParams(), lowDimElementIdx); + const auto kernelWidth = kernelWidthFactor*radius; + const auto a = intersectionGeometry.corner(0); + const auto b = intersectionGeometry.corner(1); + cylIntegration.setGeometry(a, b, kernelWidth); + + // we can have multiple 3d elements per 1d intersection + for (int outsideIdx = 0; outsideIdx < is.numDomainNeighbors(); ++outsideIdx) { - // compute the coupling stencils - for (int outsideIdx = 0; outsideIdx < is.numDomainNeighbors(); ++outsideIdx) + // compute source id + // for each id we have + // (1) a point source for the 1d domain + // (2) a bulk source weight for the each element in the 3d domain (the fraction of the total source/sink for the 1d element with the point source) + // TODO: i.e. one integration of the kernel should be enough (for each of the weights it's weight/embeddings) + // (3) the flux scaling factor for each outside element, i.e. each id + const auto id = this->idCounter_++; + + // compute the weights for the bulk volume sources + const auto& outside = is.domainEntity(outsideIdx); + const auto bulkElementIdx = bulkGridGeometry.elementMapper().index(outside); + const auto surfaceFactor = computeBulkSource(intersectionGeometry, radius, kernelWidth, id, lowDimElementIdx, bulkElementIdx, cylIntegration, is.numDomainNeighbors()); + + // place a point source at the intersection center + const auto center = intersectionGeometry.center(); + this->pointSources(lowDimIdx).emplace_back(center, id, surfaceFactor, intersectionGeometry.volume(), lowDimElementIdx); + this->pointSources(lowDimIdx).back().setEmbeddings(is.numDomainNeighbors()); + + // pre compute additional data used for the evaluation of + // the actual solution dependent source term + PointSourceData psData; + + // lowdim interpolation (evaluate at center) + if constexpr (isBox<lowDimIdx>()) { - const auto& outside = is.domainEntity(outsideIdx); - const auto bulkElementIdx = bulkGridGeometry.elementMapper().index(outside); - - // each quadrature point will be a point source for the sub problem - const auto globalPos = intersectionGeometry.global(qp.position()); - const auto id = this->idCounter_++; - const auto qpweight = qp.weight(); - const auto ie = intersectionGeometry.integrationElement(qp.position()); - this->pointSources(lowDimIdx).emplace_back(globalPos, id, qpweight, ie, std::vector<std::size_t>({lowDimElementIdx})); - this->pointSources(lowDimIdx).back().setEmbeddings(is.numDomainNeighbors()); - computeBulkSource(globalPos, kernelWidth, id, lowDimElementIdx, bulkElementIdx, qpweight*ie/is.numDomainNeighbors()); - - // pre compute additional data used for the evaluation of - // the actual solution dependent source term - PointSourceData psData; - - if constexpr (isBox<lowDimIdx>()) - { - using ShapeValues = std::vector<Dune::FieldVector<Scalar, 1> >; - const auto lowDimGeometry = lowDimGridGeometry.element(lowDimElementIdx).geometry(); - ShapeValues shapeValues; - this->getShapeValues(lowDimIdx, lowDimGridGeometry, lowDimGeometry, globalPos, shapeValues); - psData.addLowDimInterpolation(shapeValues, this->vertexIndices(lowDimIdx, lowDimElementIdx), lowDimElementIdx); - } - else - { - psData.addLowDimInterpolation(lowDimElementIdx); - } - - // add data needed to compute integral over the circle - if constexpr (isBox<bulkIdx>()) - { - using ShapeValues = std::vector<Dune::FieldVector<Scalar, 1> >; - const auto bulkGeometry = bulkGridGeometry.element(bulkElementIdx).geometry(); - ShapeValues shapeValues; - this->getShapeValues(bulkIdx, bulkGridGeometry, bulkGeometry, globalPos, shapeValues); - psData.addBulkInterpolation(shapeValues, this->vertexIndices(bulkIdx, bulkElementIdx), bulkElementIdx); - } - else - { - psData.addBulkInterpolation(bulkElementIdx); - } - - // publish point source data in the global vector - this->pointSourceData().emplace_back(std::move(psData)); - - // compute average distance to bulk cell - this->averageDistanceToBulkCell().push_back(averageDistancePointGeometry(globalPos, outside.geometry())); - - // export the lowdim coupling stencil - // we insert all vertices / elements and make it unique later - if (isBox<bulkIdx>()) - { - const auto& vertices = this->vertexIndices(bulkIdx, bulkElementIdx); - this->couplingStencils(lowDimIdx)[lowDimElementIdx].insert(this->couplingStencils(lowDimIdx)[lowDimElementIdx].end(), - vertices.begin(), vertices.end()); - } - else - { - this->couplingStencils(lowDimIdx)[lowDimElementIdx].push_back(bulkElementIdx); - } + using ShapeValues = std::vector<Dune::FieldVector<Scalar, 1> >; + const auto lowDimGeometry = this->problem(lowDimIdx).gridGeometry().element(lowDimElementIdx).geometry(); + ShapeValues shapeValues; + this->getShapeValues(lowDimIdx, this->problem(lowDimIdx).gridGeometry(), lowDimGeometry, center, shapeValues); + psData.addLowDimInterpolation(shapeValues, this->vertexIndices(lowDimIdx, lowDimElementIdx), lowDimElementIdx); + } + else + { + psData.addLowDimInterpolation(lowDimElementIdx); } - } - } - // make extra stencils unique - for (auto&& stencil : extendedSourceStencil_.stencil()) - { - std::sort(stencil.second.begin(), stencil.second.end()); - stencil.second.erase(std::unique(stencil.second.begin(), stencil.second.end()), stencil.second.end()); + // bulk interpolation (evaluate at center) + if constexpr (isBox<bulkIdx>()) + { + using ShapeValues = std::vector<Dune::FieldVector<Scalar, 1> >; + const auto bulkGeometry = this->problem(bulkIdx).gridGeometry().element(bulkElementIdx).geometry(); + ShapeValues shapeValues; + this->getShapeValues(bulkIdx, this->problem(bulkIdx).gridGeometry(), bulkGeometry, center, shapeValues); + psData.addBulkInterpolation(shapeValues, this->vertexIndices(bulkIdx, bulkElementIdx), bulkElementIdx); + } + else + { + psData.addBulkInterpolation(bulkElementIdx); + } - // remove the vertices element (box) - if (isBox<bulkIdx>()) - { - const auto& indices = this->vertexIndices(bulkIdx, stencil.first); - stencil.second.erase(std::remove_if(stencil.second.begin(), stencil.second.end(), - [&](auto i){ return std::find(indices.begin(), indices.end(), i) != indices.end(); }), - stencil.second.end()); - } - // remove the own element (cell-centered) - else - { - stencil.second.erase(std::remove_if(stencil.second.begin(), stencil.second.end(), - [&](auto i){ return i == stencil.first; }), - stencil.second.end()); + // publish point source data in the global vector + this->pointSourceData().emplace_back(std::move(psData)); + + const auto avgMinDist = averageDistanceSegmentGeometry(a, b, outside.geometry()); + this->averageDistanceToBulkCell().push_back(avgMinDist); + fluxScalingFactor_.push_back(this->problem(bulkIdx).fluxScalingFactor(avgMinDist, radius, kernelWidth)); + + // export the lowdim coupling stencil + // we insert all vertices / elements and make it unique later + if constexpr (isBox<bulkIdx>()) + { + const auto& vertices = this->vertexIndices(bulkIdx, bulkElementIdx); + this->couplingStencils(lowDimIdx)[lowDimElementIdx].insert(this->couplingStencils(lowDimIdx)[lowDimElementIdx].end(), + vertices.begin(), vertices.end()); + } + else + { + this->couplingStencils(lowDimIdx)[lowDimElementIdx].push_back(bulkElementIdx); + } } } - // make stencils unique - using namespace Dune::Hybrid; - forEach(integralRange(Dune::index_constant<2>{}), [&](const auto domainIdx) - { - for (auto&& stencil : this->couplingStencils(domainIdx)) - { - std::sort(stencil.second.begin(), stencil.second.end()); - stencil.second.erase(std::unique(stencil.second.begin(), stencil.second.end()), stencil.second.end()); - } - }); + // make the stencils unique + makeUniqueStencil_(); if (!this->pointSources(bulkIdx).empty()) DUNE_THROW(Dune::InvalidStateException, "Kernel method shouldn't have point sources in the bulk domain but only volume sources!"); - std::cout << "took " << watch.elapsed() << " seconds." << std::endl; + std::cout << "[coupling] Finished preparing manager in " << watch.elapsed() << " seconds." << std::endl; } //! Compute the low dim volume fraction in the bulk domain cells @@ -362,95 +353,226 @@ public: } //! return all source ids for a bulk elements - const std::vector<std::size_t> bulkSourceIds(GridIndex<bulkIdx> eIdx) const - { return bulkSourceIds_[eIdx]; } + const std::vector<std::size_t>& bulkSourceIds(GridIndex<bulkIdx> eIdx, int scvIdx = 0) const + { return bulkSourceIds_[eIdx][scvIdx]; } //! return all source ids for a bulk elements - const std::vector<Scalar> bulkSourceWeights(GridIndex<bulkIdx> eIdx) const - { return bulkSourceWeights_[eIdx]; } + const std::vector<Scalar>& bulkSourceWeights(GridIndex<bulkIdx> eIdx, int scvIdx = 0) const + { return bulkSourceWeights_[eIdx][scvIdx]; } + + //! The flux scaling factor for a source with id + Scalar fluxScalingFactor(std::size_t id) const + { return fluxScalingFactor_[id]; } // \} private: - //! TODO: How to optimize this? - void computeBulkSource(const GlobalPosition& globalPos, const Scalar kernelWidth, - std::size_t id, GridIndex<lowDimIdx> lowDimElementIdx, GridIndex<bulkIdx> coupledBulkElementIdx, - Scalar pointSourceWeight) + //! compute the kernel distributed sources and add stencils + template<class Line, class CylIntegration> + Scalar computeBulkSource(const Line& line, const Scalar radius, const Scalar kernelWidth, + std::size_t id, GridIndex<lowDimIdx> lowDimElementIdx, GridIndex<bulkIdx> coupledBulkElementIdx, + const CylIntegration& cylIntegration, int embeddings) { - // make sure it is mass conservative - // i.e. the point source in the 1d domain needs to have the exact same integral as the distributed - // kernel source integral in the 3d domain. Correct the integration formula by balancing the error - // by scaling the kernel with the checksum inverse - Scalar checkSum = 0.0; - std::vector<bool> mask(this->gridView(bulkIdx).size(0), false); - for (const auto& element : elements(this->gridView(bulkIdx))) + // Monte-carlo integration on the cylinder defined by line and radius + static const auto bulkParamGroup = this->problem(bulkIdx).paramGroup(); + static const auto min = getParamFromGroup<GlobalPosition>(bulkParamGroup, "Grid.LowerLeft"); + static const auto max = getParamFromGroup<GlobalPosition>(bulkParamGroup, "Grid.UpperRight"); + static const auto cells = getParamFromGroup<std::array<int, bulkDim>>(bulkParamGroup, "Grid.Cells"); + const auto cylSamples = cylIntegration.size(); + const auto& a = line.corner(0); + const auto& b = line.corner(1); + + // optionally write debugging / visual output of the integration points + static const auto writeIntegrationPointsToFile = getParam<bool>("MixedDimension.WriteIntegrationPointsToFile", false); + if (writeIntegrationPointsToFile) { - Scalar weight = 0.0; - const auto geometry = element.geometry(); - const auto& quad = Dune::QuadratureRules<Scalar, bulkDim>::rule(geometry.type(), 3); - for (auto&& qp : quad) + std::ofstream ipPointFile("kernel_points.log", std::ios::app); + for (int i = 0; i < cylSamples; ++i) { - const auto qpweight = qp.weight(); - const auto ie = geometry.integrationElement(qp.position()); - weight += evalKernel(globalPos, geometry.global(qp.position()), kernelWidth)*qpweight*ie; + const auto& point = cylIntegration.integrationPoint(i); + if (const bool hasIntersection = intersectsPointBoundingBox(point, min, max); hasIntersection) + ipPointFile << point[0] << "," << point[1] << "," << point[2] << '\n'; } + } - if (weight > 1e-13) + Scalar integral = 0.0; + for (int i = 0; i < cylSamples; ++i) + { + const auto& point = cylIntegration.integrationPoint(i); + // TODO: below only works for Cartesian grids with ijk numbering (e.g. level 0 YaspGrid (already fails when refined)) + // more general is the bounding box tree solution which always works, however it's much slower + //const auto bulkIndices = intersectingEntities(point, this->problem(bulkIdx).gridGeometry().boundingBoxTree(), true); + if (const bool hasIntersection = intersectsPointBoundingBox(point, min, max); hasIntersection) { - const auto& bulkGridGeometry = this->problem(bulkIdx).gridGeometry(); - const auto bulkElementIdx = bulkGridGeometry.elementMapper().index(element); - bulkSourceIds_[bulkElementIdx].push_back(id); - bulkSourceWeights_[bulkElementIdx].push_back(weight*pointSourceWeight); - mask[bulkElementIdx] = true; - - // add lowDim dofs that the source is related to to the bulk stencil - if (isBox<lowDimIdx>()) + const auto bulkElementIdx = intersectingEntityCartesianGrid(point, min, max, cells); + assert(bulkElementIdx < this->problem(bulkIdx).gridGeometry().gridView().size(0)); + const auto localWeight = evalConstKernel_(a, b, point, radius, kernelWidth)*cylIntegration.integrationElement(i)/Scalar(embeddings); + integral += localWeight; + if (!bulkSourceIds_[bulkElementIdx][0].empty() && id == bulkSourceIds_[bulkElementIdx][0].back()) { - const auto& vertices = this->vertexIndices(lowDimIdx, lowDimElementIdx); - this->couplingStencils(bulkIdx)[bulkElementIdx].insert(this->couplingStencils(bulkIdx)[bulkElementIdx].end(), - vertices.begin(), vertices.end()); - + bulkSourceWeights_[bulkElementIdx][0].back() += localWeight; } else { - this->couplingStencils(bulkIdx)[bulkElementIdx].push_back(lowDimElementIdx); + bulkSourceIds_[bulkElementIdx][0].emplace_back(id); + bulkSourceWeights_[bulkElementIdx][0].emplace_back(localWeight); + addBulkSourceStencils_(bulkElementIdx, lowDimElementIdx, coupledBulkElementIdx); } + } + } + + // the surface factor (which fraction of the source is inside the domain and needs to be considered) + const auto length = (a-b).two_norm()/Scalar(embeddings); + return integral/length; + } + + void prepareDataStructures_() + { + // clear all internal members like pointsource vectors and stencils + // initializes the point source id counter + this->clear(); + bulkSourceIds_.clear(); + bulkSourceWeights_.clear(); + extendedSourceStencil_.stencil().clear(); + + // precompute the vertex indices for efficiency for the box method + this->precomputeVertexIndices(bulkIdx); + this->precomputeVertexIndices(lowDimIdx); + + bulkSourceIds_.resize(this->gridView(bulkIdx).size(0)); + bulkSourceWeights_.resize(this->gridView(bulkIdx).size(0)); + + // intersect the bounding box trees + this->glueGrids(); + + // reserve memory for data + this->pointSourceData().reserve(this->glue().size()); + this->averageDistanceToBulkCell().reserve(this->glue().size()); + fluxScalingFactor_.reserve(this->glue().size()); + + // reserve memory for stencils + const auto numBulkElements = this->gridView(bulkIdx).size(0); + for (GridIndex<bulkIdx> bulkElementIdx = 0; bulkElementIdx < numBulkElements; ++bulkElementIdx) + { + this->couplingStencils(bulkIdx)[bulkElementIdx].reserve(10); + extendedSourceStencil_.stencil()[bulkElementIdx].reserve(10); + bulkSourceIds_[bulkElementIdx][0].reserve(10); + bulkSourceWeights_[bulkElementIdx][0].reserve(10); + } + } - // tpfa - extendedSourceStencil_.stencil()[bulkElementIdx].push_back(coupledBulkElementIdx); + //! Make the stencils unique + void makeUniqueStencil_() + { + // make extra stencils unique + for (auto&& stencil : extendedSourceStencil_.stencil()) + { + std::sort(stencil.second.begin(), stencil.second.end()); + stencil.second.erase(std::unique(stencil.second.begin(), stencil.second.end()), stencil.second.end()); - // compute check sum -> should sum up to 1.0 to be mass conservative - checkSum += weight; + // remove the vertices element (box) + if constexpr (isBox<bulkIdx>()) + { + const auto& indices = this->vertexIndices(bulkIdx, stencil.first); + stencil.second.erase(std::remove_if(stencil.second.begin(), stencil.second.end(), + [&](auto i){ return std::find(indices.begin(), indices.end(), i) != indices.end(); }), + stencil.second.end()); + } + // remove the own element (cell-centered) + else + { + stencil.second.erase(std::remove_if(stencil.second.begin(), stencil.second.end(), + [&](auto i){ return i == stencil.first; }), + stencil.second.end()); } } - for (GridIndex<bulkIdx> eIdx = 0; eIdx < bulkSourceWeights_.size(); ++eIdx) - if (mask[eIdx]) bulkSourceWeights_[eIdx].back() /= checkSum; + // make stencils unique + using namespace Dune::Hybrid; + forEach(integralRange(Dune::index_constant<2>{}), [&](const auto domainIdx) + { + for (auto&& stencil : this->couplingStencils(domainIdx)) + { + std::sort(stencil.second.begin(), stencil.second.end()); + stencil.second.erase(std::unique(stencil.second.begin(), stencil.second.end()), stencil.second.end()); + } + }); + } + + //! add additional stencil entries for the bulk element + void addBulkSourceStencils_(GridIndex<bulkIdx> bulkElementIdx, GridIndex<lowDimIdx> coupledLowDimElementIdx, GridIndex<bulkIdx> coupledBulkElementIdx) + { + // add the lowdim element to the coupling stencil of this bulk element + if constexpr (isBox<lowDimIdx>()) + { + const auto& vertices = this->vertexIndices(lowDimIdx, coupledLowDimElementIdx); + this->couplingStencils(bulkIdx)[bulkElementIdx].insert(this->couplingStencils(bulkIdx)[bulkElementIdx].end(), + vertices.begin(), vertices.end()); + + } + else + { + auto& s = this->couplingStencils(bulkIdx)[bulkElementIdx]; + s.push_back(coupledLowDimElementIdx); + } - // balance error of the quadrature rule -> TODO: what to do at boundaries - // const auto diff = 1.0 - checkSum; - // std::cout << "Integrated kernel with integration error of " << diff << std::endl; + // the extended source stencil, every 3d element with a source is coupled to + // the element/dofs where the 3d quantities are measured + if constexpr (isBox<bulkIdx>()) + { + const auto& vertices = this->vertexIndices(bulkIdx, coupledBulkElementIdx); + extendedSourceStencil_.stencil()[bulkElementIdx].insert(extendedSourceStencil_.stencil()[bulkElementIdx].end(), + vertices.begin(), vertices.end()); + } + else + { + auto& s = extendedSourceStencil_.stencil()[bulkElementIdx]; + s.push_back(coupledBulkElementIdx); + } } - //! an isotropic cubic kernel with derivatives 0 at r=origin and r=width and domain integral 1 - inline Scalar evalKernel(const GlobalPosition& origin, - const GlobalPosition& pos, - const Scalar width) const noexcept + //! a cylindrical kernel around the segment a->b + Scalar evalConstKernel_(const GlobalPosition& a, + const GlobalPosition& b, + const GlobalPosition& point, + const Scalar R, + const Scalar rho) const noexcept { - const auto r = (pos-origin).two_norm(); - const auto r2 = r*r; - const auto r3 = r2*r; + // projection of point onto line a + t*(b-a) + const auto ab = b - a; + const auto t = (point - a)*ab/ab.two_norm2(); + + // return 0 if we are outside cylinder + if (t < 0.0 || t > 1.0) + return 0.0; + + // compute distance + auto proj = a; proj.axpy(t, ab); + const auto r = (proj - point).two_norm(); - if (r > width) + if (r > rho) return 0.0; - const Scalar w2 = width*width; - const Scalar w3 = w2*width; - const Scalar k = 15.0/(4*M_PI*w3); - const Scalar a = 2.0/w3; - const Scalar b = 3.0/w2; + return 1.0/(M_PI*rho*rho); + } + + /*! + * \brief Get the kernel width factor from the spatial params (if possible) + */ + template<class SpatialParams> + auto kernelWidthFactor_(const SpatialParams& spatialParams, unsigned int eIdx) + -> std::enable_if_t<hasKernelWidthFactor<SpatialParams, unsigned int>(), Scalar> + { return spatialParams.kernelWidthFactor(eIdx); } - return k*(a*r3 - b*r2 + 1.0); + /*! + * \brief Get the kernel width factor (constant) from the input file (if possible) + */ + template<class SpatialParams> + auto kernelWidthFactor_(const SpatialParams& spatialParams, unsigned int eIdx) + -> std::enable_if_t<!hasKernelWidthFactor<SpatialParams, unsigned int>(), Scalar> + { + static const Scalar kernelWidthFactor = getParam<Scalar>("MixedDimension.KernelWidthFactor"); + return kernelWidthFactor; } //! the extended source stencil object for kernel coupling @@ -458,9 +580,12 @@ private: //! vector for the volume fraction of the lowdim domain in the bulk domain cells std::vector<Scalar> lowDimVolumeInBulkElement_; //! kernel sources to integrate for each bulk element - std::vector<std::vector<std::size_t>> bulkSourceIds_; + std::vector<std::array<std::vector<std::size_t>, isBox<bulkIdx>() ? 1<<bulkDim : 1>> bulkSourceIds_; //! the integral of the kernel for each point source / integration point, i.e. weight for the source - std::vector<std::vector<Scalar>> bulkSourceWeights_; + std::vector<std::array<std::vector<Scalar>, isBox<bulkIdx>() ? 1<<bulkDim : 1>> bulkSourceWeights_; + + //! the flux scaling factor for the respective source id + std::vector<Scalar> fluxScalingFactor_; }; } // end namespace Dumux diff --git a/test/multidomain/embedded/1d3d/1p_1p/params.input b/test/multidomain/embedded/1d3d/1p_1p/params.input index 5ba10acd67..3da443d15c 100644 --- a/test/multidomain/embedded/1d3d/1p_1p/params.input +++ b/test/multidomain/embedded/1d3d/1p_1p/params.input @@ -1,7 +1,7 @@ [MixedDimension] NumCircleSegments = 100 IntegrationOrder = 2 -KernelWidth = 0.3 +KernelWidthFactor = 1.0 [Vessel.Grid] LowerLeft = 0 0 0 diff --git a/test/multidomain/embedded/1d3d/1p_1p/problem_bloodflow.hh b/test/multidomain/embedded/1d3d/1p_1p/problem_bloodflow.hh index b9c0ab852a..06bffeb9a1 100644 --- a/test/multidomain/embedded/1d3d/1p_1p/problem_bloodflow.hh +++ b/test/multidomain/embedded/1d3d/1p_1p/problem_bloodflow.hh @@ -256,9 +256,7 @@ public: * the absolute rate mass generated or annihilated in kg/s. Positive values mean * that mass is created, negative ones mean that it vanishes. */ - template<class ElementVolumeVariables, - bool enable = (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel), - std::enable_if_t<!enable, int> = 0> + template<class ElementVolumeVariables> void pointSource(PointSource& source, const Element &element, const FVElementGeometry& fvGeometry, @@ -272,60 +270,13 @@ public: // calculate the source const Scalar radius = this->couplingManager().radius(source.id()); const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); - const Scalar sourceValue = beta*(pressure3D - pressure1D);//*bulkVolVars.density(); + Scalar sourceValue = beta*(pressure3D - pressure1D); + if constexpr(CouplingManager::couplingMode == EmbeddedCouplingMode::kernel) + sourceValue *= this->couplingManager().fluxScalingFactor(source.id()); source = sourceValue*source.quadratureWeight()*source.integrationElement(); } - //! Specialization for kernel method - template<class ElementVolumeVariables, - bool enable = (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel), - std::enable_if_t<enable, int> = 0> - void pointSource(PointSource& source, - const Element &element, - const FVElementGeometry& fvGeometry, - const ElementVolumeVariables& elemVolVars, - const SubControlVolume &scv) const - { - static const Scalar kernelWidth = getParam<Scalar>("MixedDimension.KernelWidth"); - static const Scalar kernelFactor = std::log(kernelWidth)-9.0/10.0; - - // compute source at every integration point - const Scalar pressure1D = this->couplingManager().lowDimPriVars(source.id())[Indices::pressureIdx]; - const Scalar pressure3D = this->couplingManager().bulkPriVars(source.id())[Indices::pressureIdx]; - - // calculate the source - const Scalar radius = this->couplingManager().radius(source.id()); - const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); - const Scalar sourceValue = beta*(pressure3D / kernelFactor * std::log(radius) - pressure1D);//*bulkVolVars.density(); - - source = sourceValue*source.quadratureWeight()*source.integrationElement(); - } - - //! Evaluate coupling residual for the derivative bulk DOF with respect to low dim DOF - //! We only need to evaluate the part of the residual that will be influence by the low dim DOF - template<class MatrixBlock, class VolumeVariables> - void addSourceDerivatives(MatrixBlock& block, - const Element& element, - const FVElementGeometry& fvGeometry, - const VolumeVariables& curElemVolVars, - const SubControlVolume& scv) const - { - const auto eIdx = this->gridGeometry().elementMapper().index(element); - - auto key = std::make_pair(eIdx, 0); - if (this->pointSourceMap().count(key)) - { - // call the solDependent function. Herein the user might fill/add values to the point sources - // we make a copy of the local point sources here - auto pointSources = this->pointSourceMap().at(key); - - // add the point source values to the local residual (negative sign is convention for source term) - for (const auto& source : pointSources) - block[0][0] -= this->couplingManager().pointSourceDerivative(source, Dune::index_constant<1>{}, Dune::index_constant<1>{}); - } - } - /*! * \brief Evaluates the initial value for a control volume. * diff --git a/test/multidomain/embedded/1d3d/1p_1p/problem_tissue.hh b/test/multidomain/embedded/1d3d/1p_1p/problem_tissue.hh index 834e0e1b9e..65f307a5d9 100644 --- a/test/multidomain/embedded/1d3d/1p_1p/problem_tissue.hh +++ b/test/multidomain/embedded/1d3d/1p_1p/problem_tissue.hh @@ -247,15 +247,18 @@ public: const ElementVolumeVariables& elemVolVars, const SubControlVolume &scv) const { - // compute source at every integration point - const Scalar pressure3D = this->couplingManager().bulkPriVars(source.id())[Indices::pressureIdx]; - const Scalar pressure1D = this->couplingManager().lowDimPriVars(source.id())[Indices::pressureIdx]; - - // calculate the source - const Scalar radius = this->couplingManager().radius(source.id()); - const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); - const Scalar sourceValue = beta*(pressure1D - pressure3D); - source = sourceValue*source.quadratureWeight()*source.integrationElement(); + if constexpr (CouplingManager::couplingMode != Embedded1d3dCouplingMode::kernel) + { + // compute source at every integration point + const Scalar pressure3D = this->couplingManager().bulkPriVars(source.id())[Indices::pressureIdx]; + const Scalar pressure1D = this->couplingManager().lowDimPriVars(source.id())[Indices::pressureIdx]; + + // calculate the source + const Scalar radius = this->couplingManager().radius(source.id()); + const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); + const Scalar sourceValue = beta*(pressure1D - pressure3D); + source = sourceValue*source.quadratureWeight()*source.integrationElement(); + } } /*! @@ -276,77 +279,51 @@ public: * that the conserved quantity is created, negative ones mean that it vanishes. * E.g. for the mass balance that would be a mass rate in \f$ [ kg / (m^3 \cdot s)] \f$. */ - template<class ElementVolumeVariables, - bool enable = (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel), - std::enable_if_t<enable, int> = 0> + template<class ElementVolumeVariables> NumEqVector source(const Element &element, const FVElementGeometry& fvGeometry, const ElementVolumeVariables& elemVolVars, const SubControlVolume &scv) const { - static const Scalar kernelWidth = getParam<Scalar>("MixedDimension.KernelWidth"); - static const Scalar kernelFactor = std::log(kernelWidth)-9.0/10.0; - NumEqVector source(0.0); - const auto eIdx = this->gridGeometry().elementMapper().index(element); - const auto& sourceIds = this->couplingManager().bulkSourceIds(eIdx); - const auto& sourceWeights = this->couplingManager().bulkSourceWeights(eIdx); - - for (int i = 0; i < sourceIds.size(); ++i) + if constexpr(CouplingManager::couplingMode == EmbeddedCouplingMode::kernel) { - const auto id = sourceIds[i]; - const auto weight = sourceWeights[i]; - - const Scalar radius = this->couplingManager().radius(id); - const Scalar pressure3D = this->couplingManager().bulkPriVars(id)[Indices::pressureIdx]; - const Scalar pressure1D = this->couplingManager().lowDimPriVars(id)[Indices::pressureIdx]; + const auto eIdx = this->gridGeometry().elementMapper().index(element); + const auto& sourceIds = this->couplingManager().bulkSourceIds(eIdx, scv.indexInElement()); + const auto& sourceWeights = this->couplingManager().bulkSourceWeights(eIdx, scv.indexInElement()); - // calculate the source - const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); - const Scalar sourceValue = beta*(pressure1D - pressure3D / kernelFactor * std::log(radius)); + for (int i = 0; i < sourceIds.size(); ++i) + { + const auto id = sourceIds[i]; + const auto weight = sourceWeights[i]; + const auto xi = this->couplingManager().fluxScalingFactor(id); + + const Scalar radius = this->couplingManager().radius(id); + const Scalar pressure3D = this->couplingManager().bulkPriVars(id)[Indices::pressureIdx]; + const Scalar pressure1D = this->couplingManager().lowDimPriVars(id)[Indices::pressureIdx]; + + // calculate the source + static const Scalar beta = 2*M_PI/(2*M_PI + std::log(radius)); + const Scalar sourceValue = beta*(pressure1D - pressure3D)*xi; + source[Indices::conti0EqIdx] += sourceValue*weight; + } - source[Indices::conti0EqIdx] += sourceValue*weight; + const auto volume = scv.volume()*elemVolVars[scv].extrusionFactor(); + source[Indices::conti0EqIdx] /= volume; } - const auto volume = scv.volume()*elemVolVars[scv].extrusionFactor(); - source[Indices::conti0EqIdx] /= volume; - return source; } - //! Other methods - template<class ElementVolumeVariables, - bool enable = (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel), - std::enable_if_t<!enable, int> = 0> - NumEqVector source(const Element &element, - const FVElementGeometry& fvGeometry, - const ElementVolumeVariables& elemVolVars, - const SubControlVolume &scv) const - { return NumEqVector(0.0); } - - //! Evaluate coupling residual for the derivative bulk DOF with respect to low dim DOF - //! We only need to evaluate the part of the residual that will be influence by the low dim DOF - template<class MatrixBlock, class VolumeVariables> - void addSourceDerivatives(MatrixBlock& block, - const Element& element, - const FVElementGeometry& fvGeometry, - const VolumeVariables& curElemVolVars, - const SubControlVolume& scv) const + //! compute the flux scaling factor (xi) for a distance r for a vessel with radius R and kernel width rho + Scalar fluxScalingFactor(const Scalar r, const Scalar R, const Scalar rho) const { - const auto eIdx = this->gridGeometry().elementMapper().index(element); - - auto key = std::make_pair(eIdx, 0); - if (this->pointSourceMap().count(key)) - { - // call the solDependent function. Herein the user might fill/add values to the point sources - // we make a copy of the local point sources here - auto pointSources = this->pointSourceMap().at(key); - - // add the point source values to the local residual (negative sign is convention for source term) - for (const auto& source : pointSources) - block[0][0] -= this->couplingManager().pointSourceDerivative(source, Dune::index_constant<0>{}, Dune::index_constant<0>{}); - } + using std::log; + static const Scalar beta = 2*M_PI/(2*M_PI + log(R)); + static const Scalar kernelWidthFactorLog = log(rho/R); + return r < rho ? 1.0/(1.0 + R*beta/(2*M_PI*R)*(r*r/(2*rho*rho) + kernelWidthFactorLog - 0.5)) + : 1.0/(1.0 + R*beta/(2*M_PI*R)*log(r/R)); } /*! @@ -360,33 +337,32 @@ public: PrimaryVariables initialAtPos(const GlobalPosition &globalPos) const { return PrimaryVariables(0.0); } + Scalar p1DExact(const GlobalPosition &globalPos) const + { return 1.0 + globalPos[2]; } + //! The exact solution Scalar exactSolution(const GlobalPosition &globalPos) const { - Dune::FieldVector<double, 2> xy({globalPos[0], globalPos[1]}); - if (CouplingManager::couplingMode == Embedded1d3dCouplingMode::surface) + const auto r = std::sqrt(globalPos[0]*globalPos[0] + globalPos[1]*globalPos[1]); + static const auto R = getParam<Scalar>("SpatialParams.Radius"); + + if (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel) { - static const auto R = getParam<Scalar>("SpatialParams.Radius"); - if (xy.two_norm() > R) - return -1.0*(1+globalPos[2])/(2*M_PI)*std::log(xy.two_norm()); + static const auto rho = getParam<Scalar>("MixedDimension.KernelWidthFactor")*R; + if (r > rho) + return -1.0*p1DExact(globalPos)/(2*M_PI)*std::log(r); else - return -1.0*(1+globalPos[2])/(2*M_PI)*std::log(R); - + return -1.0*p1DExact(globalPos)/(2*M_PI)*(r*r/(2.0*rho*rho) + std::log(rho) - 0.5); } - else if (CouplingManager::couplingMode == Embedded1d3dCouplingMode::kernel) + else if (CouplingManager::couplingMode == Embedded1d3dCouplingMode::surface) { - static const auto rho = getParam<Scalar>("MixedDimension.KernelWidth"); - const auto& r = xy.two_norm(); - if (xy.two_norm() >= rho) - return -1.0*(1+globalPos[2])/(2*M_PI)*std::log(xy.two_norm()); + if (r > R) + return -1.0*p1DExact(globalPos)/(2*M_PI)*std::log(r); else - return -1.0*(1+globalPos[2])/(2*M_PI)*(11.0/15.0*std::pow(r/rho, 5) - - 3.0/2.0*std::pow(r/rho, 4) - + 5.0/3.0*std::pow(r/rho, 2) - - 9.0/10.0 + std::log(rho)); + return -1.0*p1DExact(globalPos)/(2*M_PI)*std::log(R); } - else - return -1.0*(1+globalPos[2])/(2*M_PI)*std::log(xy.two_norm()); + + return -1.0*p1DExact(globalPos)/(2*M_PI)*std::log(r); } //! Called after every time step -- GitLab