// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- // vi: set et ts=4 sw=4 sts=4: /* 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 2 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. */ /** * \file * * \copydoc Opm::TracerModel */ #ifndef OPM_TRACER_MODEL_HPP #define OPM_TRACER_MODEL_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Opm::Properties { template struct EnableTracerModel { using type = UndefinedProperty; }; } // namespace Opm::Properties namespace Opm { /*! * \ingroup BlackOilSimulator * * \brief A class which handles tracers as specified in by ECL */ template class TracerModel : public GenericTracerModel, GetPropType, GetPropType, GetPropType, GetPropType, GetPropType> { using BaseType = GenericTracerModel, GetPropType, GetPropType, GetPropType, GetPropType, GetPropType>; using Simulator = GetPropType; using GridView = GetPropType; using Grid = GetPropType; using Scalar = GetPropType; using Stencil = GetPropType; using FluidSystem = GetPropType; using ElementContext = GetPropType; using RateVector = GetPropType; using Indices = GetPropType; using TracerEvaluation = DenseAd::Evaluation; using TracerMatrix = typename BaseType::TracerMatrix; using TracerVector = typename BaseType::TracerVector; using TracerVectorSingle = typename BaseType::TracerVectorSingle; enum { numEq = getPropValue() }; enum { numPhases = FluidSystem::numPhases }; enum { waterPhaseIdx = FluidSystem::waterPhaseIdx }; enum { oilPhaseIdx = FluidSystem::oilPhaseIdx }; enum { gasPhaseIdx = FluidSystem::gasPhaseIdx }; public: explicit TracerModel(Simulator& simulator) : BaseType(simulator.vanguard().gridView(), simulator.vanguard().eclState(), simulator.vanguard().cartesianIndexMapper(), simulator.model().dofMapper(), simulator.vanguard().cellCentroids()) , simulator_(simulator) , tbatch({waterPhaseIdx, oilPhaseIdx, gasPhaseIdx}) , wat_(tbatch[0]) , oil_(tbatch[1]) , gas_(tbatch[2]) , element_chunks_(simulator.gridView(), Dune::Partitions::all, ThreadManager::maxThreads()) { } /* The initialization of the tracer model is a three step process: 1. The init() method is called. This will allocate buffers and initialize some phase index stuff. If this is a normal run the initial tracer concentrations will be assigned from the TBLK or TVDPF keywords. 2. [Restart only:] The tracer concentration are read from the restart file and the concentrations are applied with repeated calls to the setTracerConcentration() method. This is currently done in the eclwriter::beginRestart() method. 3. Internally the tracer model manages the concentrations in "batches" for the oil, water and gas tracers respectively. The batches should be initialized with the initial concentration, that must be performed after the concentration values have been assigned. This is done in method prepareTracerBatches() called from eclproblem::finishInit(). */ void init(bool rst) { this->doInit(rst, simulator_.model().numGridDof(), gasPhaseIdx, oilPhaseIdx, waterPhaseIdx); } void prepareTracerBatches() { for (std::size_t tracerIdx = 0; tracerIdx < this->tracerPhaseIdx_.size(); ++tracerIdx) { if (this->tracerPhaseIdx_[tracerIdx] == FluidSystem::waterPhaseIdx) { if (! FluidSystem::phaseIsActive(FluidSystem::waterPhaseIdx)){ throw std::runtime_error("Water tracer specified for non-water fluid system: " + this->name(tracerIdx)); } wat_.addTracer(tracerIdx, this->tracerConcentration_[tracerIdx]); } else if (this->tracerPhaseIdx_[tracerIdx] == FluidSystem::oilPhaseIdx) { if (! FluidSystem::phaseIsActive(FluidSystem::oilPhaseIdx)){ throw std::runtime_error("Oil tracer specified for non-oil fluid system: " + this->name(tracerIdx)); } oil_.addTracer(tracerIdx, this->tracerConcentration_[tracerIdx]); } else if (this->tracerPhaseIdx_[tracerIdx] == FluidSystem::gasPhaseIdx) { if (! FluidSystem::phaseIsActive(FluidSystem::gasPhaseIdx)){ throw std::runtime_error("Gas tracer specified for non-gas fluid system: " + this->name(tracerIdx)); } gas_.addTracer(tracerIdx, this->tracerConcentration_[tracerIdx]); } // resize free and solution volume storages vol1_[0][this->tracerPhaseIdx_[tracerIdx]]. resize(this->freeTracerConcentration_[tracerIdx].size()); vol1_[1][this->tracerPhaseIdx_[tracerIdx]]. resize(this->freeTracerConcentration_[tracerIdx].size()); dVol_[0][this->tracerPhaseIdx_[tracerIdx]]. resize(this->solTracerConcentration_[tracerIdx].size()); dVol_[1][this->tracerPhaseIdx_[tracerIdx]]. resize(this->solTracerConcentration_[tracerIdx].size()); } // will be valid after we move out of tracerMatrix_ TracerMatrix* base = this->tracerMatrix_.get(); for (auto& tr : this->tbatch) { if (tr.numTracer() != 0) { if (this->tracerMatrix_) { tr.mat = std::move(this->tracerMatrix_); } else { tr.mat = std::make_unique(*base); } } } } void beginTimeStep() { if (this->numTracers() == 0) { return; } OPM_TIMEBLOCK(tracerUpdateCache); updateStorageCache(); } /*! * \brief Informs the tracer model that a time step has just been finished. */ void endTimeStep() { if (this->numTracers() == 0) { return; } OPM_TIMEBLOCK(tracerAdvance); advanceTracerFields(); } /*! * \brief This method writes the complete state of all tracer * to the hard disk. */ template void serialize(Restarter&) { /* not implemented */ } /*! * \brief This method restores the complete state of the tracer * from disk. * * It is the inverse of the serialize() method. */ template void deserialize(Restarter&) { /* not implemented */ } template void serializeOp(Serializer& serializer) { serializer(static_cast(*this)); serializer(tbatch); } protected: using TracerTypeIdx = typename BaseType::TracerTypeIdx; using BaseType::Free; using BaseType::Solution; // compute volume associated with free/solution concentration template Scalar computeVolume_(const int tracerPhaseIdx, const unsigned globalDofIdx, const unsigned timeIdx) const { const auto& intQuants = simulator_.model().intensiveQuantities(globalDofIdx, timeIdx); const auto& fs = intQuants.fluidState(); constexpr Scalar min_volume = 1e-10; if constexpr (Index == Free) { return std::max(decay(fs.saturation(tracerPhaseIdx)) * decay(fs.invB(tracerPhaseIdx)) * decay(intQuants.porosity()), min_volume); } else { // vaporized oil if (tracerPhaseIdx == FluidSystem::oilPhaseIdx && FluidSystem::enableVaporizedOil()) { return std::max(decay(fs.saturation(FluidSystem::gasPhaseIdx)) * decay(fs.invB(FluidSystem::gasPhaseIdx)) * decay(fs.Rv()) * decay(intQuants.porosity()), min_volume); } // dissolved gas else if (tracerPhaseIdx == FluidSystem::gasPhaseIdx && FluidSystem::enableDissolvedGas()) { return std::max(decay(fs.saturation(FluidSystem::oilPhaseIdx)) * decay(fs.invB(FluidSystem::oilPhaseIdx)) * decay(fs.Rs()) * decay(intQuants.porosity()), min_volume); } return min_volume; } } template std::pair computeFlux_(const int tracerPhaseIdx, const ElementContext& elemCtx, const unsigned scvfIdx, const unsigned timeIdx) const { const auto& stencil = elemCtx.stencil(timeIdx); const auto& scvf = stencil.interiorFace(scvfIdx); const auto& extQuants = elemCtx.extensiveQuantities(scvfIdx, timeIdx); const unsigned inIdx = extQuants.interiorIndex(); Scalar v; unsigned upIdx; if constexpr (Index == Free) { upIdx = extQuants.upstreamIndex(tracerPhaseIdx); const auto& intQuants = elemCtx.intensiveQuantities(upIdx, timeIdx); const auto& fs = intQuants.fluidState(); v = decay(extQuants.volumeFlux(tracerPhaseIdx)) * decay(fs.invB(tracerPhaseIdx)); } else { if (tracerPhaseIdx == FluidSystem::oilPhaseIdx && FluidSystem::enableVaporizedOil()) { upIdx = extQuants.upstreamIndex(FluidSystem::gasPhaseIdx); const auto& intQuants = elemCtx.intensiveQuantities(upIdx, timeIdx); const auto& fs = intQuants.fluidState(); v = decay(fs.invB(FluidSystem::gasPhaseIdx)) * decay(extQuants.volumeFlux(FluidSystem::gasPhaseIdx)) * decay(fs.Rv()); } // dissolved gas else if (tracerPhaseIdx == FluidSystem::gasPhaseIdx && FluidSystem::enableDissolvedGas()) { upIdx = extQuants.upstreamIndex(FluidSystem::oilPhaseIdx); const auto& intQuants = elemCtx.intensiveQuantities(upIdx, timeIdx); const auto& fs = intQuants.fluidState(); v = decay(fs.invB(FluidSystem::oilPhaseIdx)) * decay(extQuants.volumeFlux(FluidSystem::oilPhaseIdx)) * decay(fs.Rs()); } else { upIdx = 0; v = 0.0; } } const Scalar A = scvf.area(); return inIdx == upIdx ? std::pair{A * v * variable(1.0, 0), true} : std::pair{A * v, false}; } template Scalar storage1_(const TrRe& tr, const unsigned tIdx, const unsigned I, const unsigned I1, const bool cache) { if (cache) { return tr.storageOfTimeIndex1_[tIdx][I][Index]; } else { return computeVolume_(tr.phaseIdx_, I1, 1) * tr.concentration_[tIdx][I1][Index]; } } template void assembleTracerEquationVolume(TrRe& tr, const ElementContext& elemCtx, const Scalar scvVolume, const Scalar dt, unsigned I, unsigned I1) { if (tr.numTracer() == 0) { return; } const TracerEvaluation fVol = computeVolume_(tr.phaseIdx_, I, 0) * variable(1.0, 0); const TracerEvaluation sVol = computeVolume_(tr.phaseIdx_, I, 0) * variable(1.0, 0); dVol_[Solution][tr.phaseIdx_][I] += sVol.value() * scvVolume - vol1_[1][tr.phaseIdx_][I]; dVol_[Free][tr.phaseIdx_][I] += fVol.value() * scvVolume - vol1_[0][tr.phaseIdx_][I]; for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { // Free part const Scalar fStorageOfTimeIndex0 = fVol.value() * tr.concentration_[tIdx][I][Free]; const Scalar fLocalStorage = (fStorageOfTimeIndex0 - storage1_(tr, tIdx, I, I1, elemCtx.enableStorageCache())) * scvVolume / dt; tr.residual_[tIdx][I][Free] += fLocalStorage; // residual + flux // Solution part const Scalar sStorageOfTimeIndex0 = sVol.value() * tr.concentration_[tIdx][I][Solution]; const Scalar sLocalStorage = (sStorageOfTimeIndex0 - storage1_(tr, tIdx, I, I1, elemCtx.enableStorageCache())) * scvVolume / dt; tr.residual_[tIdx][I][Solution] += sLocalStorage; // residual + flux } // Derivative matrix (*tr.mat)[I][I][Free][Free] += fVol.derivative(0) * scvVolume/dt; (*tr.mat)[I][I][Solution][Solution] += sVol.derivative(0) * scvVolume/dt; } template void assembleTracerEquationFlux(TrRe& tr, const ElementContext& elemCtx, unsigned scvfIdx, unsigned I, unsigned J, const Scalar dt) { if (tr.numTracer() == 0) { return; } const auto& [fFlux, isUpF] = computeFlux_(tr.phaseIdx_, elemCtx, scvfIdx, 0); const auto& [sFlux, isUpS] = computeFlux_(tr.phaseIdx_, elemCtx, scvfIdx, 0); dVol_[Solution][tr.phaseIdx_][I] += sFlux.value() * dt; dVol_[Free][tr.phaseIdx_][I] += fFlux.value() * dt; const int fGlobalUpIdx = isUpF ? I : J; const int sGlobalUpIdx = isUpS ? I : J; for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { // Free and solution fluxes tr.residual_[tIdx][I][Free] += fFlux.value()*tr.concentration_[tIdx][fGlobalUpIdx][Free]; // residual + flux tr.residual_[tIdx][I][Solution] += sFlux.value()*tr.concentration_[tIdx][sGlobalUpIdx][Solution]; // residual + flux } // Derivative matrix if (isUpF){ (*tr.mat)[J][I][Free][Free] = -fFlux.derivative(0); (*tr.mat)[I][I][Free][Free] += fFlux.derivative(0); } if (isUpS) { (*tr.mat)[J][I][Solution][Solution] = -sFlux.derivative(0); (*tr.mat)[I][I][Solution][Solution] += sFlux.derivative(0); } } template void assembleTracerEquationWell(TrRe& tr, const Well& well) { if (tr.numTracer() == 0) { return; } const auto& eclWell = well.wellEcl(); // Init. well output to zero auto& tracerRate = this->wellTracerRate_[eclWell.seqIndex()]; tracerRate.reserve(tr.numTracer()); auto& solTracerRate = this->wellSolTracerRate_[eclWell.seqIndex()]; solTracerRate.reserve(tr.numTracer()); auto& freeTracerRate = this->wellFreeTracerRate_[eclWell.seqIndex()]; freeTracerRate.reserve(tr.numTracer()); auto* mswTracerRate = eclWell.isMultiSegment() ? &this->mSwTracerRate_[eclWell.seqIndex()] : nullptr; if (mswTracerRate) { mswTracerRate->reserve(tr.numTracer()); } for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { tracerRate.emplace_back(this->name(tr.idx_[tIdx]), 0.0); freeTracerRate.emplace_back(this->wellfname(tr.idx_[tIdx]), 0.0); solTracerRate.emplace_back(this->wellsname(tr.idx_[tIdx]), 0.0); if (eclWell.isMultiSegment()) { auto& wtr = mswTracerRate->emplace_back(this->name(tr.idx_[tIdx])); wtr.rate.reserve(eclWell.getConnections().size()); for (std::size_t i = 0; i < eclWell.getConnections().size(); ++i) { wtr.rate.emplace(eclWell.getConnections().get(i).segment(), 0.0); } } } std::vector wtracer(tr.numTracer()); for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { wtracer[tIdx] = this->currentConcentration_(eclWell, this->name(tr.idx_[tIdx]), simulator_.problem().wellModel().summaryState()); } const Scalar dt = simulator_.timeStepSize(); const auto& ws = simulator_.problem().wellModel().wellState().well(well.name()); for (std::size_t i = 0; i < ws.perf_data.size(); ++i) { const auto I = ws.perf_data.cell_index[i]; const Scalar rate = well.volumetricSurfaceRateForConnection(I, tr.phaseIdx_); Scalar rate_s; if (tr.phaseIdx_ == FluidSystem::oilPhaseIdx && FluidSystem::enableVaporizedOil()) { rate_s = ws.perf_data.phase_mixing_rates[i][ws.vaporized_oil]; } else if (tr.phaseIdx_ == FluidSystem::gasPhaseIdx && FluidSystem::enableDissolvedGas()) { rate_s = ws.perf_data.phase_mixing_rates[i][ws.dissolved_gas]; } else { rate_s = 0.0; } const Scalar rate_f = rate - rate_s; if (rate_f > 0) { for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { const Scalar delta = rate_f * wtracer[tIdx]; // Injection of free tracer only tr.residual_[tIdx][I][Free] -= delta; // Store _injector_ tracer rate for reporting // (can be done here since WTRACER is constant) tracerRate[tIdx].rate += delta; freeTracerRate[tIdx].rate += delta; if (eclWell.isMultiSegment()) { (*mswTracerRate)[tIdx].rate[eclWell.getConnections().get(i).segment()] += delta; } } dVol_[Free][tr.phaseIdx_][I] -= rate_f * dt; } else if (rate_f < 0) { for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { const Scalar delta = rate_f * wtracer[tIdx]; // Store _injector_ tracer rate for cross-flowing well connections // (can be done here since WTRACER is constant) tracerRate[tIdx].rate += delta; freeTracerRate[tIdx].rate += delta; // Production of free tracer tr.residual_[tIdx][I][Free] -= rate_f * tr.concentration_[tIdx][I][Free]; } dVol_[Free][tr.phaseIdx_][I] -= rate_f * dt; // Derivative matrix for free tracer producer (*tr.mat)[I][I][Free][Free] -= rate_f * variable(1.0, 0).derivative(0); } if (rate_s < 0) { for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { // Production of solution tracer tr.residual_[tIdx][I][Solution] -= rate_s * tr.concentration_[tIdx][I][Solution]; } dVol_[Solution][tr.phaseIdx_][I] -= rate_s * dt; // Derivative matrix for solution tracer producer (*tr.mat)[I][I][Solution][Solution] -= rate_s * variable(1.0, 0).derivative(0); } } } template void assembleTracerEquationSource(TrRe& tr, const Scalar dt, unsigned I) { if (tr.numTracer() == 0) { return; } // Skip if solution tracers do not exist if (tr.phaseIdx_ == FluidSystem::waterPhaseIdx || (tr.phaseIdx_ == FluidSystem::gasPhaseIdx && !FluidSystem::enableDissolvedGas()) || (tr.phaseIdx_ == FluidSystem::oilPhaseIdx && !FluidSystem::enableVaporizedOil())) { return; } const Scalar& dsVol = dVol_[Solution][tr.phaseIdx_][I]; const Scalar& dfVol = dVol_[Free][tr.phaseIdx_][I]; // Source term determined by sign of dsVol: if dsVol > 0 then ms -> mf, else mf -> ms for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { if (dsVol >= 0) { const auto delta = (dfVol / dt) * tr.concentration_[tIdx][I][Free]; tr.residual_[tIdx][I][Free] -= delta; tr.residual_[tIdx][I][Solution] += delta; } else { const auto delta = (dsVol / dt) * tr.concentration_[tIdx][I][Solution]; tr.residual_[tIdx][I][Free] += delta; tr.residual_[tIdx][I][Solution] -= delta; } } // Derivative matrix if (dsVol >= 0) { const auto delta = (dfVol / dt) * variable(1.0, 0).derivative(0); (*tr.mat)[I][I][Free][Free] -= delta; (*tr.mat)[I][I][Solution][Free] += delta; } else { const auto delta = (dsVol / dt) * variable(1.0, 0).derivative(0); (*tr.mat)[I][I][Free][Solution] += delta; (*tr.mat)[I][I][Solution][Solution] -= delta; } } void assembleTracerEquations_() { // Note that we formulate the equations in terms of a concentration update // (compared to previous time step) and not absolute concentration. // This implies that current concentration (tr.concentration_[][]) contributes // to the rhs both through storage and flux terms. // Compare also advanceTracerFields(...) below. DeferredLogger local_deferredLogger{}; OPM_BEGIN_PARALLEL_TRY_CATCH() { OPM_TIMEBLOCK(tracerAssemble); for (auto& tr : tbatch) { if (tr.numTracer() != 0) { (*tr.mat) = 0.0; for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { tr.residual_[tIdx] = 0.0; } } } this->wellTracerRate_.clear(); this->wellFreeTracerRate_.clear(); this->wellSolTracerRate_.clear(); // educated guess for new container size const auto num_msw = this->mSwTracerRate_.size(); this->mSwTracerRate_.clear(); // Well terms const auto& wellPtrs = simulator_.problem().wellModel().localNonshutWells(); this->wellTracerRate_.reserve(wellPtrs.size()); this->wellFreeTracerRate_.reserve(wellPtrs.size()); this->wellSolTracerRate_.reserve(wellPtrs.size()); this->mSwTracerRate_.reserve(num_msw); for (const auto& wellPtr : wellPtrs) { for (auto& tr : tbatch) { this->assembleTracerEquationWell(tr, *wellPtr); } } // Parallel loop over element chunks #ifdef _OPENMP #pragma omp parallel for #endif for (const auto& chunk : element_chunks_) { ElementContext elemCtx(simulator_); const Scalar dt = elemCtx.simulator().timeStepSize(); for (const auto& elem : chunk) { elemCtx.updateStencil(elem); const std::size_t I = elemCtx.globalSpaceIndex(/*dofIdx=*/ 0, /*timeIdx=*/0); if (elem.partitionType() != Dune::InteriorEntity) { // Dirichlet boundary conditions for parallel matrix // This is safe as each element has a unique I. So each thread // always writes to different memory locations in the shared arrays. for (const auto& tr : tbatch) { if (tr.numTracer() != 0) { (*tr.mat)[I][I][0][0] = 1.; (*tr.mat)[I][I][1][1] = 1.; } } continue; } elemCtx.updateAllIntensiveQuantities(); elemCtx.updateAllExtensiveQuantities(); const Scalar extrusionFactor = elemCtx.intensiveQuantities(/*dofIdx=*/ 0, /*timeIdx=*/0).extrusionFactor(); Valgrind::CheckDefined(extrusionFactor); assert(isfinite(extrusionFactor)); assert(extrusionFactor > 0.0); const Scalar scvVolume = elemCtx.stencil(/*timeIdx=*/0).subControlVolume(/*dofIdx=*/ 0).volume() * extrusionFactor; const std::size_t I1 = elemCtx.globalSpaceIndex(/*dofIdx=*/ 0, /*timeIdx=*/1); // This is safe as each element has a unique I. So each thread // always writes to different memory locations in the shared arrays. for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } this->assembleTracerEquationVolume(tr, elemCtx, scvVolume, dt, I, I1); } const std::size_t numInteriorFaces = elemCtx.numInteriorFaces(/*timIdx=*/0); for (unsigned scvfIdx = 0; scvfIdx < numInteriorFaces; scvfIdx++) { const auto& face = elemCtx.stencil(0).interiorFace(scvfIdx); const unsigned j = face.exteriorIndex(); const unsigned J = elemCtx.globalSpaceIndex(/*dofIdx=*/ j, /*timIdx=*/0); for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } this->assembleTracerEquationFlux(tr, elemCtx, scvfIdx, I, J, dt); } } // Source terms (mass transfer between free and solution tracer) for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } this->assembleTracerEquationSource(tr, dt, I); } } } } OPM_END_PARALLEL_TRY_CATCH_LOG(local_deferredLogger, "assembleTracerEquations() failed: ", true, simulator_.gridView().comm()) // Communicate overlap using grid Communication for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } auto handle = VectorVectorDataHandle>(tr.residual_, simulator_.gridView()); simulator_.gridView().communicate(handle, Dune::InteriorBorder_All_Interface, Dune::ForwardCommunication); } } template void updateElem(TrRe& tr, const Scalar scvVolume, const unsigned globalDofIdx) { const Scalar vol1 = computeVolume_(tr.phaseIdx_, globalDofIdx, 0); vol1_[Index][tr.phaseIdx_][globalDofIdx] = vol1 * scvVolume; dVol_[Index][tr.phaseIdx_][globalDofIdx] = 0.0; for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { tr.storageOfTimeIndex1_[tIdx][globalDofIdx][Index] = vol1 * tr.concentrationInitial_[tIdx][globalDofIdx][Index]; } } void updateStorageCache() { for (auto& tr : tbatch) { if (tr.numTracer() != 0) { tr.concentrationInitial_ = tr.concentration_; } } // Parallel loop over element chunks #ifdef _OPENMP #pragma omp parallel for #endif for (const auto& chunk : element_chunks_) { ElementContext elemCtx(simulator_); for (const auto& elem : chunk) { elemCtx.updatePrimaryStencil(elem); elemCtx.updatePrimaryIntensiveQuantities(/*timeIdx=*/0); const Scalar extrusionFactor = elemCtx.intensiveQuantities(/*dofIdx=*/ 0, /*timeIdx=*/0).extrusionFactor(); const Scalar scvVolume = elemCtx.stencil(/*timeIdx=*/0).subControlVolume(/*dofIdx=*/ 0).volume() * extrusionFactor; const unsigned globalDofIdx = elemCtx.globalSpaceIndex(0, /*timeIdx=*/0); for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } // This is safe as each element has a unique globalDofIdx. So each thread // always writes to different memory locations in the shared arrays. updateElem(tr, scvVolume, globalDofIdx); updateElem(tr, scvVolume, globalDofIdx); } } } } template void copyForOutput(TrRe& tr, const std::vector& dx, const Scalar S, const unsigned tIdx, const unsigned globalDofIdx, std::vector& sc) { constexpr Scalar tol_gas_sat = 1e-6; tr.concentration_[tIdx][globalDofIdx][Index] -= dx[tIdx][globalDofIdx][Index]; if (tr.concentration_[tIdx][globalDofIdx][Index] < 0.0 || S < tol_gas_sat) { tr.concentration_[tIdx][globalDofIdx][Index] = 0.0; } sc[tr.idx_[tIdx]][globalDofIdx] = tr.concentration_[tIdx][globalDofIdx][Index]; } template void assignRates(const TrRe& tr, const Well& eclWell, const std::size_t i, const std::size_t I, const Scalar rate, std::vector>& tracerRate, std::vector>* mswTracerRate, std::vector>& splitRate) { if (rate < 0) { for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { // Store _producer_ free tracer rate for reporting const Scalar delta = rate * tr.concentration_[tIdx][I][Index]; tracerRate[tIdx].rate += delta; splitRate[tIdx].rate += delta; if (eclWell.isMultiSegment()) { (*mswTracerRate)[tIdx].rate[eclWell.getConnections().get(i).segment()] += delta; } } } } void advanceTracerFields() { assembleTracerEquations_(); for (auto& tr : tbatch) { if (tr.numTracer() == 0) { continue; } // Note that we solve for a concentration update (compared to previous time step) // Confer also assembleTracerEquations_(...) above. std::vector dx(tr.concentration_); for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { dx[tIdx] = 0.0; } const bool converged = this->linearSolveBatchwise_(*tr.mat, dx, tr.residual_); if (!converged) { OpmLog::warning("### Tracer model: Linear solver did not converge. ###"); } OPM_TIMEBLOCK(tracerPost); for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { for (std::size_t globalDofIdx = 0; globalDofIdx < tr.concentration_[tIdx].size(); ++globalDofIdx) { // New concetration. Concentrations that are negative or where free/solution phase is not // present are set to zero const auto& intQuants = simulator_.model().intensiveQuantities(globalDofIdx, 0); const auto& fs = intQuants.fluidState(); const Scalar Sf = decay(fs.saturation(tr.phaseIdx_)); Scalar Ss = 0.0; if (tr.phaseIdx_ == FluidSystem::gasPhaseIdx && FluidSystem::enableDissolvedGas()) { Ss = decay(fs.saturation(FluidSystem::oilPhaseIdx)); } else if (tr.phaseIdx_ == FluidSystem::oilPhaseIdx && FluidSystem::enableVaporizedOil()) { Ss = decay(fs.saturation(FluidSystem::gasPhaseIdx)); } copyForOutput(tr, dx, Sf, tIdx, globalDofIdx, this->freeTracerConcentration_); copyForOutput(tr, dx, Ss, tIdx, globalDofIdx, this->solTracerConcentration_); } } // Store _producer_ tracer rate for reporting const auto& wellPtrs = simulator_.problem().wellModel().localNonshutWells(); for (const auto& wellPtr : wellPtrs) { const auto& eclWell = wellPtr->wellEcl(); // Injection rates already reported during assembly if (!eclWell.isProducer()) { continue; } Scalar rateWellPos = 0.0; Scalar rateWellNeg = 0.0; const std::size_t well_index = simulator_.problem().wellModel().wellState().index(eclWell.name()).value(); const auto& ws = simulator_.problem().wellModel().wellState().well(well_index); auto& tracerRate = this->wellTracerRate_[eclWell.seqIndex()]; auto& freeTracerRate = this->wellFreeTracerRate_[eclWell.seqIndex()]; auto& solTracerRate = this->wellSolTracerRate_[eclWell.seqIndex()]; auto* mswTracerRate = eclWell.isMultiSegment() ? &this->mSwTracerRate_[eclWell.seqIndex()] : nullptr; for (std::size_t i = 0; i < ws.perf_data.size(); ++i) { const auto I = ws.perf_data.cell_index[i]; const Scalar rate = wellPtr->volumetricSurfaceRateForConnection(I, tr.phaseIdx_); Scalar rate_s; if (tr.phaseIdx_ == FluidSystem::oilPhaseIdx && FluidSystem::enableVaporizedOil()) { rate_s = ws.perf_data.phase_mixing_rates[i][ws.vaporized_oil]; } else if (tr.phaseIdx_ == FluidSystem::gasPhaseIdx && FluidSystem::enableDissolvedGas()) { rate_s = ws.perf_data.phase_mixing_rates[i][ws.dissolved_gas]; } else { rate_s = 0.0; } const Scalar rate_f = rate - rate_s; assignRates(tr, eclWell, i, I, rate_f, tracerRate, mswTracerRate, freeTracerRate); assignRates(tr, eclWell, i, I, rate_s, tracerRate, mswTracerRate, solTracerRate); if (rate < 0) { rateWellNeg += rate; } else { rateWellPos += rate; } } // TODO: Some inconsistencies here that perhaps should be clarified. // The "offical" rate as reported below is occasionally significant // different from the sum over connections (as calculated above). Only observed // for small values, neglible for the rate itself, but matters when used to // calculate tracer concentrations. const Scalar official_well_rate_total = simulator_.problem().wellModel().wellState().well(well_index).surface_rates[tr.phaseIdx_]; const Scalar rateWellTotal = official_well_rate_total; if (rateWellTotal > rateWellNeg) { // Cross flow constexpr Scalar bucketPrDay = 10.0 / (1000. * 3600. * 24.); // ... keeps (some) trouble away const Scalar factor = (rateWellTotal < -bucketPrDay) ? rateWellTotal / rateWellNeg : 0.0; for (int tIdx = 0; tIdx < tr.numTracer(); ++tIdx) { tracerRate[tIdx].rate *= factor; } } } } } Simulator& simulator_; // This struct collects tracers of the same type (i.e, transported in same phase). // The idea being that, under the assumption of linearity, tracers of same type can // be solved in concert, having a common system matrix but separate right-hand-sides. // Since oil or gas tracers appears in dual compositions when VAPOIL respectively DISGAS // is active, the template argument is intended to support future extension to these // scenarios by supplying an extended vector type. template struct TracerBatch { std::vector idx_; const int phaseIdx_; std::vector concentrationInitial_; std::vector concentration_; std::vector storageOfTimeIndex1_; std::vector residual_; std::unique_ptr mat; bool operator==(const TracerBatch& rhs) const { return this->concentrationInitial_ == rhs.concentrationInitial_ && this->concentration_ == rhs.concentration_; } static TracerBatch serializationTestObject() { TracerBatch result(4); result.idx_ = {1,2,3}; result.concentrationInitial_ = {5.0, 6.0}; result.concentration_ = {7.0, 8.0}; result.storageOfTimeIndex1_ = {9.0, 10.0, 11.0}; result.residual_ = {12.0, 13.0}; return result; } template void serializeOp(Serializer& serializer) { serializer(concentrationInitial_); serializer(concentration_); } TracerBatch(int phaseIdx = 0) : phaseIdx_(phaseIdx) {} int numTracer() const { return idx_.size(); } void addTracer(const int idx, const TV& concentration) { const int numGridDof = concentration.size(); idx_.emplace_back(idx); concentrationInitial_.emplace_back(concentration); concentration_.emplace_back(concentration); residual_.emplace_back(numGridDof); storageOfTimeIndex1_.emplace_back(numGridDof); } }; std::array,numPhases> tbatch; TracerBatch& wat_; TracerBatch& oil_; TracerBatch& gas_; std::array,numPhases>,2> vol1_; std::array,numPhases>,2> dVol_; ElementChunks element_chunks_; }; } // namespace Opm #endif // OPM_TRACER_MODEL_HPP