Skip to content

feat: well_manager + well estimator#3972

Open
tjb-ltk wants to merge 140 commits into
developfrom
feature/byer3/wm_we
Open

feat: well_manager + well estimator#3972
tjb-ltk wants to merge 140 commits into
developfrom
feature/byer3/wm_we

Conversation

@tjb-ltk

@tjb-ltk tjb-ltk commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Replaces PR #3735 and extends PR #3971

Overview

This PR introduces

  1. An improved well schema layout and code refactor better suited for well modeling. The previous implementation primarily targeted Jacobian generation requirements for the coupled reservoir and well system.

  2. The well estimator is used to select the active well constraint by solving the well system assuming fixed reservoir conditions and selecting the constraint with the highest or lowest well flowing pressure.

    • the estimator is applied at beginning of Jacobian assembly of the coupled system
    • frequency determined via input setting
    • estimator is a data member of WellControl and a tailed version of PhysicsSolver for wells
    • logic underpins future capability to replace the segmented well model with other formulations

This is a breaking change, detailed migration instructions and migration script are posted at #4081

tjb-ltk and others added 30 commits September 29, 2025 16:50
…aint class hierachy , 3) Remove all old constraint handling methods/variables, 4) it compiles ...
…t compositionalMultiphaseFlow/soreideWhitson/1D_100cells/1D_benchmark.xml
…d convert compositionalMultiphaseFlow/soreideWhitson/1D_100cells/1D_benchmark.xml"

This reverts commit fa61ab1.
…well initialization vagaries assoiated with useSurfaceConditions=0
// set the reference well element where the BHP control is applied
wellControls.setReferenceGravityCoef( refElev * gravVector[2] );
wellControls.setReferenceGravityCoef( refElev * gravVector[2] ); // tjb remove

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually remove?

rankNegPressureIds,
rankNegDensityIds,
rankTotalNegDensityIds );
globalCheck = MpiWrapper::min( localCheck );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this MPI call outside the for loop (for performance reasons)?

Comment on lines +617 to +620
for( integer i=0; i<numNorm; i++ )
{
localResidualNorm[i] = 0.0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading it correctly, there might be an issue when an open unconverged well is followed by a closed well. This branch resets the manager's accumulated maximum residual to zero, while I think closed wells should contribute nothing without clearing residuals already calculated for other wells.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think optionizing the wellResidualNorm on open wells and then max the localResidualNorm sounds safer

if( wellControls.isWellOpen() )
{
  wellResidualNorm = wellControls.calculateLocalWellResidualNorm( time_n, dt,
                                                                  m_nonlinearSolverParameters,
                                                                  subRegion, dofManager, localRhs );
  for( integer i = 0; i < numNorm; i++ )
  {
    localResidualNorm[i] = LvArray::math::max( localResidualNorm[i], wellResidualNorm[i] );
  }
}

Comment on lines +1022 to 1027
ENUM_STRINGS( WellControls_Control,
"BHP",
"phaseVolRate",
"totalVolRate",
"massRate",
"uninitialized" );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum ConstraintTypeId has 7 values, while we are listing only five here. Should we update this list?

Comment on lines +675 to +676
// Assumes useMass is true
currentMassRate = currentTotalRate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a check here on useMass instead of assuming it's true?

}
void WellManager::initializePostSubGroups()
{
#if 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why this code block is ifdef'ed out

Comment on lines +382 to +389
// 13.4) Make sure there is at least one non-BHP constraint
bool const rate_match_found = std::any_of( constraints.begin(), constraints.end(), [&bhp_type]( const auto & constraint_tuple )
{
return std::get< 1 >( constraint_tuple ) != bhp_type;
} );
GEOS_THROW_IF( !rate_match_found,
GEOS_FMT( "Missing rate constraint for {} well {}",
(isProducerWell ? "producer" : "injector"), getName() ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a well has phaseVolRate while defining only a total-volume rate constraint, this check would flag things are good, but i guess initialization would hit issues later.

Comment on lines +372 to +377
string const bhp_type = isProducerWell ? MinimumBHPConstraint::catalogName() : MaximumBHPConstraint::catalogName();
bool const no_match_found = std::none_of( constraints.begin(), constraints.end(), [&bhp_type]( const auto & constraint_tuple )
{
return std::get< 1 >( constraint_tuple ) == bhp_type;
} );
GEOS_THROW_IF( no_match_found,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the sole BHP or rate constraint has constraintActive=0, these existence checks still accept it, while getBHPConstraint and the rate-list helpers filter it out. Subsequent selection either dereferences null or erases an end() iterator because the current constraint is absent from the active list.

Comment on lines +433 to +442
if( !hasZeroRate )
{
for( auto const * constraint : getRateConstraints() )
{
if( isZero( constraint->getConstraintValue( currentTime ) ) )
{
hasZeroRate = true;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When one active secondary rate schedule evaluates to zero while another rate target remains positive, this loop closes the entire well on the first zero (code below). Should we close the well only when every active rate target is zero?

Comment on lines +138 to +140
well.setThermal( isThermal() );
well.setUseMass( m_useMass );
well.registerWellDataOnMesh( subRegion );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this also be setting useTotalMassEquation and allowLocalCompDensityChopping?

Comment on lines +411 to +414
forSubGroups< InjectionConstraint< PhaseVolumeRateConstraint >, ProductionConstraint< PhaseVolumeRateConstraint > >( [&]( auto & constraint )
{
constraint.validatePhaseType( fluid );
} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a liquid-rate constraint becomes limiting, only phase-volume constraints have their phase names validated here. The liquid constraint's m_phaseIndices remains empty, which I believe could lead to issues during assembly (zero Jacobian).

{
real64 const currentValue = currentConstraint.liquidRate();
real64 const constraintValue = this->getConstraintValue( currentTime );
return ( LvArray::math::abs( currentValue ) <= LvArray::math::abs( constraintValue ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read correctly, for a production liquid-rate maximum, this comparison marks values below the target as violations and accepts values above it. This is the reverse of the other maximum-rate constraints and switches control incorrectly.

* @param domain the physical domain object
*/
void chopNegativeDensities( DomainPartition & domain );
#if 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be gone ?

m_useMass( false ),
m_useTotalMassEquation( 1 ),
m_isThermal( 0 ),
m_isCompositional( true ),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should'nt it be an integer and registered for restart ? what happen if we want to restard a non-compositional well ?

array1d< real64 > localResidualNorm, wellResidalNorm;
array1d< real64 > localResidualNormalizer;

if( isThermal() )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sticking to conventionm shouldn't it be set by NDOF or so ?

Comment on lines +609 to +612
if( wellResidalNorm[i] > localResidualNorm[i] )
{
localResidualNorm[i] = wellResidalNorm[i];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if( wellResidalNorm[i] > localResidualNorm[i] )
{
localResidualNorm[i] = wellResidalNorm[i];
}
localResidualNorm[i] = LvArray::math::max( localResidualNorm[i], wellResidualNorm[i] );

// step 1: compute the norm in the subRegion
if( wellControls.isWellOpen( ) )
{
wellResidalNorm = wellControls.calculateLocalWellResidualNorm( time_n,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wellResidalNorm = wellControls.calculateLocalWellResidualNorm( time_n,
wellResidualNorm = wellControls.calculateLocalWellResidualNorm( time_n,

Comment on lines +617 to +620
for( integer i=0; i<numNorm; i++ )
{
localResidualNorm[i] = 0.0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think optionizing the wellResidualNorm on open wells and then max the localResidualNorm sounds safer

if( wellControls.isWellOpen() )
{
  wellResidualNorm = wellControls.calculateLocalWellResidualNorm( time_n, dt,
                                                                  m_nonlinearSolverParameters,
                                                                  subRegion, dofManager, localRhs );
  for( integer i = 0; i < numNorm; i++ )
  {
    localResidualNorm[i] = LvArray::math::max( localResidualNorm[i], wellResidualNorm[i] );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes XML input ci: run code coverage enables running of the code coverage CI jobs ci: run CUDA builds Allows to triggers (costly) CUDA jobs ci: run integrated tests Allows to run the integrated tests in GEOS CI flag: ready for review flag: requires rebaseline Requires rebaseline branch in integratedTests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants