GEOS
CoupledReservoirAndWellKernels.hpp
1 /*
2  * ------------------------------------------------------------------------------------------------------------
3  * SPDX-License-Identifier: LGPL-2.1-only
4  *
5  * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC
6  * Copyright (c) 2018-2024 TotalEnergies
7  * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University
8  * Copyright (c) 2023-2024 Chevron
9  * Copyright (c) 2019- GEOS/GEOSX Contributors
10  * All rights reserved
11  *
12  * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details.
13  * ------------------------------------------------------------------------------------------------------------
14  */
15 
20 #ifndef GEOS_PHYSICSSOLVERS_MULTIPHYSICS_COUPLEDRESERVOIRANDWELLS_HPP
21 #define GEOS_PHYSICSSOLVERS_MULTIPHYSICS_COUPLEDRESERVOIRANDWELLS_HPP
22 
23 
24 #include "common/DataTypes.hpp"
25 #include "common/GEOS_RAJA_Interface.hpp"
26 #include "constitutive/fluid/multifluid/Layouts.hpp"
31 namespace geos
32 {
33 
34 namespace coupledReservoirAndWellKernels
35 {
36 
37 using namespace constitutive;
38 
44 template< integer NC, integer IS_THERMAL >
46 {
47 public:
48 
50  static constexpr integer numComp = NC;
51  static constexpr integer resNumDOF = NC+1+IS_THERMAL;
52 
53  // Well jacobian column and row indicies
56 
59 
60  using CP_Deriv = multifluid::DerivativeOffsetC< NC, IS_THERMAL >;
61 
63 
64 
65 
67  static constexpr integer numDof = WJ_COFFSET::nDer;
68 
70  static constexpr integer numEqn = WJ_ROFFSET::nEqn - 2;
71 
87  globalIndex const rankOffset,
88  string const wellDofKey,
89  WellElementSubRegion const & subRegion,
91  PerforationData const * const perforationData,
92  MultiFluidBase const & fluid,
93 
94  arrayView1d< real64 > const & localRhs,
96  bool const & detectCrossflow,
97  integer & numCrossFlowPerforations,
98  BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags )
99  :
100  m_dt( dt ),
101  m_numPhases ( fluid.numFluidPhases()),
102  m_rankOffset( rankOffset ),
103  m_compPerfRate( perforationData->getField< fields::well::compPerforationRate >() ),
104  m_dCompPerfRate( perforationData->getField< fields::well::dCompPerforationRate >() ),
105  m_perfWellElemIndex( perforationData->getField< fields::perforation::wellElementIndex >() ),
106  m_wellElemDofNumber( subRegion.getReference< array1d< globalIndex > >( wellDofKey ) ),
107  m_resElemDofNumber( resDofNumber ),
108  m_resElementRegion( perforationData->getField< fields::perforation::reservoirElementRegion >() ),
109  m_resElementSubRegion( perforationData->getField< fields::perforation::reservoirElementSubRegion >() ),
110  m_resElementIndex( perforationData->getField< fields::perforation::reservoirElementIndex >() ),
111  m_localRhs( localRhs ),
112  m_localMatrix( localMatrix ),
113  m_detectCrossflow( detectCrossflow ),
114  m_numCrossFlowPerforations( numCrossFlowPerforations ),
115  m_useTotalMassEquation ( kernelFlags.isSet( isothermalCompositionalMultiphaseBaseKernels::KernelFlags::TotalMassEquation ) )
116  { }
117 
118 
127  template< typename FUNC = NoOpFunc >
129  inline
130  void computeFlux( localIndex const iperf,
131  FUNC && compFluxKernelOp = NoOpFunc{} ) const
132  {
133 
134  using namespace compositionalMultiphaseUtilities;
135  // local working variables and arrays
136  stackArray1d< localIndex, 2* numComp > eqnRowIndices( 2 * numComp );
137  stackArray1d< globalIndex, 2*resNumDOF > dofColIndices( 2 * resNumDOF );
138 
139  stackArray1d< real64, 2 * numComp > localPerf( 2 * numComp );
140  stackArray2d< real64, 2 * resNumDOF * 2 * numComp > localPerfJacobian( 2 * numComp, 2 * resNumDOF );
141 
142  // get the reservoir (sub)region and element indices
143  localIndex const er = m_resElementRegion[iperf];
144  localIndex const esr = m_resElementSubRegion[iperf];
145  localIndex const ei = m_resElementIndex[iperf];
146 
147  // get the well element index for this perforation
148  localIndex const iwelem = m_perfWellElemIndex[iperf];
149  globalIndex const resOffset = m_resElemDofNumber[er][esr][ei];
150  globalIndex const wellElemOffset = m_wellElemDofNumber[iwelem];
151 
152  for( integer ic = 0; ic < numComp; ++ic )
153  {
154  eqnRowIndices[TAG::RES * numComp + ic] = LvArray::integerConversion< localIndex >( resOffset - m_rankOffset ) + ic;
155  eqnRowIndices[TAG::WELL * numComp + ic] = LvArray::integerConversion< localIndex >( wellElemOffset - m_rankOffset ) + WJ_ROFFSET::MASSBAL + ic;
156  }
157  // Note res and well have same col lineup for P and compdens
158  for( integer jdof = 0; jdof < NC+1; ++jdof )
159  {
160  dofColIndices[TAG::RES * resNumDOF + jdof] = resOffset + jdof;
161  dofColIndices[TAG::WELL * resNumDOF + jdof] = wellElemOffset + WJ_COFFSET::dP + jdof;
162  }
163  // For temp its different
164  if constexpr ( IS_THERMAL )
165  {
166  dofColIndices[TAG::RES * resNumDOF + NC+1 ] = resOffset + NC+1;
167  dofColIndices[TAG::WELL * resNumDOF + NC+1 ] = wellElemOffset + WJ_COFFSET::dT;
168  }
169  // populate local flux vector and derivatives
170  for( integer ic = 0; ic < numComp; ++ic )
171  {
172  localPerf[TAG::RES * numComp + ic] = m_dt * m_compPerfRate[iperf][ic];
173  localPerf[TAG::WELL * numComp + ic] = -m_dt * m_compPerfRate[iperf][ic];
174 
175  if( m_detectCrossflow )
176  {
177  if( m_compPerfRate[iperf][ic] > LvArray::NumericLimits< real64 >::epsilon )
178  {
179  m_numCrossFlowPerforations += 1;
180  }
181  }
182  for( integer ke = 0; ke < 2; ++ke )
183  {
184  localIndex localDofIndexPres = ke * resNumDOF;
185 
186  localPerfJacobian[TAG::RES * numComp + ic][localDofIndexPres] = m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dP];
187  localPerfJacobian[TAG::WELL * numComp + ic][localDofIndexPres] = -m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dP];
188  for( integer jc = 0; jc < numComp; ++jc )
189  {
190  localIndex const localDofIndexComp = localDofIndexPres + jc + 1;
191 
192  localPerfJacobian[TAG::RES * numComp + ic][localDofIndexComp] = m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dC+jc];
193  localPerfJacobian[TAG::WELL * numComp + ic][localDofIndexComp] = -m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dC+jc];
194  }
195  if constexpr ( IS_THERMAL )
196  {
197  localIndex localDofIndexTemp = localDofIndexPres + NC + 1;
198  localPerfJacobian[TAG::RES * numComp + ic][localDofIndexTemp] = m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dT];
199  localPerfJacobian[TAG::WELL * numComp + ic][localDofIndexTemp] = -m_dt * m_dCompPerfRate[iperf][ke][ic][CP_Deriv::dT];
200  }
201  }
202  }
203 
204  if( m_useTotalMassEquation )
205  {
206  // Apply equation/variable change transformation(s)
207  stackArray1d< real64, 2 * resNumDOF > work( 2 * resNumDOF );
208  shiftBlockRowsAheadByOneAndReplaceFirstRowWithColumnSum( numComp, numComp, resNumDOF * 2, 2, localPerfJacobian, work );
209  shiftBlockElementsAheadByOneAndReplaceFirstElementWithSum( numComp, numComp, 2, localPerf );
210  }
211 
212  for( localIndex i = 0; i < localPerf.size(); ++i )
213  {
214  if( eqnRowIndices[i] >= 0 && eqnRowIndices[i] < m_localMatrix.numRows() )
215  {
216  m_localMatrix.addToRowBinarySearchUnsorted< parallelDeviceAtomic >( eqnRowIndices[i],
217  dofColIndices.data(),
218  localPerfJacobian[i].dataIfContiguous(),
219  2 * resNumDOF );
220  RAJA::atomicAdd( parallelDeviceAtomic{}, &m_localRhs[eqnRowIndices[i]], localPerf[i] );
221  }
222  }
223  compFluxKernelOp( resOffset, wellElemOffset, dofColIndices, iwelem );
224 
225  }
226 
227 
236  template< typename POLICY, typename KERNEL_TYPE >
237  static void
238  launch( localIndex const numElements,
239  KERNEL_TYPE const & kernelComponent )
240  {
242  forAll< POLICY >( numElements, [=] GEOS_HOST_DEVICE ( localIndex const ie )
243  {
244  kernelComponent.computeFlux( ie );
245 
246  } );
247  }
248 
249 protected:
250 
252  real64 const m_dt;
253 
256 
257  globalIndex const m_rankOffset;
258  // Perfoation variables
259  arrayView2d< real64 const > const m_compPerfRate;
260  arrayView4d< real64 const > const m_dCompPerfRate;
261  arrayView1d< localIndex const > const m_perfWellElemIndex;
262 
263  // Element region, subregion, index
264  arrayView1d< globalIndex const > const m_wellElemDofNumber;
266  arrayView1d< localIndex const > const m_resElementRegion;
267  arrayView1d< localIndex const > const m_resElementSubRegion;
268  arrayView1d< localIndex const > const m_resElementIndex;
269 
270  // RHS and Jacobian
271  arrayView1d< real64 > const m_localRhs;
273 
274  bool const m_detectCrossflow;
275  integer & m_numCrossFlowPerforations;
276  integer const m_useTotalMassEquation;
277 };
278 
283 {
284 public:
285 
299  template< typename POLICY >
300  static void
301  createAndLaunch( integer const numComps,
302  real64 const dt,
303  globalIndex const rankOffset,
304  string const wellDofKey,
305  WellElementSubRegion const & subRegion,
307  PerforationData const * const perforationData,
308  MultiFluidBase const & fluid,
309  BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags,
310  bool const & detectCrossflow,
311  integer & numCrossFlowPerforations,
312  arrayView1d< real64 > const & localRhs,
314  )
315  {
316  isothermalCompositionalMultiphaseBaseKernels::internal::kernelLaunchSelectorCompSwitch( numComps, [&]( auto NC )
317  {
318  integer constexpr NUM_COMP = NC();
319 
321  kernelType kernel( dt, rankOffset, wellDofKey, subRegion, resDofNumber, perforationData,
322  fluid, localRhs, localMatrix, detectCrossflow, numCrossFlowPerforations, kernelFlags );
323  kernelType::template launch< POLICY >( perforationData->size(), kernel );
324  } );
325 
326  }
327 };
328 
329 
335 template< integer NC, integer IS_THERMAL >
337 {
338 public:
341  static constexpr integer numComp = NC;
342  static constexpr integer resNumDOF = NC+1+IS_THERMAL;
343 
344  // Well jacobian column and row indicies
347 
350 
351  using CP_Deriv = multifluid::DerivativeOffsetC< NC, IS_THERMAL >;
352 
354 
355  using Base::m_dt;
356  using Base::m_localRhs;
357  using Base::m_localMatrix;
358  using Base::m_rankOffset;
359 
360 
361 
363  static constexpr integer numDof = WJ_COFFSET::nDer;
364 
366  static constexpr integer numEqn = WJ_ROFFSET::nEqn - 2;
367 
383  integer const isProducer,
384  globalIndex const rankOffset,
385  string const wellDofKey,
386  WellElementSubRegion const & subRegion,
388  PerforationData const * const perforationData,
389  MultiFluidBase const & fluid,
390  arrayView1d< real64 > const & localRhs,
391  CRSMatrixView< real64, globalIndex const > const & localMatrix,
392  bool const & detectCrossflow,
393  integer & numCrossFlowPerforations,
394  BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags )
395  : Base( dt,
396  rankOffset,
397  wellDofKey,
398  subRegion,
399  resDofNumber,
400  perforationData,
401  fluid,
402  localRhs,
403  localMatrix,
404  detectCrossflow,
405  numCrossFlowPerforations,
406  kernelFlags ),
407  m_isProducer( isProducer ),
408  m_globalWellElementIndex( subRegion.getGlobalWellElementIndex() ),
409  m_energyPerfFlux( perforationData->getField< fields::well::energyPerforationFlux >()),
410  m_dEnergyPerfFlux( perforationData->getField< fields::well::dEnergyPerforationFlux >())
411 
412  { }
413 
414 
424  inline
425  void computeFlux( localIndex const iperf ) const
426  {
427  Base::computeFlux( iperf, [&] ( globalIndex const & resOffset,
428  globalIndex const & wellElemOffset,
430  localIndex const iwelem )
431  {
432  // No energy equation if top element and Injector
433  // Top element defined by global index == 0
434  // Assumption is global index == 0 is top segment with fixed temp BC
435  if( !m_isProducer )
436  {
437  if( m_globalWellElementIndex[iwelem] == 0 )
438  return;
439  }
440  // local working variables and arrays
441  stackArray1d< localIndex, 2* numComp > eqnRowIndices( 2 );
442 
444  stackArray2d< real64, 2 * resNumDOF * 2 * numComp > localPerfJacobian( 2, 2 * resNumDOF );
445 
446 
447  // equantion offsets - note res and well have different equation lineups
448  eqnRowIndices[TAG::RES ] = LvArray::integerConversion< localIndex >( resOffset - m_rankOffset ) + NC + 1;
449  eqnRowIndices[TAG::WELL ] = LvArray::integerConversion< localIndex >( wellElemOffset - m_rankOffset ) + WJ_ROFFSET::ENERGYBAL;
450 
451  // populate local flux vector and derivatives
452  localPerf[TAG::RES ] = m_dt * m_energyPerfFlux[iperf];
453  localPerf[TAG::WELL ] = -m_dt * m_energyPerfFlux[iperf];
454 
455  for( integer ke = 0; ke < 2; ++ke )
456  {
457  localIndex localDofIndexPres = ke * resNumDOF;
458  localPerfJacobian[TAG::RES ][localDofIndexPres] = m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dP];
459  localPerfJacobian[TAG::WELL ][localDofIndexPres] = -m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dP];
460 
461  // populate local flux vector and derivatives
462  for( integer ic = 0; ic < numComp; ++ic )
463  {
464  localIndex const localDofIndexComp = localDofIndexPres + ic + 1;
465  localPerfJacobian[TAG::RES ][localDofIndexComp] = m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dC+ic];
466  localPerfJacobian[TAG::WELL][localDofIndexComp] = -m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dC+ic];
467  }
468  localPerfJacobian[TAG::RES ][localDofIndexPres+NC+1] = m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dT];
469  localPerfJacobian[TAG::WELL][localDofIndexPres+NC+1] = -m_dt * m_dEnergyPerfFlux[iperf][ke][CP_Deriv::dT];
470  }
471 
472 
473  for( localIndex i = 0; i < localPerf.size(); ++i )
474  {
475  if( eqnRowIndices[i] >= 0 && eqnRowIndices[i] < m_localMatrix.numRows() )
476  {
477  m_localMatrix.template addToRowBinarySearchUnsorted< parallelDeviceAtomic >( eqnRowIndices[i],
478  dofColIndices.data(),
479  localPerfJacobian[i].dataIfContiguous(),
480  2 * resNumDOF );
481  RAJA::atomicAdd( parallelDeviceAtomic{}, &m_localRhs[eqnRowIndices[i]], localPerf[i] );
482  }
483  }
484  } );
485 
486 
487  }
488 
489 
498  template< typename POLICY, typename KERNEL_TYPE >
499  static void
500  launch( localIndex const numElements,
501  KERNEL_TYPE const & kernelComponent )
502  {
504  forAll< POLICY >( numElements, [=] GEOS_HOST_DEVICE ( localIndex const ie )
505  {
506  kernelComponent.computeFlux( ie );
507 
508  } );
509  }
510 
511 protected:
512 
515 
518 
521  arrayView3d< real64 const > const m_dEnergyPerfFlux;
522 };
523 
528 {
529 public:
530 
544  template< typename POLICY >
545  static void
546  createAndLaunch( integer const numComps,
547  integer const isProducer,
548  real64 const dt,
549  globalIndex const rankOffset,
550  string const wellDofKey,
551  WellElementSubRegion const & subRegion,
553  PerforationData const * const perforationData,
554  MultiFluidBase const & fluid,
555  BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags,
556  bool const & detectCrossflow,
557  integer & numCrossFlowPerforations,
558  arrayView1d< real64 > const & localRhs,
560  )
561  {
562  isothermalCompositionalMultiphaseBaseKernels::internal::kernelLaunchSelectorCompSwitch( numComps, [&]( auto NC )
563  {
564  integer constexpr NUM_COMP = NC();
565 
567  kernelType kernel( dt, isProducer, rankOffset, wellDofKey, subRegion, resDofNumber, perforationData,
568  fluid, localRhs, localMatrix, detectCrossflow, numCrossFlowPerforations, kernelFlags );
569  kernelType::template launch< POLICY >( perforationData->size(), kernel );
570  } );
571 
572  }
573 };
574 
575 } // end namespace coupledReservoirAndWellKernels
576 
577 } // end namespace geos
578 
579 #endif // GEOS_PHYSICSSOLVERS_MULTIPHYSICS_COUPLEDRESERVOIRANDWELLS_HPP
GEOS_HOST_DEVICE void shiftBlockRowsAheadByOneAndReplaceFirstRowWithColumnSum(integer const numRowsToShift, integer const numRowsInBlock, integer const numColsInBlock, integer const numBlocks, MATRIX &&mat, VEC &&work)
In each block, shift the elements from 0 to numRowsToShift-1, shifts all rows one position ahead and ...
GEOS_HOST_DEVICE void shiftBlockElementsAheadByOneAndReplaceFirstElementWithSum(integer const numRowsToShift, integer const numRowsInBlock, integer const numBlocks, VEC &&v)
In each block, shift the elements from 0 to numRowsToShift-1 one position ahead and replaces the firs...
#define GEOS_HOST_DEVICE
Marks a host-device function.
Definition: GeosxMacros.hpp:49
#define GEOS_MARK_FUNCTION
Mark function with both Caliper and NVTX if enabled.
typename ElementViewAccessor< VIEWTYPE >::NestedViewTypeConst ElementViewConst
The ElementViewAccessor at the ElementRegionManager level is the type resulting from ElementViewAcces...
This class describes a collection of local well elements and perforations.
static void createAndLaunch(integer const numComps, real64 const dt, globalIndex const rankOffset, string const wellDofKey, WellElementSubRegion const &subRegion, ElementRegionManager::ElementViewConst< arrayView1d< globalIndex const > > const resDofNumber, PerforationData const *const perforationData, MultiFluidBase const &fluid, BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags, bool const &detectCrossflow, integer &numCrossFlowPerforations, arrayView1d< real64 > const &localRhs, CRSMatrixView< real64, globalIndex const > const &localMatrix)
Create a new kernel and launch.
GEOS_HOST_DEVICE void computeFlux(localIndex const iperf, FUNC &&compFluxKernelOp=NoOpFunc{}) const
Compute the local flux contributions to the residual and Jacobian.
IsothermalCompositionalMultiPhaseFluxKernel(real64 const dt, globalIndex const rankOffset, string const wellDofKey, WellElementSubRegion const &subRegion, ElementRegionManager::ElementViewConst< arrayView1d< globalIndex const > > const resDofNumber, PerforationData const *const perforationData, MultiFluidBase const &fluid, arrayView1d< real64 > const &localRhs, CRSMatrixView< real64, globalIndex const > const &localMatrix, bool const &detectCrossflow, integer &numCrossFlowPerforations, BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags)
Constructor for the kernel interface.
static void launch(localIndex const numElements, KERNEL_TYPE const &kernelComponent)
Performs the kernel launch.
static void createAndLaunch(integer const numComps, integer const isProducer, real64 const dt, globalIndex const rankOffset, string const wellDofKey, WellElementSubRegion const &subRegion, ElementRegionManager::ElementViewConst< arrayView1d< globalIndex const > > const resDofNumber, PerforationData const *const perforationData, MultiFluidBase const &fluid, BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags, bool const &detectCrossflow, integer &numCrossFlowPerforations, arrayView1d< real64 > const &localRhs, CRSMatrixView< real64, globalIndex const > const &localMatrix)
Create a new kernel and launch.
GEOS_HOST_DEVICE void computeFlux(localIndex const iperf) const
Compute the local flux contributions to the residual and Jacobian.
ThermalCompositionalMultiPhaseFluxKernel(real64 const dt, integer const isProducer, globalIndex const rankOffset, string const wellDofKey, WellElementSubRegion const &subRegion, ElementRegionManager::ElementViewConst< arrayView1d< globalIndex const > > const resDofNumber, PerforationData const *const perforationData, MultiFluidBase const &fluid, arrayView1d< real64 > const &localRhs, CRSMatrixView< real64, globalIndex const > const &localMatrix, bool const &detectCrossflow, integer &numCrossFlowPerforations, BitFlags< isothermalCompositionalMultiphaseBaseKernels::KernelFlags > kernelFlags)
Constructor for the kernel interface.
static void launch(localIndex const numElements, KERNEL_TYPE const &kernelComponent)
Performs the kernel launch.
arrayView1d< globalIndex const > m_globalWellElementIndex
Global index of local element.
localIndex size() const
Get the "size" of the group, which determines the number of elements in resizable wrappers.
Definition: Group.hpp:1315
ArrayView< T, 1 > arrayView1d
Alias for 1D array view.
Definition: DataTypes.hpp:180
StackArray< T, 2, MAXSIZE > stackArray2d
Alias for 2D stack array.
Definition: DataTypes.hpp:204
LvArray::CRSMatrixView< T, COL_INDEX, localIndex const, LvArray::ChaiBuffer > CRSMatrixView
Alias for CRS Matrix View.
Definition: DataTypes.hpp:310
GEOS_GLOBALINDEX_TYPE globalIndex
Global index type (for indexing objects across MPI partitions).
Definition: DataTypes.hpp:88
StackArray< T, 1, MAXSIZE > stackArray1d
Alias for 1D stack array.
Definition: DataTypes.hpp:188
double real64
64-bit floating point type.
Definition: DataTypes.hpp:99
GEOS_LOCALINDEX_TYPE localIndex
Local index type (for indexing objects within an MPI partition).
Definition: DataTypes.hpp:85
std::int32_t integer
Signed integer type.
Definition: DataTypes.hpp:82
ArrayView< T, 4, USD > arrayView4d
Alias for 4D array view.
Definition: DataTypes.hpp:228
ArrayView< T, 2, USD > arrayView2d
Alias for 2D array view.
Definition: DataTypes.hpp:196
Array< T, 1 > array1d
Alias for 1D array.
Definition: DataTypes.hpp:176
ArrayView< T, 3, USD > arrayView3d
Alias for 3D array view.
Definition: DataTypes.hpp:212