opm-simulators
HybridNewton.hpp
1 // -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 // vi: set et ts=4 sw=4 sts=4:
3 /*
4  Copyright 2025 NORCE Research AS
5 
6  This file is part of the Open Porous Media project (OPM).
7 
8  OPM is free software: you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation, either version 3 of the License, or
11  (at your option) any later version.
12 
13  OPM is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  GNU General Public License for more details.
17 
18  You should have received a copy of the GNU General Public License
19  along with OPM. If not, see <http://www.gnu.org/licenses/>.
20 */
21 
22 #ifndef HYBRID_NEWTON_HPP
23 #define HYBRID_NEWTON_HPP
24 
25 
27 #include <opm/simulators/flow/HybridNewtonConfig.hpp>
28 #include <opm/input/eclipse/Units/UnitSystem.hpp>
29 
30 #include <opm/ml/ml_model.hpp>
31 
32 #include <limits>
33 #include <string>
34 #include <vector>
35 
36 namespace Opm {
37 
58 template <typename TypeTag>
60 {
61 protected:
68 
69  enum { numPhases = FluidSystem::numPhases };
70 
71  enum { gasPhaseIdx = FluidSystem::gasPhaseIdx };
72  enum { oilPhaseIdx = FluidSystem::oilPhaseIdx };
73  enum { waterPhaseIdx = FluidSystem::waterPhaseIdx };
74 
75  static constexpr bool compositionSwitchEnabled =
76  Indices::compositionSwitchIdx != std::numeric_limits<unsigned>::max();
77 
78 public:
79  explicit BlackOilHybridNewton(Simulator& simulator)
80  : simulator_(simulator)
81  , configsLoaded_(false)
82  {}
83 
102  {
103  // Check if flag activated
104  if (!Parameters::Get<Parameters::UseHybridNewton>())
105  return;
106 
107  validateFluidSystem();
108 
109  if (!configsLoaded_) {
110  std::string config_file = Parameters::Get<Parameters::HybridNewtonConfigFile>();
111  PropertyTree pt(config_file);
112  for (const auto& model_key : pt.get_child_keys()) {
113  HybridNewtonConfig config(pt.get_child(model_key));
114  config.validateConfig(compositionSwitchEnabled);
115  configs_.push_back(std::move(config));
116  }
117  configsLoaded_ = true;
118  }
119 
120  Scalar current_time = simulator_.time();
121  // Find and apply all models that should run at this time
122  for (const auto& config : configs_) {
123  if (shouldApplyHybridNewton(current_time, config)) {
124  runHybridNewton(config);
125  }
126  }
127  }
128 
129 private:
144  void runHybridNewton(const HybridNewtonConfig& config)
145  {
146  auto input = constructInputTensor(config);
147  auto output = constructOutputTensor(input, config);
148  updateInitialGuess(output, config);
149  }
150 
151 protected:
152  void validateFluidSystem()
153  {
154  const auto& eclState = simulator_.vanguard().eclState();
155  const auto& phases = eclState.runspec().phases();
156 
157  bool hasWater = phases.active(Phase::WATER);
158  bool hasGas = phases.active(Phase::GAS);
159  bool hasOil = phases.active(Phase::OIL);
160  bool hasSolvent = phases.active(Phase::SOLVENT);
161 
162  // Check for three-phase black oil system
163  if (!(hasWater && hasOil && hasGas && !hasSolvent)) {
164  OPM_THROW(std::runtime_error,
165  "HybridNewton: Unsupported fluid system. Only three-phase black oil is supported.");
166  }
167  }
168 
189  bool shouldApplyHybridNewton(Scalar current_time, const HybridNewtonConfig& config) const
190  {
191  if (config.apply_times.size() == 1) {
192  // Apply exactly at one point in time (with optional tolerance)
193  constexpr Scalar tolerance = 1e-6;
194  bool apply = std::abs(current_time - config.apply_times[0]) < tolerance;
195  return apply;
196  }
197 
198  if (config.apply_times.size() == 2) {
199  Scalar start_time = config.apply_times[0];
200  Scalar end_time = config.apply_times[1];
201  bool apply = (current_time >= start_time) && (current_time <= end_time);
202  return apply;
203  }
204 
205  // If apply_times has an unexpected number of entries
206  return false;
207  }
208 
209 
234  ML::Tensor<Evaluation>
236  {
237  const auto& features = config.input_features;
238 
239  // compute total length directly
240  std::size_t input_tensor_length = 0;
241  for (const auto& feature : features) {
242  const FeatureSpec& spec = feature.second;
243  if (spec.actual_name == "TIMESTEP") {
244  input_tensor_length += 1;
245  } else {
246  input_tensor_length += config.n_cells;
247  }
248  }
249 
250  ML::Tensor<Evaluation> input(input_tensor_length);
251  std::size_t offset = 0;
252 
253  // fill in exact feature order from config
254  for (const auto& feature: features) {
255  const FeatureSpec& spec = feature.second;
256 
257  if (spec.actual_name == "TIMESTEP") {
258  auto value = static_cast<Evaluation>(getScalarFeatureValue(spec));
259  input(offset++) = value;
260  } else {
261  for (std::size_t i = 0; i < config.n_cells; ++i) {
262  auto value = static_cast<Evaluation>(
263  getPerCellFeatureValue(spec, config.cell_indices[i])
264  );
265  input(offset + i) = value;
266  }
267  offset += config.n_cells;
268  }
269  }
270 
271  return input;
272  }
273 
288  Scalar getScalarFeatureValue(const FeatureSpec& spec)
289  {
290  Scalar value = 0.0;
291 
292  if (spec.actual_name == "TIMESTEP") {
293  value = simulator_.timeStepSize();
294  } else {
295  OPM_THROW(std::runtime_error, "Unknown scalar feature: " + spec.actual_name);
296  }
297 
298  value = spec.transform.apply(value);
299  return spec.scaler.scale(value);
300  }
301 
319  Scalar getPerCellFeatureValue(const FeatureSpec& spec, int cell_index)
320  {
321  const auto& intQuants = simulator_.model().intensiveQuantities(cell_index, 0);
322  const auto& fs = intQuants.fluidState();
323  const auto& unitSyst = simulator_.vanguard().schedule().getUnits();
324 
325  Scalar value = 0.0;
326 
327  if (spec.actual_name == "PRESSURE") {
328  value = getValue(fs.pressure(oilPhaseIdx));
329  value = unitSyst.from_si(UnitSystem::measure::pressure, value);
330  } else if (spec.actual_name == "SWAT") {
331  value = getValue(fs.saturation(waterPhaseIdx));
332  } else if (spec.actual_name == "SGAS") {
333  value = getValue(fs.saturation(gasPhaseIdx));
334  } else if (spec.actual_name == "SOIL") {
335  value = getValue(fs.saturation(oilPhaseIdx));
336  } else if (spec.actual_name == "RS") {
337  value = getValue(fs.Rs());
338  value = unitSyst.from_si(UnitSystem::measure::gas_oil_ratio, value);
339  } else if (spec.actual_name == "RV") {
340  value = getValue(fs.Rv());
341  value = unitSyst.from_si(UnitSystem::measure::oil_gas_ratio, value);
342  } else if (spec.actual_name == "PERMX") {
343  const auto& eclState = simulator_.vanguard().eclState();
344  const auto& fp = eclState.fieldProps();
345  auto permX = fp.get_double("PERMX");
346  value = permX[cell_index];
347  value = unitSyst.from_si(UnitSystem::measure::permeability, value);
348  } else {
349  OPM_THROW(std::runtime_error, "Unknown per-cell feature: " + spec.actual_name);
350  }
351 
352  Scalar transformed = spec.transform.apply(value);
353  Scalar scaled = spec.scaler.scale(transformed);
354  return scaled;
355  }
356 
357 
376  ML::Tensor<Evaluation>
377  constructOutputTensor(const ML::Tensor<Evaluation>& input,
378  const HybridNewtonConfig& config)
379  {
380  const auto& features = config.output_features;
381  const int n_features = features.size();
382 
383  ML::NNModel<Evaluation> model;
384  model.loadModel(config.model_path);
385 
386  ML::Tensor<Evaluation> output(1, config.n_cells * n_features);
387  model.apply(input, output);
388 
389  return output;
390  }
391 
419  void updateInitialGuess(ML::Tensor<Evaluation>& output,
420  const HybridNewtonConfig& config)
421  {
422  const auto& features = config.output_features;
423 
424  FeatureFlags flags = flagFeatures(features);
425 
426  const auto& unitSyst = simulator_.vanguard().schedule().getUnits();
427 
428  for (std::size_t i = 0; i < config.n_cells; ++i) {
429  const int cell_idx = config.cell_indices[i];
430  const auto& intQuants = simulator_.model().intensiveQuantities(cell_idx, /*timeIdx*/0);
431  auto fs = intQuants.fluidState();
432 
433  int feature_idx = 0;
434 
435  // Temp variables per cell
436  Scalar sw_val = -1.0;
437  Scalar so_val = -1.0;
438  Scalar sg_val = -1.0;
439  Scalar po_val = -1.0;
440 
441  for (const auto& [name, spec] : features) {
442 
443  auto scaled_value = getValue(output(feature_idx * config.n_cells + i));
444 
445  // Inverse scaling
446  Scalar raw_value = spec.scaler.unscale(scaled_value);
447 
448  // Inverse transform
449  raw_value = spec.transform.applyInverse(raw_value);
450 
451  if (spec.is_delta) {
452  if (spec.actual_name == "PRESSURE") {
453  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::pressure, getValue(fs.pressure(oilPhaseIdx)));
454  } else if (spec.actual_name == "SWAT") {
455  raw_value += getValue(fs.saturation(waterPhaseIdx));
456  } else if (spec.actual_name == "SOIL") {
457  raw_value += getValue(fs.saturation(oilPhaseIdx));
458  } else if (spec.actual_name == "SGAS") {
459  raw_value += getValue(fs.saturation(gasPhaseIdx));
460  } else if (spec.actual_name == "RS") {
461  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::gas_oil_ratio, getValue(fs.Rs()));
462  } else if (spec.actual_name == "RV") {
463  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::oil_gas_ratio, getValue(fs.Rv()));
464  } else {
465  OPM_THROW(std::runtime_error, "Unknown delta feature: " + name);
466  }
467  }
468 
469  if (spec.actual_name == "PRESSURE") {
470  po_val = unitSyst.to_si(UnitSystem::measure::pressure, raw_value);
471  } else if (spec.actual_name == "SWAT") {
472  sw_val = raw_value;
473  } else if (spec.actual_name == "SOIL") {
474  so_val = raw_value;
475  } else if (spec.actual_name == "SGAS") {
476  sg_val = raw_value;
477  } else if (spec.actual_name == "RS") {
478  if constexpr (compositionSwitchEnabled) {
479  raw_value = unitSyst.to_si(UnitSystem::measure::gas_oil_ratio, raw_value);
480  fs.setRs(raw_value);
481  }
482  } else if (spec.actual_name == "RV") {
483  if constexpr (compositionSwitchEnabled) {
484  raw_value = unitSyst.to_si(UnitSystem::measure::oil_gas_ratio, raw_value);
485  fs.setRv(raw_value);
486  }
487  } else {
488  OPM_THROW(std::runtime_error, "Unknown output feature: " + name);
489  }
490 
491  ++feature_idx;
492  }
493 
494  int sat_count = static_cast<int>(flags.has_SWAT) +
495  static_cast<int>(flags.has_SOIL) +
496  static_cast<int>(flags.has_SGAS);
497 
498  if (sat_count >= 2) {
499  Scalar sw = sw_val;
500  Scalar so = so_val;
501  Scalar sg = sg_val;
502 
503  if (!flags.has_SWAT) {
504  sw = 1.0 - so - sg;
505  } else if (!flags.has_SOIL) {
506  so = 1.0 - sw - sg;
507  } else if (!flags.has_SGAS) {
508  sg = 1.0 - sw - so;
509  }
510 
511  sw = max(0.0, min(sw, 1.0));
512  so = max(0.0, min(so, 1.0));
513  sg = max(0.0, min(sg, 1.0));
514 
515  Scalar sum = sw + so + sg;
516  if (sum <= 1e-12) {
517  OPM_THROW(std::runtime_error, "Saturation sum is zero in cell " + std::to_string(cell_idx));
518  }
519 
520  fs.setSaturation(waterPhaseIdx, sw / sum);
521  fs.setSaturation(oilPhaseIdx, so / sum);
522  fs.setSaturation(gasPhaseIdx, sg / sum);
523  }
524 
525  if (flags.has_PRESSURE) {
526  std::array<Evaluation, numPhases> pC;
527  const auto& materialParams = simulator_.problem().materialLawParams(cell_idx);
528  MaterialLaw::capillaryPressures(pC, materialParams, fs);
529 
530  for (unsigned phaseIdx = 0; phaseIdx < numPhases; ++phaseIdx) {
531  if (!FluidSystem::phaseIsActive(phaseIdx)) continue;
532  if (phaseIdx == oilPhaseIdx) {
533  fs.setPressure(phaseIdx, po_val);
534  } else {
535  fs.setPressure(phaseIdx, po_val - pC[phaseIdx]);
536  }
537  }
538  }
539 
540  auto& primaryVars = simulator_.model().solution(/*timeIdx*/0)[cell_idx];
541  primaryVars.assignNaive(fs);
542  }
543 
544  simulator_.model().invalidateAndUpdateIntensiveQuantities(/*timeIdx*/0);
545  }
546 
548  {
549  bool has_SWAT = false;
550  bool has_SOIL = false;
551  bool has_SGAS = false;
552  bool has_PRESSURE = false;
553  };
554 
555  FeatureFlags flagFeatures(const std::vector<std::pair<std::string, FeatureSpec>>& features)
556  {
557  FeatureFlags flags;
558 
559  for (const auto& [name, spec] : features) {
560 
561  if (spec.actual_name == "SWAT") {
562  flags.has_SWAT = true;
563  } else if (spec.actual_name == "SOIL") {
564  flags.has_SOIL = true;
565  } else if (spec.actual_name == "SGAS") {
566  flags.has_SGAS = true;
567  } else if (spec.actual_name == "PRESSURE") {
568  flags.has_PRESSURE = true;
569  }
570  }
571 
572  return flags;
573  }
574 
575 protected:
576  Simulator& simulator_;
577  std::vector<HybridNewtonConfig> configs_;
578  bool configsLoaded_;
579 };
580 
581 } // namespace Opm
582 
583 #endif // HYBRID_NEWTON_CLASS_HPP
typename Properties::Detail::GetPropImpl< TypeTag, Property >::type::type GetPropType
get the type alias defined in the property (equivalent to old macro GET_PROP_TYPE(...))
Definition: propertysystem.hh:233
void tryApplyHybridNewton()
Attempt to apply the Hybrid Newton correction at the current timestep.
Definition: HybridNewton.hpp:101
void validateConfig(bool compositionSwitchEnabled) const
Validate feature compatibility with simulator settings.
Definition: HybridNewtonConfig.cpp:82
void updateInitialGuess(ML::Tensor< Evaluation > &output, const HybridNewtonConfig &config)
Update the nonlinear solver&#39;s initial guess using ML predictions.
Definition: HybridNewton.hpp:419
ML::Tensor< Evaluation > constructInputTensor(const HybridNewtonConfig &config)
Construct the input feature tensor for the Hybrid Newton model.
Definition: HybridNewton.hpp:235
Hybrid Newton solver extension for the black-oil model.
Definition: HybridNewton.hpp:59
Structs needed for tpfalinearizer and its gpuparams struct extracted to be defined in one place that ...
Definition: blackoilbioeffectsmodules.hh:45
Scalar getScalarFeatureValue(const FeatureSpec &spec)
Retrieve and transform a scalar feature (global across the domain).
Definition: HybridNewton.hpp:288
Metadata for a single feature (input or output).
Definition: HybridNewtonConfig.hpp:119
ML::Tensor< Evaluation > constructOutputTensor(const ML::Tensor< Evaluation > &input, const HybridNewtonConfig &config)
Run the Hybrid Newton model to produce output predictions.
Definition: HybridNewton.hpp:377
bool shouldApplyHybridNewton(Scalar current_time, const HybridNewtonConfig &config) const
Check whether the Hybrid Newton method should be applied at the given time.
Definition: HybridNewton.hpp:189
Scalar getPerCellFeatureValue(const FeatureSpec &spec, int cell_index)
Retrieve and transform a per-cell feature value.
Definition: HybridNewton.hpp:319
Hierarchical collection of key/value pairs.
Definition: PropertyTree.hpp:38
Definition: HybridNewton.hpp:547
Configuration for a Hybrid Newton ML model.
Definition: HybridNewtonConfig.hpp:135