diff --git a/CMakeLists_files.cmake b/CMakeLists_files.cmake index 0d31c27e3..178484d85 100644 --- a/CMakeLists_files.cmake +++ b/CMakeLists_files.cmake @@ -51,6 +51,7 @@ list(APPEND MAIN_SOURCE_FILES opm/grid/ColumnExtract.cpp opm/grid/FaceQuadrature.cpp opm/grid/GraphOfGrid.cpp + opm/grid/CoarseGraphOfGrid.cpp opm/grid/GraphOfGridWrappers.cpp opm/grid/GridHelpers.cpp opm/grid/GridManager.cpp @@ -91,6 +92,7 @@ list(APPEND TEST_SOURCE_FILES tests/test_geom2d.cpp tests/test_graphofgrid.cpp tests/test_graphofgrid_parallel.cpp + tests/test_coarsegraphofgrid.cpp tests/test_gridutilities.cpp tests/test_minpvprocessor.cpp tests/test_polyhedralgrid.cpp @@ -251,6 +253,7 @@ list(APPEND PUBLIC_HEADER_FILES opm/grid/ColumnExtract.hpp opm/grid/FaceQuadrature.hpp opm/grid/GraphOfGrid.hpp + opm/grid/CoarseGraphOfGrid.hpp opm/grid/GraphOfGridWrappers.hpp opm/grid/GridHelpers.hpp opm/grid/GridManager.hpp diff --git a/opm/grid/CoarseGraphOfGrid.cpp b/opm/grid/CoarseGraphOfGrid.cpp new file mode 100644 index 000000000..91e34fc92 --- /dev/null +++ b/opm/grid/CoarseGraphOfGrid.cpp @@ -0,0 +1,291 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +/* + Copyright 2026 Equinor ASA. + + This file is part of the Open Porous Media project (OPM). + + OPM 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. + + OPM 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 OPM. If not, see . + + Consult the COPYING file in the top-level source directory of this + module for the precise wording of the license and the list of + copyright holders. +*/ + +#include +#include "CoarseGraphOfGrid.hpp" + +#include + +namespace Opm { + +template +void CoarseGraphOfGrid::mergeWellCellsForCoarseGraph(std::vector& hasWell, + std::vector>& wellPerf, + const Dune::cpgrid::WellConnections& wellConn) +{ + int wellId = 0; + + for (const auto& well : wellConn) { + + bool cellInMultWells = false; + std::set otherWell; + std::vector perfs; + + // Loop over all connections in the well. Create list perf of connections + for (int idx : well) { + + if (!cellInMultWells) { + //Check if connection is intersected by other well + if (hasWell[idx]!=-1) { + cellInMultWells = true; + otherWell.insert( hasWell[idx] ); + } else { + hasWell[idx] = wellId; + perfs.push_back(idx); + } + } + else { + if (hasWell[idx]!=-1) { + otherWell.insert( hasWell[idx] ); + } + } + } + // If current well intersects with other wells. Add the well connections together + if (cellInMultWells) { + int minOtherWell = *otherWell.begin(); + for (int idx : well) { + if (hasWell[idx]!=minOtherWell) { + hasWell[idx] = minOtherWell; + wellPerf[minOtherWell].push_back(idx); + } + } + // If more than one intersection add all wells to smallest wellId and delete others. + for (int otherWellId : otherWell) { + if (otherWellId != minOtherWell) { + for (int idx : wellPerf[otherWellId]) { + if (hasWell[idx]!=minOtherWell) { + hasWell[idx] = minOtherWell; + wellPerf[minOtherWell].push_back(idx); + } + } + wellPerf[otherWellId].clear(); + } + } + } + else { + wellPerf.push_back(perfs); + wellId++; + } + } +} + +template +void CoarseGraphOfGrid::dfsqw(const Row& row, std::priority_queue &q, int v, int master, + double w, int maxNode, std::vector& visited, + std::vector>>& gEdges, + const std::vector& hasWell, const std::vector>& wellPerf) +{ + + auto& current_cnode = coarseNodes.back(); + auto& current_edges = gEdges.back(); + + std::vector wellIdxs; + if (hasWell[v] == -1) { + visited[v] = true; + map_to_coarse_[v] = master; + current_cnode.push_back(v); + + // Add all neighboring vertices of v with transmissibility larger than w to the queue. + auto col = row.begin(); + for (; col != row.end(); ++col) { + int nab = col.index(); + double wgt = (*transGraph)[v][nab]; + if ( wgt > w) { + if (!visited[nab]) { + q.push({wgt, nab}); + } + } + } + } else { + // If node has a well, merge all connections to master node. + int wellId = hasWell[v]; + const std::vector& perfs = wellPerf[wellId]; + + for (const auto& idx : perfs) { + visited[idx] = true; + map_to_coarse_[idx] = master; + current_cnode.push_back(idx); + wellIdxs.push_back(idx); + } + for (const auto& idx : perfs) { + auto wrow = (*transGraph)[idx]; + auto col = wrow.begin(); + for (; col != wrow.end(); ++col) { + int nab = col.index(); + double wgt = (*transGraph)[idx][nab]; + if ( wgt > w) { + if (!visited[nab]) { + q.push({wgt, nab}); + } + } + } + } + } + + // Only merge more vertices if the current coarse node is smaller than maxNode. + if ( (int)current_cnode.size() < maxNode ) { + if (!q.empty()) { + + // Find strongest connection in queue q not already merged to current_cnode. + auto strongCon = q.top(); + int nab = strongCon.idx; + q.pop(); + while (visited[nab] && !q.empty()) { + strongCon = q.top(); + nab = strongCon.idx; + q.pop(); + } + // Call dfsq reflexively on strongest connection in queue + if (!visited[nab]) + dfsqw((*transGraph)[nab],q,nab,master,w,maxNode,visited,gEdges,hasWell,wellPerf); + } + } else { + q = std::priority_queue(); + } + + // Add connection between v and nab if v and nab are not merged + if (wellIdxs.size() > 0) { + for (const auto& idx : wellIdxs) { + auto wrow = (*transGraph)[idx]; + auto col = wrow.begin(); + for (; col != wrow.end(); ++col) { + int nab = col.index(); + if (map_to_coarse_[v]!=map_to_coarse_[nab]) { + current_edges.push_back({v,nab,(*transGraph)[idx][nab]}); + } + } + } + } + else { + auto col = row.begin(); + for (; col != row.end(); ++col) { + int nab = col.index(); + if (map_to_coarse_[v]!=map_to_coarse_[nab]) { + current_edges.push_back({v,nab,(*transGraph)[v][nab]}); + } + } + } +} + +template +void CoarseGraphOfGrid::createCoarseGraph(const Dune::EdgeWeightMethod edgeWeightMethod, + double coarseThreshold, + int coarsePartitionMaxNodeSize, + bool allowDistributedWells, + int root, + const Dune::cpgrid::WellConnections& wellConn) +{ + int N = grid.size(0); + const auto& rank = grid.comm().rank(); + + // List to keep track of visited vertices in original fine graph + std::vector visited(N, false); + // List to know if vertex i is perferated by a well. hasWell[i]==-1 means no, hasWell[i]!=-1 yes + std::vector hasWell(N,-1); + // For each well i, wellPerf[i] lists vertices perferated by well i. + // If wells intersect they are merged. + std::vector> wellPerf; + + // Only add data to hasWell and wellPerf if allowDistributedWells==false + if (!allowDistributedWells) { + mergeWellCellsForCoarseGraph(hasWell, wellPerf, wellConn); + } + // Map from index of fine graph to index of coarse graph. + map_to_coarse_.resize(N, -1); + + // Vector that describes the coarse graph. + // Each coarse vertex v has vector<(int,int,double)> = gEdges[v], + // where each tuple (idx,nab,wgt) is index idx of fine graph vertex idx contained in v, + // (idx,nab) is edge in fine graph, and wgt is transmissibility between idx and nab. + std::vector >> gEdges; + + // Counter for coarse graph index + int newV = 0; + // Keep track of largest coarse vertex. + int biggest = 0; + + if (rank == root) { + // Loop over all idecies in fine graph + for (int v = 0; v < N; ++v) { + + // if idx v is not visited, add it to coarse graph + if (!visited[v]) { + + std::priority_queue q; + + // Allocate coarse node v in gEdges and coarseNodes + gEdges.emplace_back(); + coarseNodes.emplace_back(); + + // Call depth first search from row transGraph[v] + dfsqw((*transGraph)[v],q,v,newV,coarseThreshold, + coarsePartitionMaxNodeSize,visited, + gEdges,hasWell,wellPerf); + + newV++; + + if ((int)coarseNodes.back().size() > biggest) + biggest = coarseNodes.back().size(); + } + } + std::cout << "Coarse partitioning graph size: " << coarseNodes.size() <<" Largest node: "<< biggest << std::endl; + + // Construct the coarse graph from gEdges + // + // Loop over coarse nodes with cIdx=0,...,newV -1 + for (const std::vector >& es : gEdges ) { + + // ce represents the edges of cIdx. If cIdx has connection with cNab, + // wgt=ce[cNab] exists and is greater than 0. + std::map ce; + + // Loop over edges of all vertices from fine graph contained in coarse node cIdx + for (const std::tuple& fe : es) { + + // Coarse index of the fine graph edge + int coarseNab = map_to_coarse_[std::get<1>(fe)]; + // Transmissibility of fine edge + double transVal = std::get<2>(fe); + // Besed on edgeWeightMethod, choose weight of coarse edgeWeight + double weight = edgeWeightMethod == 0 ? 1.0 : transVal; + + // Only add fine edge to coarse graph if transmissibility is non-zero. Overlap cells + // between partitions are not added if connection has non-zero transmissibility. + if (transVal > 0) { + if ( ce.count(coarseNab) == 1 ) { + ce[coarseNab] += weight; + } else { + ce.insert({coarseNab,weight}); + } + } + } + cedges.push_back(ce); + } + } +} + +template class CoarseGraphOfGrid; + +} // namespace Opm diff --git a/opm/grid/CoarseGraphOfGrid.hpp b/opm/grid/CoarseGraphOfGrid.hpp new file mode 100644 index 000000000..5db2321e8 --- /dev/null +++ b/opm/grid/CoarseGraphOfGrid.hpp @@ -0,0 +1,148 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +/* + Copyright 2026 Equinor ASA. + + This file is part of the Open Porous Media project (OPM). + + OPM 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. + + OPM 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 OPM. If not, see . + + Consult the COPYING file in the top-level source directory of this + module for the precise wording of the license and the list of + copyright holders. +*/ + +#ifndef OPM_COARSE_GRAPH_OF_GRID_HEADER +#define OPM_COARSE_GRAPH_OF_GRID_HEADER + +#include +#include +#include + +namespace Opm { + +struct WgtIdx { + + double wgt; + int idx; + + bool operator<(const WgtIdx& other) const { + return wgt < other.wgt; + } +}; + +/// \brief A class storing a Coarse graph representation of the grid +/// +/// Similar to GraphOfGrid, but here nodes are merged if +/// the transmissibility on the edge connecting them is below +/// a certain threshold. +template +class CoarseGraphOfGrid{ + + using TransGraph = Dune::BCRSMatrix>; + using Row = typename TransGraph::row_type; + +public: + + explicit CoarseGraphOfGrid (const Grid& grid_, + const Dune::EdgeWeightMethod edgeWeightMethod, + const TransGraph* tg, + double coarseThreshold, + int coarsePartitionMaxNodeSize, + bool allowDistributedWells, + int root, + const Dune::cpgrid::WellConnections& wellConn) + : grid(grid_), transGraph(tg) + { + createCoarseGraph(edgeWeightMethod, coarseThreshold, coarsePartitionMaxNodeSize, + allowDistributedWells, root, wellConn); + } + + const Grid& getGrid() const + { + return grid; + } + + /// \brief Number of graph vertices + int size() const + { + return coarseNodes.size(); + } + + const std::vector>& getCoarseNodes() const + { + return coarseNodes; + } + + const std::vector >& getCoarseEdges() const + { + return cedges; + } + + /// \brief returns map of global cell id to coarse vertex id + const std::vector& getMapToCoarse() const + { + return map_to_coarse_; + } + + /// \brief Return the list of wells + const auto& getWells () const + { + return wells; + } +private: + + /// \brief Merge vertices that share a common well + void mergeWellCellsForCoarseGraph(std::vector& hasWell, + std::vector>& wellPerf, + const Dune::cpgrid::WellConnections& wells); + + /// \brief Coarsen the graph by doing a (d)epth (f)irst (s)earch with priority (q)ueue + /// and merges (w)ells. + /// + /// Recurseive dfs that merges strongly connected nodes (transmissibility) in the + /// partitioning graph. + /// A priority queue is used to make sure the largest connections are prioritised in the + /// merging of vertices. + void dfsqw(const Row& row, std::priority_queue &q, int v, int master, + double w, int maxNode, std::vector& visited, + std::vector>>& gEdges, + const std::vector& hasWell, const std::vector>& wellPerf); + + /// \brief Create the coarse graph merging all vertices connected with a large transmissibility. + /// + /// Creates a coarsened graph by merging vertices connected with a + /// transmissibility larger than coarseThreshold. + /// Coarse vertices will not be larger than coarsePartitionMaxNodeSize. + /// Also meges wells if allowDistributedWells == true. + void createCoarseGraph(const Dune::EdgeWeightMethod edgeWeightMethod, + double coarseThreshold, + int coarsePartitionMaxNodeSize, + bool allowDistributedWells, + int root, + const Dune::cpgrid::WellConnections& wells); + + const Grid& grid; + std::list> wells; + + const TransGraph* transGraph; + std::vector map_to_coarse_; + std::vector > cedges; + std::vector> coarseNodes; + +}; + +} // namespace Opm + +#endif // OPM_COARSE_GRAPH_OF_GRID_HEADER diff --git a/opm/grid/CpGrid.hpp b/opm/grid/CpGrid.hpp index 7cbb06627..8e44929e4 100644 --- a/opm/grid/CpGrid.hpp +++ b/opm/grid/CpGrid.hpp @@ -43,6 +43,7 @@ #include #include +#include #include @@ -958,12 +959,16 @@ namespace Dune bool addCornerCells=false, int overlapLayers=1, int partitionMethod = Dune::PartitionMethod::zoltanGoG, double imbalanceTol = 1.1, bool allowDistributedWells = false, - bool useTransToFilterOverlap = true) + bool useTransToFilterOverlap = true, + const Dune::BCRSMatrix>* graph = nullptr, + double coarseThreshold = 1.0, + int coarsePartitionMaxNodeSize = -1) { auto ret = scatterGrid(method, ownersFirst, wells, possibleFutureConnections, serialPartitioning, transmissibilities, addCornerCells, overlapLayers, partitionMethod, imbalanceTol, allowDistributedWells, /* input_cell_parts = */ std::vector{}, /* level = */ 0, - useTransToFilterOverlap); + useTransToFilterOverlap, graph, + coarseThreshold, coarsePartitionMaxNodeSize); using std::get; if (get<0>(ret)) { @@ -1481,7 +1486,10 @@ namespace Dune bool allowDistributedWells = true, const std::vector& input_cell_part = {}, int level = -1, - bool useTransToFilterOverlap = true); + bool useTransToFilterOverlap = true, + const Dune::BCRSMatrix>* transGraph = nullptr, + double coarseThreshold = 1.0, + int coarsePartitionMaxNodeSize = -1); /** @brief The data stored in the grid. * diff --git a/opm/grid/GraphOfGridWrappers.cpp b/opm/grid/GraphOfGridWrappers.cpp index 34e540df6..7d8caca77 100644 --- a/opm/grid/GraphOfGridWrappers.cpp +++ b/opm/grid/GraphOfGridWrappers.cpp @@ -32,9 +32,10 @@ namespace Opm { #if HAVE_MPI +template int getGraphOfGridNumVertices(void* pGraph, int *err) { - const GraphOfGrid& gog = *static_cast*>(pGraph); + const GraphType& gog = *static_cast(pGraph); int size = gog.size(); *err = ZOLTAN_OK; return size; @@ -131,12 +132,90 @@ void getGraphOfGridEdgeList(void *pGraph, *err = ZOLTAN_OK; } -template +void getCoarseGraphVerticesList(void* pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + int weightDim, + float *objWeights, + int *err) +{ + assert(dimGlobalID==1); // ID is a single int + assert(weightDim==1); // vertex weight is a single float + const CoarseGraphOfGrid& gog = *static_cast*>(pGraph); + const std::vector>& cnodes = gog.getCoarseNodes(); + int i=0; + for (const auto& v : cnodes) + { + gIDs[i] = i; + // lIDs are left unused + objWeights[i] = v.size(); + ++i; + } + *err = ZOLTAN_OK; +} +void getCoarseGraphNumEdges(void *pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + [[maybe_unused]] int numCells, + ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + int *numEdges, + int *err) +{ + assert(dimGlobalID==1); // ID is a single int + const CoarseGraphOfGrid& gog = *static_cast*>(pGraph); + + const std::vector >& edges = gog.getCoarseEdges(); + + for (size_t idx = 0; idx < edges.size(); ++idx) + { + numEdges[idx] = edges[gIDs[idx]].size(); + } + + *err = ZOLTAN_OK; +} + +void getCoarseGraphEdgeList(void *pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + [[maybe_unused]] int numCells, + [[maybe_unused]] ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + [[maybe_unused]] int *numEdges, + ZOLTAN_ID_PTR nborGIDs, + int *nborProc, + int weightDim, + float *edgeWeights, + int *err) +{ + assert(dimGlobalID==1); // ID is a single int + assert(weightDim==1); // edge weight is a single float + const CoarseGraphOfGrid& gog = *static_cast*>(pGraph); + const std::vector >& edges = gog.getCoarseEdges(); + int id=0; + + const auto& rank = gog.getGrid().comm().rank(); + for (const auto& node : edges) + { + for (const auto& edge : node) { + nborGIDs[id] = edge.first; + edgeWeights[id] = edge.second; + nborProc[id++] = rank; + } + } + + *err = ZOLTAN_OK; +} + +template void setGraphOfGridZoltanGraphFunctions(Zoltan_Struct *zz, - GraphOfGrid& gog, + GraphType& gog, bool pretendNull) { - GraphOfGrid* pGraph = &gog; + using DecayedGraph = std::decay_t; + GraphType* pGraph = &gog; if (pretendNull) { Zoltan_Set_Num_Obj_Fn(zz, Dune::cpgrid::getNullNumCells, pGraph); @@ -146,12 +225,21 @@ void setGraphOfGridZoltanGraphFunctions(Zoltan_Struct *zz, } else { - Zoltan_Set_Num_Obj_Fn(zz, getGraphOfGridNumVertices, pGraph); - Zoltan_Set_Obj_List_Fn(zz, getGraphOfGridVerticesList, pGraph); - Zoltan_Set_Num_Edges_Multi_Fn(zz, getGraphOfGridNumEdges, pGraph); - Zoltan_Set_Edge_List_Multi_Fn(zz, getGraphOfGridEdgeList, pGraph); + if constexpr (std::is_same_v>) { + Zoltan_Set_Num_Obj_Fn(zz, getGraphOfGridNumVertices, pGraph); + Zoltan_Set_Obj_List_Fn(zz, getGraphOfGridVerticesList, pGraph); + Zoltan_Set_Num_Edges_Multi_Fn(zz, getGraphOfGridNumEdges, pGraph); + Zoltan_Set_Edge_List_Multi_Fn(zz, getGraphOfGridEdgeList, pGraph); + } + else if constexpr (std::is_same_v>) { + Zoltan_Set_Num_Obj_Fn(zz, getGraphOfGridNumVertices, pGraph); + Zoltan_Set_Obj_List_Fn(zz, getCoarseGraphVerticesList, pGraph); + Zoltan_Set_Num_Edges_Multi_Fn(zz, getCoarseGraphNumEdges, pGraph); + Zoltan_Set_Edge_List_Multi_Fn(zz, getCoarseGraphEdgeList, pGraph); + } } } + #endif // HAVE_MPI void addFutureConnectionWells(GraphOfGrid& gog, @@ -209,9 +297,9 @@ void extendGIDtoRank(const GraphOfGrid& gog, #if HAVE_MPI namespace Impl{ - +template std::vector> -extendRootExportList(const GraphOfGrid& gog, +extendRootExportList(const GOG& gog, std::vector>& exportList, int root, const std::vector& gIDtoRank) @@ -491,6 +579,128 @@ makeImportAndExportLists(const GraphOfGrid& gog, std::move(myImportList) ); } +template +std::tuple, + std::vector>, + std::vector >, + std::vector > > +makeImportAndExportLists(const CoarseGraphOfGrid& gog, + const Dune::Communication& cc, + const std::vector * wells, + const Dune::cpgrid::WellConnections& wellConnections, + const std::unordered_map>& possibleFutureConnections, + int root, + int numExport, + int numImport, + [[maybe_unused]] const Id* exportLocalGids, + const Id* exportGlobalGids, + const int* exportToPart, + [[maybe_unused]] const Id* importGlobalGids, + bool allowDistributedWells) +{ + int size = gog.getMapToCoarse().size(); + cc.broadcast(&size, 1, root); + int rank = cc.rank(); + std::vector gIDtoRank(size, rank); + std::vector > wellsOnProc; + + // List entry: process to export to, (global) index, process rank, attribute there (not needed?) + std::vector> myExportList; + // List entry: process to import from, global index, process rank, attribute here, local index (determined later) + std::vector> myImportList; + float buffer = 1.05; // to allocate extra space for wells in myExportList and myImportList + assert(rank==root || numExport==0); + assert(rank!=root || numImport==0); + // all cells on root are added to its export and its import list + std::size_t reserveEx = rank!=root ? 0 : size; + std::size_t reserveIm = size*buffer/cc.size(); + myExportList.reserve(reserveEx); + myImportList.reserve(reserveIm); + using AttributeSet = Dune::cpgrid::CpGridData::AttributeSet; + + std::vector> importListFromRoot(cc.size()); + std::vector sizeOfImport(cc.size(), 0); + if (rank==root) + { + std::vector coarsePartRes(gog.size(), root); + auto cnodes = gog.getCoarseNodes(); + for ( int i=0; i < numExport; ++i ) + { + coarsePartRes[exportGlobalGids[i]] = exportToPart[i]; + sizeOfImport[exportToPart[i]] += cnodes[exportGlobalGids[i]].size(); + } + + const std::vector m2c = gog.getMapToCoarse(); + for (int i = 0; i < size; ++i) { + + gIDtoRank[i] = coarsePartRes[m2c[i]]; + myExportList.emplace_back(i, coarsePartRes[m2c[i]], static_cast(AttributeSet::owner)); + } + + for ( std::size_t i = 0; i < gIDtoRank.size(); ++i) + { + if ( gIDtoRank[i] == rank ) + { + myImportList.emplace_back(i, rank, static_cast(AttributeSet::owner), -1 ); + } + else { + importListFromRoot[gIDtoRank[i]].emplace_back(i); + } + } + } + std::vector newImportList; + int newNumImport; + if (cc.rank() == root) { + std::vector requestSize(2 * (cc.size() - 1)); + + for (int i = 0; i < cc.size() - 1; ++i) { + int ii = i + (int)(i >= root); // ii takes values {0,...,mpisize-1} but skips root + int tag = 15; // a random number + MPI_Isend(&sizeOfImport[ii], 1, MPI_INT, ii, tag, cc, &requestSize[2 * i]); + MPI_Isend(importListFromRoot[ii].data(), sizeOfImport[ii], MPI_INT,ii, tag + 1, cc, &requestSize[2 * i + 1]); + } + newNumImport = 0; + MPI_Waitall(requestSize.size(), requestSize.data(), MPI_STATUS_IGNORE); + } else { + int tag = 15; // a random number + MPI_Recv(&newNumImport, 1, MPI_INT, root, tag, cc, MPI_STATUS_IGNORE); + newImportList.resize(newNumImport); + MPI_Recv(newImportList.data(), newNumImport, MPI_INT, root, tag + 1, cc, MPI_STATUS_IGNORE); + } + + for ( int i=0; i < newNumImport; ++i ) + { + myImportList.emplace_back(newImportList[i], root, static_cast(AttributeSet::owner), -1); + } + std::vector> parallel_wells; + if( wells ) + { + if (allowDistributedWells) { + wellsOnProc = Dune::cpgrid::perforatingWellIndicesOnProc(gIDtoRank, *wells, + possibleFutureConnections, + gog.getGrid()); + parallel_wells = Dune::cpgrid::computeParallelWells(wellsOnProc, + *wells, + cc, + root); + } + else { + auto wellRanks = getWellRanks(gIDtoRank, wellConnections); + parallel_wells = wellsOnThisRank(*wells, wellRanks, cc, root); + } + } + else + { + std::ranges::sort(myExportList); + std::ranges::sort(myImportList); + } + return std::make_tuple( std::move(gIDtoRank), + std::move(parallel_wells), + std::move(myExportList), + std::move(myImportList) ); + +} + namespace { void setDefaultZoltanParameters(Zoltan_Struct* zz) { @@ -654,6 +864,115 @@ zoltanPartitioningWithGraphOfGrid(const Dune::CpGrid& grid, return importExportLists; } +std::tuple, std::vector>, + std::vector >, + std::vector >, + Dune::cpgrid::WellConnections> +zoltanPartitioningWithCoarseGraph(const Dune::CpGrid& grid, + const std::vector * wells, + const std::unordered_map>& possibleFutureConnections, + const Dune::cpgrid::CpGridDataTraits::Communication& cc, + Dune::EdgeWeightMethod edgeWeightMethod, + int root, + const double zoltanImbalanceTol, + bool allowDistributedWells, + const std::map& params, + const Dune::BCRSMatrix>* transGraph, + double coarseThreshold, + int coarsePartitionMaxNodeSize) +{ + float ver = 0; + struct Zoltan_Struct *zz; + int changes, numGidEntries, numLidEntries, numImport, numExport; + ZOLTAN_ID_PTR importGlobalGids, importLocalGids, exportGlobalGids, exportLocalGids; + int *importProcs, *importToPart, *exportProcs, *exportToPart; + int argc=0; + char** argv = 0 ; + int rc = Zoltan_Initialize(argc, argv, &ver); + if (rc != ZOLTAN_OK) { + OPM_THROW(std::runtime_error, "Could not initialize Zoltan!"); + } + zz = Zoltan_Create(cc); + if (zz == nullptr) { + OPM_THROW(std::runtime_error, "Could not create Zoltan data structures!"); + } + setDefaultZoltanParameters(zz); + Zoltan_Set_Param(zz, "IMBALANCE_TOL", std::to_string(zoltanImbalanceTol).c_str()); + + for (const auto& [key, value] : params) + { + Zoltan_Set_Param(zz, key.c_str(), value.c_str()); + } + + // root process has the whole grid, other ranks nothing + bool partitionIsEmpty = cc.rank()!=root; + + auto wellConnections = partitionIsEmpty || !wells ? Dune::cpgrid::WellConnections() + : Dune::cpgrid::WellConnections(*wells, possibleFutureConnections, grid); + + // prepare graph and contract well cells + // non-root processes have empty grid and no wells + CoarseGraphOfGrid cgog(grid, edgeWeightMethod, transGraph, + coarseThreshold, coarsePartitionMaxNodeSize, + allowDistributedWells, root, wellConnections); + + assert(cgog.size()==0 || !partitionIsEmpty); + + // call partitioner + setGraphOfGridZoltanGraphFunctions(zz, cgog, partitionIsEmpty); + rc = Zoltan_LB_Partition(zz, /* input (all remaining fields are output) */ + &changes, /* 1 if partitioning was changed, 0 otherwise */ + &numGidEntries, /* Number of integers used for a global ID */ + &numLidEntries, /* Number of integers used for a local ID */ + &numImport, /* Number of vertices to be sent to me */ + &importGlobalGids, /* Global IDs of vertices to be sent to me */ + &importLocalGids, /* Local IDs of vertices to be sent to me */ + &importProcs, /* Process rank for source of each incoming vertex */ + &importToPart, /* New partition for each incoming vertex */ + &numExport, /* Number of vertices I must send to other processes*/ + &exportGlobalGids, /* Global IDs of the vertices I must send */ + &exportLocalGids, /* Local IDs of the vertices I must send */ + &exportProcs, /* Process to which I send each of the vertices */ + &exportToPart); /* Partition to which each vertex will belong */ + if (rc == ZOLTAN_WARN) { + OpmLog::warning("Zoltan_LB_Partition returned with warning"); + } else if (rc == ZOLTAN_MEMERR) { + OPM_THROW(std::runtime_error, "Memory allocation failure in Zoltan_LB_Partition"); + } else if (rc == ZOLTAN_FATAL) { + OPM_THROW(std::runtime_error, "Error returned from Zoltan_LB_Partition"); + } + + // arrange output into tuples and add well cells + auto prepareIELists = [&]() { + + auto partResult = makeImportAndExportLists(cgog, + cc, + wells, + wellConnections, + possibleFutureConnections, + root, + numExport, + numImport, + exportLocalGids, + exportGlobalGids, + exportProcs, + importGlobalGids, + allowDistributedWells); + return std::tuple(std::move(std::get<0>(partResult)), + std::move(std::get<1>(partResult)), + std::move(std::get<2>(partResult)), + std::move(std::get<3>(partResult)), + std::move(wellConnections)); + }; + auto importExportLists = prepareIELists(); + + Zoltan_LB_Free_Part(&exportGlobalGids, &exportLocalGids, &exportProcs, &exportToPart); + Zoltan_LB_Free_Part(&importGlobalGids, &importLocalGids, &importProcs, &importToPart); + Zoltan_Destroy(&zz); + + return importExportLists; +} + std::vector > makeExportListsFromGIDtoRank(const std::vector& gIDtoRank, int ccsize) { @@ -750,6 +1069,88 @@ applySerialZoltan (const Dune::CpGrid& grid, Zoltan_Destroy(&zz); return std::make_tuple(rc, gIDtoRank); } + +std::tuple> +applySerialZoltanCG (const Dune::CpGrid& grid, + const Dune::cpgrid::WellConnections& wellConnections, + int numParts, + Dune::EdgeWeightMethod edgeWeightMethod, + int root, + const double zoltanImbalanceTol, + bool allowDistributedWells, + const std::map& params, + const Dune::BCRSMatrix>* transGraph, + double coarseThreshold, + int coarsePartitionMaxNodeSize) +{ + int rc = ZOLTAN_OK; + ZOLTAN_ID_PTR importGlobalGids, importLocalGids, exportGlobalGids, exportLocalGids; + int numExport = 0, numImport = 0; + int *importProcs, *importToPart, *exportProcs, *exportToPart; + struct Zoltan_Struct* zz; + int changes, numGidEntries, numLidEntries; + + int argc = 0; + char** argv = 0; + float ver = 0; + std::vector gIDtoRank; + + rc = Zoltan_Initialize(argc, argv, &ver); + if (rc != ZOLTAN_OK) + return std::make_tuple(ZOLTAN_OK + 1, gIDtoRank); + zz = Zoltan_Create(MPI_COMM_SELF); + if (!zz) + return std::make_tuple(ZOLTAN_OK + 2, gIDtoRank); + setDefaultZoltanParameters(zz); + Zoltan_Set_Param(zz, "IMBALANCE_TOL", std::to_string(zoltanImbalanceTol).c_str()); + Zoltan_Set_Param(zz, "NUM_GLOBAL_PARTS", std::to_string(numParts).c_str()); + + for (const auto& [key, value] : params) { + Zoltan_Set_Param(zz, key.c_str(), value.c_str()); + } + + // prepare graph and contract well cells + CoarseGraphOfGrid cgog(grid, edgeWeightMethod, transGraph, + coarseThreshold, coarsePartitionMaxNodeSize, + allowDistributedWells, root, wellConnections); + + + // call partitioner + setGraphOfGridZoltanGraphFunctions(zz, cgog, false); + rc = Zoltan_LB_Partition(zz, /* input (all remaining fields are output) */ + &changes, /* 1 if partitioning was changed, 0 otherwise */ + &numGidEntries, /* Number of integers used for a global ID */ + &numLidEntries, /* Number of integers used for a local ID */ + &numImport, /* Number of vertices to be sent to me */ + &importGlobalGids, /* Global IDs of vertices to be sent to me */ + &importLocalGids, /* Local IDs of vertices to be sent to me */ + &importProcs, /* Process rank for source of each incoming vertex */ + &importToPart, /* New partition for each incoming vertex */ + &numExport, /* Number of vertices I must send to other processes*/ + &exportGlobalGids, /* Global IDs of the vertices I must send */ + &exportLocalGids, /* Local IDs of the vertices I must send */ + &exportProcs, /* Process to which I send each of the vertices */ + &exportToPart); /* Partition to which each vertex will belong */ + numImport = 0; + if (rc == ZOLTAN_OK) { + gIDtoRank.resize(grid.numCells(), root); + std::vector coarsePartRes(cgog.size(), root); + const std::vector m2c = cgog.getMapToCoarse(); + for (int i = 0; i < numExport; ++i) { + coarsePartRes[exportGlobalGids[i]] = exportToPart[i]; + } + for (int i = 0; i < grid.numCells(); ++i) { + gIDtoRank[i] = coarsePartRes[m2c[i]]; + } + + } else { + rc = ZOLTAN_OK + 3; // distinguish Zoltan failures + } + Zoltan_LB_Free_Part(&exportGlobalGids, &exportLocalGids, &exportProcs, &exportToPart); + Zoltan_LB_Free_Part(&importGlobalGids, &importLocalGids, &importProcs, &importToPart); + Zoltan_Destroy(&zz); + return std::make_tuple(rc, gIDtoRank); +} } // end anonymous namespace std::tuple, @@ -849,6 +1250,108 @@ zoltanSerialPartitioningWithGraphOfGrid(const Dune::CpGrid& grid, std::move(myImportList), std::move(wellConnections)); } + +std::tuple, + std::vector>, + std::vector >, + std::vector >, + Dune::cpgrid::WellConnections> +zoltanSerialPartitioningWithCoarseGraph(const Dune::CpGrid& grid, + const std::vector * wells, + const std::unordered_map>& possibleFutureConnections, + const Dune::cpgrid::CpGridDataTraits::Communication& cc, + Dune::EdgeWeightMethod edgeWeightMethod, + int root, + const double zoltanImbalanceTol, + bool allowDistributedWells, + const std::map& params, + const Dune::BCRSMatrix>* transGraph, + double coarseThreshold, + int coarsePartitionMaxNodeSize) +{ + // root process has the whole grid, other ranks nothing + bool partitionIsEmpty = cc.rank() != root; + int rc = ZOLTAN_OK; + std::vector gIDtoRank; + using AttributeSet = Dune::cpgrid::CpGridData::AttributeSet; + std::vector> myExportList; + std::vector> myImportList; + std::vector> exportedCells; + auto wellConnections = partitionIsEmpty || !wells ? Dune::cpgrid::WellConnections() + : Dune::cpgrid::WellConnections(*wells, possibleFutureConnections, grid); + + if (cc.rank() == root) { + std::tie(rc, gIDtoRank) = applySerialZoltanCG(grid, + wellConnections, + cc.size(), + edgeWeightMethod, + root, + zoltanImbalanceTol, + allowDistributedWells, + params, + transGraph, + coarseThreshold, + coarsePartitionMaxNodeSize); + } + + cc.broadcast(&rc, 1, root); + if (rc != ZOLTAN_OK) { + switch (rc) { + case ZOLTAN_OK+1: + OPM_THROW(std::runtime_error, "Could not initialize Zoltan!"); + case ZOLTAN_OK+2: + OPM_THROW(std::runtime_error, "Could not create Zoltan!"); + case ZOLTAN_OK+3: + OPM_THROW(std::runtime_error, "Partitioning with Zoltan failed!"); + default: + OPM_THROW(std::runtime_error, "Unknown error reported by Zoltan!"); + } + } + + if (cc.rank() == root) { + // prepare exportedCells for communication + exportedCells = makeExportListsFromGIDtoRank(gIDtoRank, cc.size()); + myImportList.reserve(exportedCells[root].size()); + for (const auto& cell : exportedCells[root]) { + myImportList.emplace_back(cell, root, static_cast(AttributeSet::owner), -1); + } + // exclude root's own cells from communication + exportedCells[root].resize(0); + } + // communicate and create import+export lists + auto importedCells = Opm::Impl::communicateExportedCells(exportedCells, cc, root); + if (cc.rank() == root) { + myExportList.reserve(grid.numCells()); + for (int i = 0; i < grid.numCells(); ++i) { + myExportList.emplace_back(i, gIDtoRank[i], static_cast(AttributeSet::owner)); + } + } else { + myImportList.reserve(importedCells.size()); + for (const auto& cell : importedCells) { + myImportList.emplace_back(cell, root, static_cast(AttributeSet::owner), -1); + } + } + + // get the distribution of wells + std::vector> parallel_wells; + if (wells) { + if (allowDistributedWells) { + // wells can be split among several processes + auto wellsOnProc = Dune::cpgrid::perforatingWellIndicesOnProc(gIDtoRank, *wells, possibleFutureConnections, grid); + parallel_wells = Dune::cpgrid::computeParallelWells(wellsOnProc, *wells, cc, root); + } else { + // each well is guaranteed to be on a single process + auto wellRanks = getWellRanks(gIDtoRank, wellConnections); + parallel_wells = wellsOnThisRank(*wells, wellRanks, cc, root); + } + } + + return std::make_tuple(std::move(gIDtoRank), + std::move(parallel_wells), + std::move(myExportList), + std::move(myImportList), + std::move(wellConnections)); +} #endif // HAVE_MPI // explicit template instantiations diff --git a/opm/grid/GraphOfGridWrappers.hpp b/opm/grid/GraphOfGridWrappers.hpp index 8e4d9c729..13b5116d5 100644 --- a/opm/grid/GraphOfGridWrappers.hpp +++ b/opm/grid/GraphOfGridWrappers.hpp @@ -26,9 +26,11 @@ #ifndef GRAPH_OF_GRID_WRAPPERS_HEADER #define GRAPH_OF_GRID_WRAPPERS_HEADER +#include #include #include +#include #include #include // defines Zoltan and null-callback-functions @@ -44,6 +46,7 @@ namespace Opm { /// \brief callback function for ZOLTAN_NUM_OBJ_FN /// /// returns the number of vertices in the graph +template > int getGraphOfGridNumVertices(void* pGraph, int *err); /// \brief callback function for ZOLTAN_OBJ_LIST_FN @@ -92,11 +95,58 @@ void getGraphOfGridEdgeList(void *pGraph, float *edgeWeights, int *err); +/// \brief callback function for ZOLTAN_OBJ_LIST_FN +/// +/// fills the vector gIDs with vertex global IDs +/// and the vector objWeights with their weights +void getCoarseGraphVerticesList(void* pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + int weightDim, + float *objWeights, + int *err); + +/// \brief callback function for ZOLTAN_NUM_EDGES_MULTI_FN +/// +/// takes the list of global IDs (gIDs) and fills (consecutively) +/// vector numEdges with the number of their edges +void getCoarseGraphNumEdges(void *pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + int numCells, + ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + int *numEdges, + int *err); + +/// \brief callback function for ZOLTAN_EDGE_LIST_MULTI_FN +/// +/// takes the list of global IDs (gIDs) and fills (consecutively): +/// vector nborGIDs with the list of neighbors (all into 1 vector), +/// vector nborProc with neighbors' process numbers, +/// vector edgeWeights with edge weights. +/// The vector numEdges provides the number of edges for each gID +void getCoarseGraphEdgeList(void *pGraph, + [[maybe_unused]] int dimGlobalID, + [[maybe_unused]] int dimLocalID, + int numCells, + ZOLTAN_ID_PTR gIDs, + [[maybe_unused]] ZOLTAN_ID_PTR lIDs, + int *numEdges, + ZOLTAN_ID_PTR nborGIDs, + int *nborProc, + int weightDim, + float *edgeWeights, + int *err); + /// \brief Register callback functions to Zoltan -template +template void setGraphOfGridZoltanGraphFunctions(Zoltan_Struct *zz, - GraphOfGrid& gog, + GraphType& gog, bool pretendNull); + #endif /// \brief Adds well to the GraphOfGrid @@ -146,8 +196,9 @@ void extendAndSortImportList(std::vector>& importLi /// On root, exportList is extended by well cells that are hidden from the partitioner. /// These cells are also collected and returned so they can be communicated to other ranks. /// \return vector[rank][cell] Each entry contains vector of cells exported to that rank. +template std::vector> -extendRootExportList(const GraphOfGrid& gog, +extendRootExportList(const GOG& gog, std::vector>& exportList, int root, const std::vector& gIDtoRank); @@ -254,6 +305,45 @@ makeImportAndExportLists(const GraphOfGrid& gog, const Id* importGlobalGids, int level); +/// \brief Transform Zoltan output into tuples +/// +/// \param gog CoarseGraphOfGrid, has ref. to CpGrid and has a coarsened partitioning graph +/// \param cc Communication object +/// \param wells Used to extract well names +/// \param wellConnections Contains wells' global IDs, ordered as \param wells. +/// \param possibleFutureConnections parameter needed if allowDistributedWells==true +/// \param root Rank of the process executing the partitioning (usually 0) +/// \param numExport Number of cells in the export list +/// \param numImport Number of cells in the import list +/// \param exportLocalGids Unused. Partitioning is performed on root +/// process that has access to all cells. +/// \param exportGlobalGids Zoltan output: Global IDs of exported cells +/// \param exportToPart Zoltan output: ranks to which cells are exported +/// \param importGlobalGids Zoltan output: Global IDs of cells imported to this rank +/// \param allowDistributedWells should wells be allowed to exist on multiple partitions. +/// \return gIDtoRank A vector indexed by global ID storing the rank of cell +/// parallel_wells A vector of pairs wells.name and bool of "Is wells.name on this rank?" +/// myExportList vector of cells to be moved from this rank +/// myImportList vector of cells to be moved to this rank +template +std::tuple, + std::vector>, + std::vector >, + std::vector > > +makeImportAndExportLists(const CoarseGraphOfGrid& gog, + const Dune::Communication& cc, + const std::vector * wells, + const Dune::cpgrid::WellConnections& wellConnections, + const std::unordered_map>& possibleFutureConnections, + int root, + int numExport, + int numImport, + [[maybe_unused]] const Id* exportLocalGids, + const Id* exportGlobalGids, + const int* exportToPart, + const Id* importGlobalGids, + bool allowDistributedWells); + /// \brief Call Zoltan partitioner on GraphOfGrid /// /// GraphOfGrid represents a well by one vertex, so wells can not be @@ -274,6 +364,27 @@ zoltanPartitioningWithGraphOfGrid(const Dune::CpGrid& grid, const std::map& params, int level); +/// \brief Call Zoltan partitioner on Coarsened graph +/// +/// CoarseGraphOfGrid incorporates transmissibility into the partitioning graph by +/// coarsening the graph +std::tuple, std::vector>, + std::vector >, + std::vector >, + Dune::cpgrid::WellConnections> +zoltanPartitioningWithCoarseGraph(const Dune::CpGrid& grid, + const std::vector * wells, + const std::unordered_map>& possibleFutureConnections, + const Dune::cpgrid::CpGridDataTraits::Communication& cc, + Dune::EdgeWeightMethod edgeWeightMethod, + int root, + const double zoltanImbalanceTol, + bool allowDistributedWells, + const std::map& params, + const Dune::BCRSMatrix>* transGraph, + double coarseThreshold, + int coarsePartitionMaxNodeSize); + /// \brief Make complete export lists from a vector holding destination rank for each global ID /// /// Intended to use on the root, as other ranks can not construct gIDtoRank. @@ -302,6 +413,27 @@ zoltanSerialPartitioningWithGraphOfGrid(const Dune::CpGrid& grid, const double zoltanImbalanceTol, bool allowDistributedWells, const std::map& params); + +/// \brief Call serial Zoltan partitioner on coarse Graph +/// +/// CoarseGraphOfGrid incorporates transmissibility into the partitioning graph by +/// coarsening the graph +std::tuple, std::vector>, + std::vector >, + std::vector >, + Dune::cpgrid::WellConnections> +zoltanSerialPartitioningWithCoarseGraph(const Dune::CpGrid& grid, + const std::vector * wells, + const std::unordered_map>& possibleFutureConnections, + const Dune::cpgrid::CpGridDataTraits::Communication& cc, + Dune::EdgeWeightMethod edgeWeightMethod, + int root, + const double zoltanImbalanceTol, + bool allowDistributedWells, + const std::map& params, + const Dune::BCRSMatrix>* transGraph, + double coarseThreshold, + int coarsePartitionMaxNodeSize); #endif // HAVE_MPI } // end namespace Opm diff --git a/opm/grid/common/GridEnums.hpp b/opm/grid/common/GridEnums.hpp index c7e097a96..a01bfce5e 100644 --- a/opm/grid/common/GridEnums.hpp +++ b/opm/grid/common/GridEnums.hpp @@ -49,7 +49,9 @@ namespace Dune { /// \brief Use METIS for partitioning metis=2, /// \brief use Zoltan on GraphOfGrid for partitioning - zoltanGoG=3 + zoltanGoG=3, + /// \brief Use Zoltan coarse graph + zoltanCG=4 }; } diff --git a/opm/grid/cpgrid/CpGrid.cpp b/opm/grid/cpgrid/CpGrid.cpp index a77084222..ac68d3444 100644 --- a/opm/grid/cpgrid/CpGrid.cpp +++ b/opm/grid/cpgrid/CpGrid.cpp @@ -218,7 +218,10 @@ CpGrid::scatterGrid(EdgeWeightMethod method, [[maybe_unused]] bool allowDistributedWells, [[maybe_unused]] const std::vector& input_cell_part, int level, - [[maybe_unused]] bool useTransToFilterOverlap) + [[maybe_unused]] bool useTransToFilterOverlap, + [[maybe_unused]] const Dune::BCRSMatrix>* transGraph, + [[maybe_unused]] double coarseThreshold, + [[maybe_unused]] int coarsePartitionMaxNodeSize) { // Silence any unused argument warnings that could occur with various configurations. static_cast(wells); @@ -227,6 +230,9 @@ CpGrid::scatterGrid(EdgeWeightMethod method, static_cast(method); static_cast(imbalanceTol); static_cast(level); + static_cast(transGraph); + static_cast(coarseThreshold); + static_cast(coarsePartitionMaxNodeSize); if(!distributed_data_.empty()) { @@ -381,8 +387,17 @@ CpGrid::scatterGrid(EdgeWeightMethod method, #else OPM_THROW(std::runtime_error, "Parallel runs depend on ZOLTAN if useZoltan is true. Please install!"); #endif // HAVE_ZOLTAN - } - else + } else if (partitionMethod == Dune::PartitionMethod::zoltanCG) + { +#ifdef HAVE_ZOLTAN + std::tie(computedCellPart, wells_on_proc, exportList, importList, wellConnections) + = serialPartitioning + ? Opm::zoltanSerialPartitioningWithCoarseGraph(*this, wells, possibleFutureConnections, cc, method, 0, imbalanceTol, allowDistributedWells, partitioningParams, transGraph, coarseThreshold, coarsePartitionMaxNodeSize) + : Opm::zoltanPartitioningWithCoarseGraph(*this, wells, possibleFutureConnections, cc, method, 0, imbalanceTol, allowDistributedWells, partitioningParams, transGraph, coarseThreshold, coarsePartitionMaxNodeSize); +#else + OPM_THROW(std::runtime_error, "Parallel runs depend on ZOLTAN if useZoltan is true. Please install!"); +#endif // HAVE_ZOLTAN + } else { std::tie(computedCellPart, wells_on_proc, exportList, importList, wellConnections) = cpgrid::vanillaPartitionGridOnRoot(*this, wells, possibleFutureConnections, transmissibilities, allowDistributedWells); diff --git a/tests/test_coarsegraphofgrid.cpp b/tests/test_coarsegraphofgrid.cpp new file mode 100644 index 000000000..1b2cf7914 --- /dev/null +++ b/tests/test_coarsegraphofgrid.cpp @@ -0,0 +1,621 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +/* + Copyright 2026 Equinor ASA. + + This file is part of the Open Porous Media project (OPM). + + OPM 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. + + OPM 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 OPM. If not, see . + + Consult the COPYING file in the top-level source directory of this + module for the precise wording of the license and the list of + copyright holders. +*/ + +#include + +#include + +#define BOOST_TEST_MODULE GraphRepresentationOfGrid +#define BOOST_TEST_NO_MAIN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if HAVE_OPM_COMMON +#include +#include +#include +#include +#include +#include +#endif + +#include + +// Create "Transmissibility graph" from grid and set all transmissibilities to 1.0 +std::unique_ptr>> createAdjacencyMatrix(const Dune::CpGrid& grid) +{ + size_t numCells = grid.numCells(); + auto gridView = grid.leafGridView(); + + using ElementMapper = + Dune::MultipleCodimMultipleGeomTypeMapper; + const auto elemMapper = ElementMapper { gridView, Dune::mcmgElementLayout() }; + + Dune::MatrixIndexSet op; + op.resize(numCells, numCells); + + // Create adjacency for transmissibility graph + for (const auto& elem : elements(gridView, Dune::Partitions::interiorBorder)) { + auto d = elemMapper.index(elem); + op.add(d, d); + for (const auto& is : intersections(gridView, elem)) { + if (!is.neighbor()) { + continue; + } + + const auto J = static_cast(elemMapper.index(is.outside())); + op.add(d, J); + } + } + + // Allocate the BCRSMatrix wrapped in a unique_ptr + auto graph = std::make_unique>>(); + op.exportIdx(*graph); + *graph = 1.0; + + return graph; +} + +// basic test to check if the graph was constructed correctly. Copied from test_graphofgrid.cpp +BOOST_AUTO_TEST_CASE(SimpleGraph) +{ + Dune::CpGrid grid; + std::array dims{2,2,2}; + std::array size{2.,2.,2.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + Dune::cpgrid::WellConnections wells; + + auto graph = createAdjacencyMatrix(grid); + double strongConnectionThreshold = 1.1; // no nodes will be merged + int maxNodeSize = 2; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt, + graph.get(), strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wells); + + BOOST_REQUIRE(cgog.size()==8); // number of graph vertices + BOOST_REQUIRE(cgog.getCoarseNodes()[0].size()==1); // weight of all nodes is one + + const std::vector< std::map > edges = cgog.getCoarseEdges(); + + BOOST_REQUIRE(edges[0].size()==3); + auto edgeL = edges[2]; + + BOOST_REQUIRE(edgeL.size()==3); // neighbors of vertex 2 are: 0, 3, 6 + BOOST_REQUIRE(edgeL[0]==1.0); + BOOST_REQUIRE(edgeL[3]==1.0); + BOOST_REQUIRE(edgeL[6]==1.0); + BOOST_REQUIRE_THROW(edgeL.at(4),std::out_of_range); // not a neighbor + + BOOST_REQUIRE_THROW(edges.at(10),std::logic_error); +} + +// basic test to check if nodes get merged. +//Similar to SimpleGraphWithVertexContraction in test_graphofgrid.cpp +BOOST_AUTO_TEST_CASE(MergeTwoNodes) +{ + Dune::CpGrid grid; + std::array dims{2,2,2}; + std::array size{2.,2.,2.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + Dune::cpgrid::WellConnections wells; + + auto graph = createAdjacencyMatrix(grid); + (*graph)[0][1] = 2.0; // Set 0->1 connection to 2 + double strongConnectionThreshold = 1.1; // Node 0 and 1 will be merged + int maxNodeSize = 2; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt, + graph.get(), strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wells); + + BOOST_REQUIRE(cgog.size()==7); // number of graph vertices + BOOST_REQUIRE(cgog.getCoarseNodes()[0].size()==2); // weight of node 0 is two + + const auto edges = cgog.getCoarseEdges(); + + auto edge0 = edges[0]; + BOOST_REQUIRE(edge0.size()==4); + + auto map_to_coarse = cgog.getMapToCoarse(); + + BOOST_REQUIRE(map_to_coarse[0]==0); + BOOST_REQUIRE(map_to_coarse[1]==0); + BOOST_REQUIRE(map_to_coarse[2]==1); + + BOOST_REQUIRE(edge0[map_to_coarse[2]]==1.0); + BOOST_REQUIRE(edge0[map_to_coarse[3]]==1.0); + BOOST_REQUIRE(edge0[map_to_coarse[4]]==1.0); + BOOST_REQUIRE(edge0[map_to_coarse[5]]==1.0); +} + +// basic test to check if the coarse graph gets correct edge weights +BOOST_AUTO_TEST_CASE(CoarseEdgeWeights) +{ + Dune::CpGrid grid; + std::array dims{3,3,1}; + std::array size{2.,2.,2.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + Dune::cpgrid::WellConnections wells; + + auto graph = createAdjacencyMatrix(grid); + (*graph)[0][1] = 2.0; + (*graph)[0][3] = 2.0; + (*graph)[7][8] = 0.0; // Fault between cell 7 and 8 + (*graph)[8][7] = 0.0; + + double strongConnectionThreshold = 1.1; + int maxNodeSize = 3; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt, + graph.get(), strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wells); + + BOOST_REQUIRE(cgog.size()==7); // number of graph vertices + BOOST_REQUIRE(cgog.getCoarseNodes()[0].size()==3); // Node weight of node 0 is 3 + + const std::vector< std::map > edges = cgog.getCoarseEdges(); + auto edge0 = edges[0]; + + BOOST_REQUIRE(edge0.size()==3); // The merged node has three edges + + auto map_to_coarse = cgog.getMapToCoarse(); + BOOST_REQUIRE(edge0[map_to_coarse[2]]==1.0); + BOOST_REQUIRE(edge0[map_to_coarse[4]]==2.0); //Node 0 has two connections to the centre node + BOOST_REQUIRE(edge0[map_to_coarse[6]]==1.0); + + auto edge7 = edges[map_to_coarse[7]]; + auto edge8 = edges[map_to_coarse[8]]; + + BOOST_REQUIRE(edge7.size()==2); // Only two edges because of fault + BOOST_REQUIRE(edge8.size()==1); // Only one edges because of fault + + BOOST_REQUIRE_THROW(edge7.at(map_to_coarse[8]),std::out_of_range); // not a neighbor + BOOST_REQUIRE_THROW(edge8.at(map_to_coarse[7]),std::out_of_range); // not a neighbor +} + + +// basic test to check if the graph uses maxNodeSize correctly +BOOST_AUTO_TEST_CASE(MaxNodeSize) +{ + Dune::CpGrid grid; + std::array dims{3,3,1}; + std::array size{2.,2.,2.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + Dune::cpgrid::WellConnections wells; + + auto graph = createAdjacencyMatrix(grid); + (*graph)[0][1] = 2.0; + (*graph)[0][3] = 3.0; // This is lager, so should be merged + (*graph)[6][7] = 2.0; + (*graph)[7][8] = 2.0; + + double strongConnectionThreshold = 1.1; + int maxNodeSize = 2; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt, + graph.get(), strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wells); + + BOOST_REQUIRE(cgog.size()==7); // number of graph vertices + BOOST_REQUIRE(cgog.getCoarseNodes()[0].size()==2); // weight of node 0 is two + + auto map_to_coarse = cgog.getMapToCoarse(); + BOOST_REQUIRE(map_to_coarse[0]==map_to_coarse[3]); // node 0 merged with node 3 + BOOST_REQUIRE(map_to_coarse[0]!=map_to_coarse[1]); // node 1 not merged with node 0 and 3 + + BOOST_REQUIRE(cgog.getCoarseNodes()[map_to_coarse[6]].size()==2); // weight of node 6 is two + BOOST_REQUIRE(map_to_coarse[6]==map_to_coarse[7]); // node 6 merged with node 7 + BOOST_REQUIRE(map_to_coarse[7]!=map_to_coarse[8]); // node 8 not merged with node 6 and 7 +} + +#if HAVE_OPM_COMMON +namespace { + // create Wells, we only use well name and cell locations + // copied from test_graphofgrid.cpp + auto createConnection (int i, int j, int k) + { + return Opm::Connection(i,j,k,0, 0,Opm::Connection::State::OPEN, + Opm::Connection::Direction::Z, + Opm::Connection::CTFKind::DeckValue, 0, + 5.,Opm::Connection::CTFProperties(),0,false); + } + auto createWell (const std::string& name) + { + using namespace Opm; + return Dune::cpgrid::OpmWellType(name,name,0,0,0,0,0.,WellType(), + Well::ProducerCMode(),Connection::Order(),UnitSystem(), + 0.,0.,false,false,0,Well::GasInflowEquation()); + }; +} // end anonymous namespace +#endif + +#if HAVE_MPI && HAVE_OPM_COMMON +// Check if merging wells work +// Similar to addWellConnections in test_graphofgrid.cpp +BOOST_AUTO_TEST_CASE(MergeWells) +{ + Dune::CpGrid grid; + std::array dims{2,2,2}; + std::array size{1.,1.,1.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + auto wellCon = std::make_shared(); // do not confuse with Dune::cpgrid::WellConnections + wellCon->add(createConnection(0,0,0)); // 0 + wellCon->add(createConnection(0,1,0)); // 2 + wellCon->add(createConnection(0,1,1)); // 6 + std::vector wells; + wells.push_back(createWell("first")); + wells[0].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(0,0,1)); // 4 + wellCon->add(createConnection(1,1,0)); // 3 + wells.push_back(createWell("second")); + wells[1].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(0,0,1)); // 4 + wellCon->add(createConnection(1,0,1)); // 5 + wells.push_back(createWell("third")); // intersects with second + wells[2].updateConnections(wellCon,true); + + Dune::cpgrid::WellConnections wellConnections(wells,std::unordered_map>(),grid); + + auto graph = createAdjacencyMatrix(grid); + (*graph)[0][1] = 2.0; // should not be merged, due to maxNodeSize = 2 + + double strongConnectionThreshold = 1.1; + int maxNodeSize = 2; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt,graph.get(), + strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wellConnections); + + BOOST_REQUIRE(cgog.size()==4); + + int err; + int nVer = Opm::getGraphOfGridNumVertices >(&cgog,&err); + BOOST_REQUIRE(err==ZOLTAN_OK); + BOOST_REQUIRE(nVer == 4); + std::vector gIDs(nVer); + std::vector objWeights(nVer); + getCoarseGraphVerticesList(&cgog, 1, 1, gIDs.data(), nullptr, 1, objWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + std::ranges::sort(gIDs); + BOOST_REQUIRE(objWeights[0]==3.0 && objWeights[1]==1.0 && objWeights[2]==3.0 && objWeights[3]==1.0); + std::vector numEdges(nVer); + getCoarseGraphNumEdges(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + BOOST_REQUIRE(numEdges[0]==3 && numEdges[1]==2 && numEdges[2]==3 && numEdges[3]==2); + int nEdges = 10; // sum of numEdges[i] + std::vector nborGIDs(nEdges); + std::vector nborProc(nEdges); + std::vector edgeWeights(nEdges); + getCoarseGraphEdgeList(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), nborGIDs.data(), nborProc.data(), 1, edgeWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + + // check all edgeWeights. Note that nborGIDs are not sorted + for (int i=0; i<3; ++i) + { + switch (nborGIDs[i]) + { + case 1: BOOST_REQUIRE(edgeWeights[i]==1); break; + case 2: BOOST_REQUIRE(edgeWeights[i]==3); break; + case 3: BOOST_REQUIRE(edgeWeights[i]==1); break; + default: throw("CoarseGraph was constructed badly."); + } + } + for (int i=3; i<5; ++i) + { + switch (nborGIDs[i]) + { + case 0: BOOST_REQUIRE(edgeWeights[i]==1); break; + case 2: BOOST_REQUIRE(edgeWeights[i]==2); break; + default: throw("CoarseGraph was constructed badly."); + } + } + for (int i=5; i<8; ++i) + { + switch (nborGIDs[i]) + { + case 0: BOOST_REQUIRE(edgeWeights[i]==3); break; + case 1: BOOST_REQUIRE(edgeWeights[i]==2); break; + case 3: BOOST_REQUIRE(edgeWeights[i]==2); break; + default: throw("CoarseGraph was constructed badly."); + } + } + for (int i=8; i<10; ++i) + { + switch (nborGIDs[i]) + { + case 0: BOOST_REQUIRE(edgeWeights[i]==1); break; + case 2: BOOST_REQUIRE(edgeWeights[i]==2); break; + default: throw("CoarseGraph was constructed badly."); + } + } +} + +// Check if merging multiple intersecting wells work +// Similar to IntersectingWells in test_graphofgrid.cpp +BOOST_AUTO_TEST_CASE(MergeWellsMoreIntersecting) +{ + Dune::CpGrid grid; + std::array dims{5,4,3}; + std::array size{1.,1.,1.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + auto wellCon = std::make_shared(); // do not confuse with Dune::cpgrid::WellConnections + wellCon->add(createConnection(0,0,0)); // 0 + wellCon->add(createConnection(1,0,0)); // 1 + wellCon->add(createConnection(2,0,0)); // 2 + wellCon->add(createConnection(3,0,0)); // 3 + wellCon->add(createConnection(4,0,0)); // 4 + std::vector wells; + wells.push_back(createWell("first")); + wells[0].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(2,2,2)); // 52 + wellCon->add(createConnection(2,2,1)); // 32 + wellCon->add(createConnection(2,2,0)); // 12 + wells.push_back(createWell("second")); + wells[1].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(4,3,2)); // 59 + wellCon->add(createConnection(3,1,2)); // 48 + wellCon->add(createConnection(2,3,1)); // 37 + wells.push_back(createWell("third")); // + wells[2].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(2,3,1)); // 37 + wellCon->add(createConnection(3,3,1)); // 38 + wellCon->add(createConnection(4,3,1)); // 39 + wellCon->add(createConnection(4,2,1)); // 34 + wells.push_back(createWell("forth")); + wells[3].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(2,0,0)); // 2 + wellCon->add(createConnection(3,1,0)); // 8 + wells.push_back(createWell("fifth")); + wells[4].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(2,0,0)); // 2 + wellCon->add(createConnection(3,3,1)); // 38 + wells.push_back(createWell("sixth")); + wells[5].updateConnections(wellCon,true); + Dune::cpgrid::WellConnections wellConnections(wells,std::unordered_map>(),grid); + + auto graph = createAdjacencyMatrix(grid); + + double strongConnectionThreshold = 1.1; + int maxNodeSize = 2; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt, + graph.get(), strongConnectionThreshold, + maxNodeSize, allowDistWells, 0, wellConnections); + + auto map_to_coarse = cgog.getMapToCoarse(); + BOOST_REQUIRE(cgog.size() == 47); + + int err; + int nVer = Opm::getGraphOfGridNumVertices >(&cgog,&err); + BOOST_REQUIRE(nVer == 47); + std::vector gIDs(nVer); + std::vector objWeights(nVer); + getCoarseGraphVerticesList(&cgog, 1, 1, gIDs.data(), nullptr, 1, objWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + + for (int i=0; i numEdges(nVer); + getCoarseGraphNumEdges(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + BOOST_REQUIRE(numEdges[7]==12); + BOOST_REQUIRE(numEdges[0]==26); + BOOST_REQUIRE(numEdges[42]==3); + + int nEdges = 232; + std::vector nborGIDs(nEdges); + std::vector nborProc(nEdges); + std::vector edgeWeights(nEdges); + getCoarseGraphEdgeList(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), nborGIDs.data(), nborProc.data(), 1, edgeWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + + int checked=0; + for (int i=0; i<26; ++i) + { + switch (nborGIDs[i]) + { + // neighboring two well cells adds up the edge weight + case 3: case 4: case 23: case 27: case 42: case 46: + BOOST_REQUIRE(edgeWeights[i]==2.); + ++checked; + break; + default: BOOST_REQUIRE(edgeWeights[i]==1.); + } + } + BOOST_REQUIRE(checked==6); +} + +// Check if merging wells and transmissibility works +BOOST_AUTO_TEST_CASE(MergeWithTransAndWells) +{ + Dune::CpGrid grid; + std::array dims{4,4,1}; + std::array size{1.,1.,1.}; + grid.createCartesian(dims,size); + if (grid.size(0)==0) + return; + + auto wellCon = std::make_shared(); // do not confuse with Dune::cpgrid::WellConnections + wellCon->add(createConnection(0,0,0)); // 0 + wellCon->add(createConnection(1,0,0)); // 1 + wellCon->add(createConnection(2,0,0)); // 2 + std::vector wells; + wells.push_back(createWell("first")); + wells[0].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(0,2,0)); // 8 + wellCon->add(createConnection(1,2,0)); // 9 + wellCon->add(createConnection(2,2,0)); // 10 + wells.push_back(createWell("second")); + wells[1].updateConnections(wellCon,true); + + wellCon = std::make_shared(); //reset + wellCon->add(createConnection(3,2,0)); // 11 + wellCon->add(createConnection(3,3,0)); // 15 + wells.push_back(createWell("third")); + wells[2].updateConnections(wellCon,true); + + Dune::cpgrid::WellConnections wellConnections(wells,std::unordered_map>(),grid); + + auto graph = createAdjacencyMatrix(grid); + (*graph)[0][4] = 2.0; // should be merged + (*graph)[4][8] = 2.0; // should be merged and add second well too + (*graph)[2][3] = 2.0; // should be merged + (*graph)[3][7] = 2.0; // should be merged + (*graph)[11][10] = 2.0; // not merged because 10 aready merged. If graph[10][11]=2, would be merged + + // How coarsening happens in this example + // + // +---+---+---+---+ +---+---+---+---+ + // | w1| w1| w1| | | | + // +---+---+---+---+ + +---+---+ + + // | | | | | | | | | | + // +---+---+---+---+ -> + +---+---+---+ + // | w2| w2| w2| w3| | | | + // +---+---+---+---+ +---+---+---+ + + // | | | | w3| | | | | | + // +---+---+---+---+ +---+---+---+---+ + + int maxNodeSize = 14; + bool allowDistWells = false; + Opm::CoarseGraphOfGrid cgog(grid, Dune::EdgeWeightMethod::uniformEdgeWgt,graph.get(), + 1.1, maxNodeSize, allowDistWells, 0, wellConnections); + + BOOST_REQUIRE(cgog.getCoarseNodes()[0].size()==9); + auto map_to_coarse = cgog.getMapToCoarse(); + + int err; + int nVer = Opm::getGraphOfGridNumVertices >(&cgog,&err); + BOOST_REQUIRE(nVer == 7); + std::vector gIDs(nVer); + std::vector objWeights(nVer); + getCoarseGraphVerticesList(&cgog, 1, 1, gIDs.data(), nullptr, 1, objWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + + for (int i=0; i numEdges(nVer); + getCoarseGraphNumEdges(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + BOOST_REQUIRE(numEdges[0]==6); + BOOST_REQUIRE(numEdges[1]==2); + BOOST_REQUIRE(numEdges[2]==2); + BOOST_REQUIRE(numEdges[3]==2); + BOOST_REQUIRE(numEdges[4]==2); + BOOST_REQUIRE(numEdges[5]==3); + BOOST_REQUIRE(numEdges[6]==3); + + int nEdges = 20; + std::vector nborGIDs(nEdges); + std::vector nborProc(nEdges); + std::vector edgeWeights(nEdges); + getCoarseGraphEdgeList(&cgog, 1, 1, nVer, gIDs.data(), nullptr, numEdges.data(), nborGIDs.data(), nborProc.data(), 1, edgeWeights.data(), &err); + BOOST_REQUIRE(err==ZOLTAN_OK); + + // Check edgeWeights for the first node + for (int i=0; i<6; ++i) + { + switch (nborGIDs[i]) + { + case 1: + BOOST_REQUIRE(edgeWeights[i]==3.); break; + case 2: + BOOST_REQUIRE(edgeWeights[i]==3.); break; + case 3: + BOOST_REQUIRE(edgeWeights[i]==2.); break; + default: BOOST_REQUIRE(edgeWeights[i]==1.); + } + } +} +#endif + +bool +init_unit_test_func() +{ + return true; +} + +int main(int argc, char** argv) +{ + Dune::MPIHelper::instance(argc, argv); + boost::unit_test::unit_test_main(&init_unit_test_func, + argc, argv); +}