diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa0908577a6a12ec3e243112f902d29079607c9..fdca7fada9833c2568db60c8543d7c120601ce32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ for (const auto& element : elements(gridGeometry.gridView())) fvGeometry.bind(element); ``` +- __Embedded coupling__: Add a coupling manager for the 1D-3D projection based scheme with resolved interface introduced in Koch 2021 (https://arxiv.org/abs/2106.06358) + ### Immediate interface changes not allowing/requiring a deprecation period: - __Virtual interface of GridDataTransfer__: The `GridDataTransfer` abstract base class now required the Grid type as a template argument. Furthermore, the `store` and `reconstruct` interface functions do now expect the grid as a function argument. This allows to correctly update grid geometries and corresponding mapper (see "Construction and update of GridGeometries changed" above in the changelog) - `PengRobinsonMixture::computeMolarVolumes` has been removed without deprecation. It was used nowhere and did not translate. diff --git a/dumux/multidomain/embedded/couplingmanager1d3d.hh b/dumux/multidomain/embedded/couplingmanager1d3d.hh index 8845f9c82a102bd457629ba0285af936738c6743..a7e1bdfd91d14228d67b77f34b5bfd0833408b5c 100644 --- a/dumux/multidomain/embedded/couplingmanager1d3d.hh +++ b/dumux/multidomain/embedded/couplingmanager1d3d.hh @@ -41,5 +41,6 @@ class Embedded1d3dCouplingManager; #include <dumux/multidomain/embedded/couplingmanager1d3d_average.hh> #include <dumux/multidomain/embedded/couplingmanager1d3d_surface.hh> #include <dumux/multidomain/embedded/couplingmanager1d3d_kernel.hh> +#include <dumux/multidomain/embedded/couplingmanager1d3d_projection.hh> #endif diff --git a/dumux/multidomain/embedded/couplingmanager1d3d_projection.hh b/dumux/multidomain/embedded/couplingmanager1d3d_projection.hh new file mode 100644 index 0000000000000000000000000000000000000000..ca29f716c47c5a4c98a3c2876d1030b112d2d46a --- /dev/null +++ b/dumux/multidomain/embedded/couplingmanager1d3d_projection.hh @@ -0,0 +1,675 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +/***************************************************************************** + * See the file COPYING for full copying permissions. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + *****************************************************************************/ +/*! + * \file + * \author Timo Koch + * \ingroup EmbeddedCoupling + * \brief Coupling manager for low-dimensional domains embedded in the bulk + * domain with spatially resolved interface. + */ + +#ifndef DUMUX_MULTIDOMAIN_EMBEDDED_COUPLINGMANAGER_1D3D_PROJECTION_HH +#define DUMUX_MULTIDOMAIN_EMBEDDED_COUPLINGMANAGER_1D3D_PROJECTION_HH + +#include <vector> +#include <algorithm> +#include <string> + +#include <dune/common/timer.hh> +#include <dune/common/exceptions.hh> +#include <dune/common/fvector.hh> +#include <dune/geometry/quadraturerules.hh> + +#include <dumux/common/tag.hh> +#include <dumux/common/properties.hh> +#include <dumux/common/math.hh> +#include <dumux/common/indextraits.hh> + +#include <dumux/geometry/refinementquadraturerule.hh> +#include <dumux/geometry/triangulation.hh> +#include <dumux/geometry/grahamconvexhull.hh> + +#include <dumux/multidomain/embedded/couplingmanagerbase.hh> +#include <dumux/multidomain/embedded/localrefinementquadrature.hh> + +namespace Dumux { + +// some implementation details of the projection coupling manager +namespace Detail { + +/*! + * \ingroup EmbeddedCoupling + * \brief Segment representation of a 1d network grid + * + * We use separate segment representation which is fast to iterate over and which + * can be used to find the unique mapping from points on coupled bulk surface facets + * to the network centerline (i.e. a 1D dof on the centerline). + */ +template <class GridGeometry> +class SegmentNetwork +{ + using GridView = typename GridGeometry::GridView; + using GlobalPosition = typename GridView::template Codim<0>::Geometry::GlobalCoordinate; + using GridIndex = typename IndexTraits<GridView>::GridIndex; + + struct Segment + { + GlobalPosition a, b; + double r; + GridIndex eIdx; + }; + +public: + template <class RadiusFunction> + SegmentNetwork(const GridGeometry& gg, const RadiusFunction& radiusFunction) + { + const auto& gridView = gg.gridView(); + segments_.resize(gridView.size(0)); + for (const auto &element : elements(gridView)) + { + const auto geometry = element.geometry(); + const auto eIdx = gg.elementMapper().index(element); + const auto radius = radiusFunction(eIdx); + + segments_[eIdx] = Segment{ geometry.corner(0), geometry.corner(1), radius, eIdx }; + } + + std::cout << "-- Extracted " << segments_.size() << " segments.\n"; + } + + //! the segment radius + auto radius(std::size_t eIdx) const + { return segments_[eIdx].r; } + + //! the segment with index eIdx + const auto& segment(std::size_t eIdx) const + { return segments_[eIdx]; } + + //! the segments + const auto& segments() const + { return segments_; } + + //! find the closest segment (the segment from which the signed distance to its surface is the closest to zero) + //! \note minDist is a positive distance + auto findClosestSegmentSurface(const GlobalPosition& globalPos) const + { + GridIndex eIdx = 0; + double minSignedDist = std::numeric_limits<double>::max(); + + for (const auto &segment : segments_) + { + const auto signedDist = computeSignedDistance_(globalPos, segment); + if (signedDist < minSignedDist) + { + minSignedDist = signedDist; + eIdx = segment.eIdx; + } + } + + using std::abs; + return std::make_tuple(eIdx, abs(minSignedDist)); + } + + //! same as overload with taking a position but only look at the segments specified by the index vector + template <class IndexRange> + auto findClosestSegmentSurface(const GlobalPosition& globalPos, + const IndexRange& segIndices) const + { + GridIndex eIdx = 0; + double minSignedDist = std::numeric_limits<double>::max(); + + for (const auto index : segIndices) + { + const auto &segment = segments_[index]; + const auto signedDist = computeSignedDistance_(globalPos, segment); + if (signedDist < minSignedDist) + { + minSignedDist = signedDist; + eIdx = segment.eIdx; + } + } + + using std::abs; + return std::make_tuple(eIdx, abs(minSignedDist)); + } + + // Compute the projection point of p onto the segment connecting a->b + GlobalPosition projectionPointOnSegment(const GlobalPosition& p, std::size_t idx) const + { + const auto& segment = segments_[idx]; + return projectionPointOnSegment_(p, segment.a, segment.b); + } + +private: + // Compute the projection of p onto the segment + // returns the closest end point if the orthogonal projection onto the line implied by the segment + // is outside the segment + GlobalPosition projectionPointOnSegment_(const GlobalPosition& p, const GlobalPosition& a, const GlobalPosition& b) const + { + const auto v = b - a; + const auto w = p - a; + + const auto proj1 = v*w; + if (proj1 <= 0.0) + return a; + + const auto proj2 = v.two_norm2(); + if (proj2 <= proj1) + return b; + + const auto t = proj1 / proj2; + auto x = a; + x.axpy(t, v); + return x; + } + + // Compute the signed (!) distance to a cylinder segment surface and the projection point onto the centerline + auto computeSignedDistance_(const GlobalPosition& p, const Segment& segment) const + { + const auto projPoint = projectionPointOnSegment_(p, segment.a, segment.b); + return (p-projPoint).two_norm()-segment.r; + } + + std::vector<Segment> segments_; +}; + +/*! + * \ingroup EmbeddedCoupling + * \brief Get the closest segment for a given surface point + * \note The point does not necessarily be exactly on the virtual surface + * implied by the networks radius function + */ +template<class Network> +class NetworkIndicatorFunction +{ +public: + NetworkIndicatorFunction(const Network& n) + : network_(n) + {} + + template<class Pos> + auto operator()(const Pos& pos) const + { + const auto& [closestSegmentIdx, _] = network_.findClosestSegmentSurface(pos); + return closestSegmentIdx; + } + + template <class Pos, class HintContainer> + auto operator()(const Pos& pos, const HintContainer& hints) const + { + const auto& [closestSegmentIdx, _] = network_.findClosestSegmentSurface(pos, hints); + return closestSegmentIdx; + } + +private: + const Network& network_; +}; + +/*! + * \ingroup EmbeddedCoupling + * \brief Simple legacy VTK writer for outputting debug data on the coupling interface + */ +class DebugIntersectionVTKOutput +{ +public: + void push(const std::array<Dune::FieldVector<double, 3>, 3>& corners, double data) + { + vtkCells_.emplace_back(std::array<std::size_t, 3>{ + { vtkPoints_.size(), vtkPoints_.size()+1, vtkPoints_.size()+2 } + }); + for (const auto& c : corners) + vtkPoints_.push_back(c); + vtkCellData_.push_back(data); + } + + void write(const std::string& name) + { + std::ofstream intersectionFile(name); + intersectionFile << "# vtk DataFile Version 2.0\n"; + intersectionFile << "Really cool intersection data\n"; + intersectionFile << "ASCII\n"; + intersectionFile << "DATASET UNSTRUCTURED_GRID\n"; + intersectionFile << "POINTS " << vtkPoints_.size() << " float\n"; + for (const auto& p : vtkPoints_) + intersectionFile << p[0] << " " << p[1] << " " << p[2] << "\n"; + intersectionFile << "\nCELLS " << vtkCells_.size() << " " << vtkCells_.size()*4 << "\n"; + for (const auto& c : vtkCells_) + intersectionFile << "3 " << c[0] << " " << c[1] << " " << c[2] << "\n"; + intersectionFile << "\nCELL_TYPES " << vtkCells_.size() << "\n"; + for (int i = 0; i < vtkCells_.size(); ++i) + intersectionFile << "5\n"; + intersectionFile << "\nCELL_DATA " << vtkCells_.size() << "\nSCALARS index float\nLOOKUP_TABLE default\n"; + for (const auto& c : vtkCellData_) + intersectionFile << c << "\n"; + } + +private: + std::vector<Dune::FieldVector<double, 3>> vtkPoints_; + std::vector<std::array<std::size_t, 3>> vtkCells_; + std::vector<double> vtkCellData_; +}; + +} // end namespace Detail + +namespace Embedded1d3dCouplingMode { +struct Projection : public Utility::Tag<Projection> { + static std::string name() { return "projection"; } +}; + +inline constexpr Projection projection{}; +} // end namespace Embedded1d3dCouplingMode + +// forward declaration +template<class MDTraits, class CouplingMode> +class Embedded1d3dCouplingManager; + +/*! + * \ingroup EmbeddedCoupling + * \brief Manages the coupling between bulk elements and lower dimensional elements + * + * The method is described in detail in the article + * "Projection-based resolved interface mixed-dimension method for embedded tubular network systems" + * by Timo Koch (2021) available at https://arxiv.org/abs/2106.06358 + * + * The network is represented as a line segment network. The bulk domain explicitly resolves the wall + * of the tube network (e.g. blood vessel wall, outer root wall), so this generally requires an unstructured grid. + * The coupling term coupling the PDEs on the network and in the bulk domain is intergrated over the + * coupling surface and each integration point couples to quantities evaluated at the closest point on the graph. + * There is a unique mapping from every point on the virtual tube surface to the tube centerline, therefore + * this coupling is determined by mapping to the closest point on the virtual surface and then evaluating + * the 1D quantity on the mapped centerline point. (The inverse mapping may be non-unique.) + * + * The original paper also includes an algorithm for computing exact intersection for the surface quadrature + * in case of simple straight vessels. However, as a comparison in the paper shows that the suggested + * approximate quadrature is exact enough (configured by parameter MixedDimension.SimplexIntegrationRefine + * specifying the number of (adaptive local) virtual refinements of a surface facet for integration), only + * the approximate general purpose algorithm is included in this implementation. Only one quadrature point + * will be added for each surface element and coupled 1D dof including all contribution evaluated by + * local refinement. That means the virtual refinement doesn't increase the number of integration points and + * additionally is also optimized by using local adaptive refinement only in the places where the index mapping + * to the 1D degree of freedom is still multivalent. + * + * Coupling sources are implement in terms of point sources at quadrature points to make it easy to reuse code + * written for other 1D-3D coupling manager modes. + * + * This algorithm can be configured by several parameters + * (although this is usually not necessary as there are sensible defaults): + * + * - MixedDimension.SimplexIntegrationRefine + * number of virtual refinement steps to determine coupled surface area + * - MixedDimension.EnableIntersectionOutput + * set to true to enable debug VTK output for intersections + * - MixedDimension.EstimateNumberOfPointSources + * provide an estimate for the expected number of coupling points for memory allocation + * - MixedDimension.ProjectionCoupledRadiusFactor + * threshold distance in which to search for coupled elements (specified as multiple of radius, default 0.1) + * - MixedDimension.ProjectionCoupledAngleFactor + * angle threshold in which to search for coupled elements (angle in radians from surface normal vector, default 0.3) + * + */ +template<class MDTraits> +class Embedded1d3dCouplingManager<MDTraits, Embedded1d3dCouplingMode::Projection> +: public EmbeddedCouplingManagerBase<MDTraits, Embedded1d3dCouplingManager<MDTraits, Embedded1d3dCouplingMode::Projection>> +{ + using ThisType = Embedded1d3dCouplingManager<MDTraits, Embedded1d3dCouplingMode::Projection>; + using ParentType = EmbeddedCouplingManagerBase<MDTraits, ThisType>; + + using Scalar = typename MDTraits::Scalar; + using SolutionVector = typename MDTraits::SolutionVector; + using PointSourceData = typename ParentType::PointSourceTraits::PointSourceData; + + static constexpr auto bulkIdx = typename MDTraits::template SubDomain<0>::Index(); + static constexpr auto lowDimIdx = typename MDTraits::template SubDomain<1>::Index(); + + // the sub domain type aliases + template<std::size_t id> using SubDomainTypeTag = typename MDTraits::template SubDomain<id>::TypeTag; + template<std::size_t id> using Problem = GetPropType<SubDomainTypeTag<id>, Properties::Problem>; + template<std::size_t id> using GridGeometry = GetPropType<SubDomainTypeTag<id>, Properties::GridGeometry>; + template<std::size_t id> using GridView = typename GridGeometry<id>::GridView; + template<std::size_t id> using Element = typename GridView<id>::template Codim<0>::Entity; + template<std::size_t id> using GridIndex = typename IndexTraits<GridView<id>>::GridIndex; + + static constexpr int lowDimDim = GridView<lowDimIdx>::dimension; + static constexpr int bulkDim = GridView<bulkIdx>::dimension; + static constexpr int dimWorld = GridView<bulkIdx>::dimensionworld; + + template<std::size_t id> + static constexpr bool isBox() + { return GridGeometry<id>::discMethod == DiscretizationMethod::box; } + + using GlobalPosition = typename Element<bulkIdx>::Geometry::GlobalCoordinate; + + using SegmentNetwork = Detail::SegmentNetwork<GridGeometry<lowDimIdx>>; + +public: + static constexpr Embedded1d3dCouplingMode::Projection couplingMode{}; + + using ParentType::ParentType; + + void init(std::shared_ptr<Problem<bulkIdx>> bulkProblem, + std::shared_ptr<Problem<lowDimIdx>> lowDimProblem, + const SolutionVector& curSol) + { + ParentType::init(bulkProblem, lowDimProblem, curSol); + } + + /* \brief Compute integration point point sources and associated data + * This method computes a source term on the explicitly given surface of the network grid + * by integrating over the elements of the surface, projecting the integration points onto + * the centerlines to evaluate 1d quantities and set point sources (minimum distance projection) + * + * \param order Unused in this algorithm! (passed from default 1D3D coupling manager) + * \param verbose If the source computation is verbose + */ + void computePointSourceData(std::size_t order = 1, bool verbose = false) + { + // initialize the maps + // do some logging and profiling + Dune::Timer watch; + std::cout << "Initializing the coupling manager (projection)" << std::endl; + + // debug VTK output + const bool enableIntersectionOutput = getParam<bool>("MixedDimension.EnableIntersectionOutput", false); + std::unique_ptr<Detail::DebugIntersectionVTKOutput> vtkCoupledFaces = + enableIntersectionOutput ? std::make_unique<Detail::DebugIntersectionVTKOutput>() : nullptr; + std::unique_ptr<Detail::DebugIntersectionVTKOutput> vtkIntersections = + enableIntersectionOutput ? std::make_unique<Detail::DebugIntersectionVTKOutput>() : nullptr; + + // temporarily represent the network domain as a list of segments + // a more efficient data structure + const auto& lowDimProblem = this->problem(lowDimIdx); + const auto& lowDimFvGridGeometry = lowDimProblem.gridGeometry(); + const auto& lowDimGridView = lowDimFvGridGeometry.gridView(); + localAreaFactor_.resize(lowDimGridView.size(0), 0.0); + const auto radiusFunc = [&](const GridIndex<lowDimIdx> eIdx) { + return lowDimProblem.spatialParams().radius(eIdx); + }; + const auto network = SegmentNetwork{ + lowDimFvGridGeometry, radiusFunc + }; + + // precompute the vertex indices for efficiency (box method) + this->precomputeVertexIndices(bulkIdx); + this->precomputeVertexIndices(lowDimIdx); + + const auto& bulkProblem = this->problem(bulkIdx); + const auto& bulkFvGridGeometry = bulkProblem.gridGeometry(); + const auto& bulkGridView = bulkFvGridGeometry.gridView(); + bulkElementMarker_.assign(bulkGridView.size(0), 0); + bulkVertexMarker_.assign(bulkGridView.size(bulkDim), 0); + const GlobalPosition threshold(1e-2*(bulkFvGridGeometry.bBoxMax()-bulkFvGridGeometry.bBoxMin()).two_norm()); + const auto bBoxMinSmall = bulkFvGridGeometry.bBoxMin() + threshold; + const auto bBoxMaxSmall = bulkFvGridGeometry.bBoxMax() - threshold; + auto insideBBox = [bBoxMin=bBoxMinSmall, bBoxMax=bBoxMaxSmall](const GlobalPosition& point) -> bool + { + static constexpr Scalar eps_ = 1.0e-7; + const Scalar eps0 = eps_*(bBoxMax[0] - bBoxMin[0]); + const Scalar eps1 = eps_*(bBoxMax[1] - bBoxMin[1]); + const Scalar eps2 = eps_*(bBoxMax[2] - bBoxMin[2]); + return (bBoxMin[0] - eps0 <= point[0] && point[0] <= bBoxMax[0] + eps0 && + bBoxMin[1] - eps1 <= point[1] && point[1] <= bBoxMax[1] + eps1 && + bBoxMin[2] - eps2 <= point[2] && point[2] <= bBoxMax[2] + eps2); + }; + + // setup simplex "quadrature" rule + static const auto quadSimplexRefineMaxLevel + = getParam<std::size_t>("MixedDimension.SimplexIntegrationRefine", 4); + const auto indicator = Detail::NetworkIndicatorFunction { network }; + + // estimate number of point sources for memory allocation + static const auto estimateNumberOfPointSources + = getParam<std::size_t>("MixedDimension.EstimateNumberOfPointSources", bulkFvGridGeometry.gridView().size(0)); + + this->pointSources(bulkIdx).reserve(estimateNumberOfPointSources); + this->pointSources(lowDimIdx).reserve(estimateNumberOfPointSources); + this->pointSourceData().reserve(estimateNumberOfPointSources); + + // we go over the bulk surface intersection, check if we are coupled, + // and if this is the case, we add integration point sources using + // the local simplex refinement quadrature procedure (see paper Koch 2021) + for (const auto& element : elements(bulkGridView)) + { + const auto bulkElementIdx = bulkFvGridGeometry.elementMapper().index(element); + const auto bulkGeometry = element.geometry(); + const auto refElement = Dune::referenceElement(element); + for (const auto& intersection : intersections(bulkGridView, element)) + { + // skip inner intersection + if (!intersection.boundary()) + continue; + + // check if we are close enough to the coupling interface + const auto [isCoupled, closestSegmentIdx, minDist] = isCoupled_(network, intersection, insideBBox); + if (!isCoupled) + continue; + + // we found an intersection on the coupling interface: mark as coupled element + bulkElementMarker_[bulkElementIdx] = 1; + + const auto interfaceGeometry = intersection.geometry(); + for (int i = 0; i < interfaceGeometry.corners(); ++i) + { + const auto localVIdx = refElement.subEntity(intersection.indexInInside(), 1, i, bulkDim); + const auto vIdx = bulkFvGridGeometry.vertexMapper().subIndex(element, localVIdx, bulkDim); + bulkVertexMarker_[vIdx] = 1; + } + + // debug graphical intersection output + if (enableIntersectionOutput) + vtkCoupledFaces->push({ + { interfaceGeometry.corner(0), interfaceGeometry.corner(1), interfaceGeometry.corner(2)} + }, closestSegmentIdx); + + const auto quadSimplex = LocalRefinementSimplexQuadrature{ interfaceGeometry, quadSimplexRefineMaxLevel, indicator }; + + // iterate over all quadrature points (children simplices) + for (const auto& qp : quadSimplex) + { + const auto surfacePoint = interfaceGeometry.global(qp.position()); + const auto closestSegmentIdx = qp.indicator(); + const auto projPoint = network.projectionPointOnSegment(surfacePoint, closestSegmentIdx); + + addPointSourceAtIP_(bulkGeometry, interfaceGeometry, qp, bulkElementIdx, closestSegmentIdx, surfacePoint, projPoint); + addStencilEntries_(bulkElementIdx, closestSegmentIdx); + } + } + } + + // 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() + ); + } + }); + + // debug VTK output + if (enableIntersectionOutput) + vtkCoupledFaces->write("coupledfaces.vtk"); + + // compare theoretical cylinder surface to actual discrete surface + // of the coupled bulk interface facets + for (const auto& element : elements(lowDimGridView)) + { + const auto length = element.geometry().volume(); + const auto eIdx = lowDimFvGridGeometry.elementMapper().index(element); + const auto radius = this->problem(lowDimIdx).spatialParams().radius(eIdx); + const auto cylinderSurface = 2.0*M_PI*radius*length; + localAreaFactor_[eIdx] /= cylinderSurface; + localAreaFactor_[eIdx] -= 1.0; + } + + this->pointSources(bulkIdx).shrink_to_fit(); + this->pointSources(lowDimIdx).shrink_to_fit(); + this->pointSourceData().shrink_to_fit(); + + std::cout << "-- Coupling at " << this->pointSourceData().size() << " integration points." << std::endl; + std::cout << "-- Finished initialization after " << watch.elapsed() << " seconds." << std::endl; + } + + /*! + * \brief Methods to be accessed by the subproblems + */ + // \{ + + //! Return a reference to the bulk problem + Scalar radius(std::size_t id) const + { + const auto& data = this->pointSourceData()[id]; + return this->problem(lowDimIdx).spatialParams().radius(data.lowDimElementIdx()); + } + + // \} + + //! Marker that is non-zero for bulk elements that are coupled + const std::vector<int>& bulkElementMarker() const + { return bulkElementMarker_; } + + //! Marker that is non-zero for bulk vertices that belong to coupled surface facets + const std::vector<int>& bulkVertexMarker() const + { return bulkVertexMarker_; } + + //! For each 1D element, the ratio of associated discrete bulk surface area + //! to the theoretical bulk surface area resulting from assuming a cylinder-shaped segment + //! (excluding cylinder caps). + const std::vector<Scalar>& localAreaFactor() const + { return localAreaFactor_; } + +private: + std::vector<int> bulkElementMarker_, bulkVertexMarker_; + std::vector<double> localAreaFactor_; + + // add a point source containing all data needed to evaluate + // the coupling source term from both domains + template<class BulkGeometry, class InterfaceGeometry, class QP> + void addPointSourceAtIP_(const BulkGeometry& bulkGeometry, + const InterfaceGeometry& ifGeometry, + const QP& qp, + const GridIndex<bulkIdx> bulkElementIdx, + const GridIndex<lowDimIdx> lowDimElementIdx, + const GlobalPosition& surfacePoint, + const GlobalPosition& projPoint) + { + // add a new point source id (for the associated data) + const auto id = this->idCounter_++; + + // add a bulk point source + const auto qpweight = qp.weight(); + const auto integrationElement = ifGeometry.integrationElement(qp.position()); + this->pointSources(bulkIdx).emplace_back(surfacePoint, id, qpweight, integrationElement, bulkElementIdx); + + // find the corresponding projection point and 1d element + this->pointSources(lowDimIdx).emplace_back(projPoint, id, qpweight, integrationElement, lowDimElementIdx); + localAreaFactor_[lowDimElementIdx] += qpweight*integrationElement; + + const auto& bulkFvGridGeometry = this->problem(bulkIdx).gridGeometry(); + const auto& lowDimFvGridGeometry = this->problem(lowDimIdx).gridGeometry(); + + // pre compute additional data used for the evaluation of + // the actual solution dependent source term + PointSourceData psData; + + if constexpr (isBox<lowDimIdx>()) + { + std::vector<Dune::FieldVector<Scalar, 1> > shapeValues; + const auto lowDimGeometry = this->problem(lowDimIdx).gridGeometry().element(lowDimElementIdx).geometry(); + this->getShapeValues(lowDimIdx, lowDimFvGridGeometry, lowDimGeometry, projPoint, shapeValues); + psData.addLowDimInterpolation(shapeValues, this->vertexIndices(lowDimIdx, lowDimElementIdx), lowDimElementIdx); + } + else + { + psData.addLowDimInterpolation(lowDimElementIdx); + } + + if constexpr (isBox<bulkIdx>()) + { + std::vector<Dune::FieldVector<Scalar, 1> > shapeValues; + this->getShapeValues(bulkIdx, bulkFvGridGeometry, bulkGeometry, surfacePoint, 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)); + } + + void addStencilEntries_(const GridIndex<bulkIdx> bulkElementIdx, const GridIndex<lowDimIdx> lowDimElementIdx) + { + // export the bulk coupling stencil + if constexpr (isBox<lowDimIdx>()) + { + const auto& vertices = this->vertexIndices(lowDimIdx, lowDimElementIdx); + this->couplingStencils(bulkIdx)[bulkElementIdx].insert( + this->couplingStencils(bulkIdx)[bulkElementIdx].end(), + vertices.begin(), vertices.end() + ); + + } + else + { + this->couplingStencils(bulkIdx)[bulkElementIdx].push_back(lowDimElementIdx); + } + + // export the lowdim coupling stencil + 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); + } + } + + template<class BoundaryIntersection, class BBoxCheck> + auto isCoupled_(const SegmentNetwork& network, const BoundaryIntersection& intersection, const BBoxCheck& insideBBox) const + { + const auto center = intersection.geometry().center(); + const auto [eIdx, minDist] = network.findClosestSegmentSurface(center); + + // all inner elements are coupled + if (insideBBox(center)) + return std::make_tuple(true, eIdx, minDist); + + // if the distance is under a certain threshold we are coupled (and the angle is not very big) + static const auto rFactor = getParam<Scalar>("MixedDimension.ProjectionCoupledRadiusFactor", 0.1); + static const auto spFactor = getParam<Scalar>("MixedDimension.ProjectionCoupledAngleFactor", 0.3); + const auto projPoint = network.projectionPointOnSegment(center, eIdx); + const auto distVec = projPoint - center; + const bool isCoupled = minDist < rFactor * network.radius(eIdx) && distVec * intersection.centerUnitOuterNormal() > distVec.two_norm() * spFactor; + return std::make_tuple(isCoupled, eIdx, minDist); + } +}; + +} // end namespace Dumux + +#endif diff --git a/dumux/multidomain/embedded/localrefinementquadrature.hh b/dumux/multidomain/embedded/localrefinementquadrature.hh new file mode 100644 index 0000000000000000000000000000000000000000..0b4ea574f674ba6c761544fc008fe3d8faa1fb42 --- /dev/null +++ b/dumux/multidomain/embedded/localrefinementquadrature.hh @@ -0,0 +1,184 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +/***************************************************************************** + * See the file COPYING for full copying permissions. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + *****************************************************************************/ +/*! + * \file + * \ingroup EmbeddedCoupling + * \brief A quadrature rule for surface coupling in the 1D-3D setting + */ + +#ifndef DUMUX_MULTIDOMAIN_EMBEDDED_LOCAL_REFINEMENT_QUADRATURE_HH +#define DUMUX_MULTIDOMAIN_EMBEDDED_LOCAL_REFINEMENT_QUADRATURE_HH + +#include <array> +#include <vector> +#include <utility> +#include <algorithm> + +namespace Dumux { + +template<class Geometry, class IndicatorFunction> +class LocalRefinementSimplexQuadrature +{ + using IndicatorTriple = std::array<std::size_t, 3>; + using GlobalPosition = typename Geometry::GlobalCoordinate; + using LocalPosition = typename Geometry::LocalCoordinate; + using Corners = std::array<GlobalPosition, 3>; + + class QuadPoint + { + LocalPosition localPos_; + double weight_; + std::size_t indicator_; + + public: + QuadPoint(LocalPosition&& localPos, double weight, std::size_t i) + : localPos_(weight*std::move(localPos)) + , weight_(weight) + , indicator_(i) + {} + + void add(LocalPosition&& localPos, double weight) + { + localPos_ += weight*localPos; + weight_ += weight; + } + + void finalize() + { + localPos_ /= weight_; + } + + const LocalPosition& position() const { return localPos_; } + double weight() const { return weight_; } + std::size_t indicator() const { return indicator_; } + }; + + class Triangle + { + Corners corners_; + public: + Triangle(const Corners& c) : corners_(c) {} + Triangle(Corners&& c) : corners_(std::move(c)) {} + const GlobalPosition& operator[](std::size_t i) const { return corners_[i]; } + + GlobalPosition center() const + { + GlobalPosition center(0.0); + for (const auto& c : corners_) + center += c; + center /= corners_.size(); + return center; + } + + auto volume() const + { + const auto ab = corners_[1] - corners_[0]; + const auto ac = corners_[2] - corners_[0]; + return crossProduct(ab, ac).two_norm()*0.5; + } + }; + +public: + LocalRefinementSimplexQuadrature(const Geometry& geo, std::size_t maxLevel, const IndicatorFunction& i) + : ind_(i) + , maxLevel_(maxLevel) + , geometry_(geo) + { + // evaluate indicator + const auto tri = Triangle{{ geo.corner(0), geo.corner(1), geo.corner(2) }}; + const auto triple = IndicatorTriple{{ ind_(tri[0]), ind_(tri[1]), ind_(tri[2]) }}; + volume_ = tri.volume(); + + // add new sub-qps recursively by adaptive refinement + addSimplex_(tri, 0, triple, triple); + + for (auto& qp : qps_) + qp.finalize(); + } + + auto begin() const { return qps_.begin(); } + auto end() const { return qps_.end(); } + auto size() const { return qps_.size(); } + +private: + void addSimplex_(const Triangle& tri, std::size_t level, const IndicatorTriple& triple, const IndicatorTriple& childTriple) + { + // if all indicator are the same just add the triangle + if (std::all_of(childTriple.begin(), childTriple.end(), [a0=childTriple[0]](auto a){ return (a == a0); })) + { + // the volumes need to sum up to 0.5 (triangle reference element volume) + auto it = std::find_if(qps_.begin(), qps_.end(), [&](const auto& qp){ return (qp.indicator() == childTriple[0]); }); + if (it != qps_.end()) + it->add(geometry_.local(tri.center()), 0.5*tri.volume()/volume_); + else + qps_.emplace_back(geometry_.local(tri.center()), 0.5*tri.volume()/volume_, childTriple[0]); + } + + // if they are different but the maximum level is reached, split volume in three + else if (level == maxLevel_) + { + for (int i = 0; i < 3; ++i) + { + // the volumes need to sum up to 0.5 (triangle reference element volume) + auto it = std::find_if(qps_.begin(), qps_.end(), [&](const auto& qp){ return (qp.indicator() == childTriple[i]); }); + if (it != qps_.end()) + it->add(geometry_.local(tri[i]), 0.5*tri.volume()/volume_/3.0); + else + qps_.emplace_back(geometry_.local(tri[i]), 0.5*tri.volume()/volume_/3.0, childTriple[i]); + } + } + + // otherwise refine and go recursively over all children + else + { + const auto children = refine(tri); + for (const auto& c : children) + { + // make sure to recompute indicator every time since new indices might come up + const auto grandChildTriple = IndicatorTriple{{ ind_(c[0]), ind_(c[1]), ind_(c[2]) }}; + addSimplex_(c, level+1, triple, grandChildTriple); + } + } + } + + std::array<Triangle, 4> refine(const Triangle& tri) const + { + const auto p01 = 0.5*(tri[0] + tri[1]); + const auto p12 = 0.5*(tri[1] + tri[2]); + const auto p20 = 0.5*(tri[2] + tri[0]); + + return {{ + {{ tri[0], p01, p20 }}, + {{ tri[1], p01, p12 }}, + {{ tri[2], p12, p20 }}, + {{ p12, p01, p20 }} + }}; + } + + const IndicatorFunction &ind_; + std::size_t maxLevel_; + const Geometry& geometry_; + double volume_; + + std::vector<QuadPoint> qps_; +}; + +} // end namespace Dumux + +#endif