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 <string>
33 #include <vector>
34 
35 namespace Opm {
36 
57 template <typename TypeTag>
59 {
60 protected:
67 
68  enum { numPhases = FluidSystem::numPhases };
69 
70  enum { gasPhaseIdx = FluidSystem::gasPhaseIdx };
71  enum { oilPhaseIdx = FluidSystem::oilPhaseIdx };
72  enum { waterPhaseIdx = FluidSystem::waterPhaseIdx };
73 
74  static constexpr bool compositionSwitchEnabled = Indices::compositionSwitchIdx >= 0;
75 
76 public:
77  explicit BlackOilHybridNewton(Simulator& simulator)
78  : simulator_(simulator)
79  , configsLoaded_(false)
80  {}
81 
100  {
101  // Check if flag activated
102  if (!Parameters::Get<Parameters::UseHybridNewton>())
103  return;
104 
105  validateFluidSystem();
106 
107  if (!configsLoaded_) {
108  std::string config_file = Parameters::Get<Parameters::HybridNewtonConfigFile>();
109  PropertyTree pt(config_file);
110  for (const auto& model_key : pt.get_child_keys()) {
111  HybridNewtonConfig config(pt.get_child(model_key));
112  config.validateConfig(compositionSwitchEnabled);
113  configs_.push_back(std::move(config));
114  }
115  configsLoaded_ = true;
116  }
117 
118  Scalar current_time = simulator_.time();
119  // Find and apply all models that should run at this time
120  for (const auto& config : configs_) {
121  if (shouldApplyHybridNewton(current_time, config)) {
122  runHybridNewton(config);
123  }
124  }
125  }
126 
127 private:
142  void runHybridNewton(const HybridNewtonConfig& config)
143  {
144  auto input = constructInputTensor(config);
145  auto output = constructOutputTensor(input, config);
146  updateInitialGuess(output, config);
147  }
148 
149 protected:
150  void validateFluidSystem()
151  {
152  const auto& eclState = simulator_.vanguard().eclState();
153  const auto& phases = eclState.runspec().phases();
154 
155  bool hasWater = phases.active(Phase::WATER);
156  bool hasGas = phases.active(Phase::GAS);
157  bool hasOil = phases.active(Phase::OIL);
158  bool hasSolvent = phases.active(Phase::SOLVENT);
159 
160  // Check for three-phase black oil system
161  if (!(hasWater && hasOil && hasGas && !hasSolvent)) {
162  OPM_THROW(std::runtime_error,
163  "HybridNewton: Unsupported fluid system. Only three-phase black oil is supported.");
164  }
165  }
166 
187  bool shouldApplyHybridNewton(Scalar current_time, const HybridNewtonConfig& config) const
188  {
189  if (config.apply_times.size() == 1) {
190  // Apply exactly at one point in time (with optional tolerance)
191  constexpr Scalar tolerance = 1e-6;
192  bool apply = std::abs(current_time - config.apply_times[0]) < tolerance;
193  return apply;
194  }
195 
196  if (config.apply_times.size() == 2) {
197  Scalar start_time = config.apply_times[0];
198  Scalar end_time = config.apply_times[1];
199  bool apply = (current_time >= start_time) && (current_time <= end_time);
200  return apply;
201  }
202 
203  // If apply_times has an unexpected number of entries
204  return false;
205  }
206 
207 
232  ML::Tensor<Evaluation>
234  {
235  const auto& features = config.input_features;
236 
237  // compute total length directly
238  std::size_t input_tensor_length = 0;
239  for (const auto& feature : features) {
240  const FeatureSpec& spec = feature.second;
241  if (spec.actual_name == "TIMESTEP") {
242  input_tensor_length += 1;
243  } else {
244  input_tensor_length += config.n_cells;
245  }
246  }
247 
248  ML::Tensor<Evaluation> input(input_tensor_length);
249  std::size_t offset = 0;
250 
251  // fill in exact feature order from config
252  for (const auto& feature: features) {
253  const FeatureSpec& spec = feature.second;
254 
255  if (spec.actual_name == "TIMESTEP") {
256  auto value = static_cast<Evaluation>(getScalarFeatureValue(spec));
257  input(offset++) = value;
258  } else {
259  for (std::size_t i = 0; i < config.n_cells; ++i) {
260  auto value = static_cast<Evaluation>(
261  getPerCellFeatureValue(spec, config.cell_indices[i])
262  );
263  input(offset + i) = value;
264  }
265  offset += config.n_cells;
266  }
267  }
268 
269  return input;
270  }
271 
286  Scalar getScalarFeatureValue(const FeatureSpec& spec)
287  {
288  Scalar value = 0.0;
289 
290  if (spec.actual_name == "TIMESTEP") {
291  value = simulator_.timeStepSize();
292  } else {
293  OPM_THROW(std::runtime_error, "Unknown scalar feature: " + spec.actual_name);
294  }
295 
296  value = spec.transform.apply(value);
297  return spec.scaler.scale(value);
298  }
299 
317  Scalar getPerCellFeatureValue(const FeatureSpec& spec, int cell_index)
318  {
319  const auto& intQuants = simulator_.model().intensiveQuantities(cell_index, 0);
320  const auto& fs = intQuants.fluidState();
321  const auto& unitSyst = simulator_.vanguard().schedule().getUnits();
322 
323  Scalar value = 0.0;
324 
325  if (spec.actual_name == "PRESSURE") {
326  value = getValue(fs.pressure(oilPhaseIdx));
327  value = unitSyst.from_si(UnitSystem::measure::pressure, value);
328  } else if (spec.actual_name == "SWAT") {
329  value = getValue(fs.saturation(waterPhaseIdx));
330  } else if (spec.actual_name == "SGAS") {
331  value = getValue(fs.saturation(gasPhaseIdx));
332  } else if (spec.actual_name == "SOIL") {
333  value = getValue(fs.saturation(oilPhaseIdx));
334  } else if (spec.actual_name == "RS") {
335  value = getValue(fs.Rs());
336  value = unitSyst.from_si(UnitSystem::measure::gas_oil_ratio, value);
337  } else if (spec.actual_name == "RV") {
338  value = getValue(fs.Rv());
339  value = unitSyst.from_si(UnitSystem::measure::oil_gas_ratio, value);
340  } else if (spec.actual_name == "PERMX") {
341  const auto& eclState = simulator_.vanguard().eclState();
342  const auto& fp = eclState.fieldProps();
343  auto permX = fp.get_double("PERMX");
344  value = permX[cell_index];
345  value = unitSyst.from_si(UnitSystem::measure::permeability, value);
346  } else {
347  OPM_THROW(std::runtime_error, "Unknown per-cell feature: " + spec.actual_name);
348  }
349 
350  Scalar transformed = spec.transform.apply(value);
351  Scalar scaled = spec.scaler.scale(transformed);
352  return scaled;
353  }
354 
355 
374  ML::Tensor<Evaluation>
375  constructOutputTensor(const ML::Tensor<Evaluation>& input,
376  const HybridNewtonConfig& config)
377  {
378  const auto& features = config.output_features;
379  const int n_features = features.size();
380 
381  ML::NNModel<Evaluation> model;
382  model.loadModel(config.model_path);
383 
384  ML::Tensor<Evaluation> output(1, config.n_cells * n_features);
385  model.apply(input, output);
386 
387  return output;
388  }
389 
417  void updateInitialGuess(ML::Tensor<Evaluation>& output,
418  const HybridNewtonConfig& config)
419  {
420  const auto& features = config.output_features;
421 
422  FeatureFlags flags = flagFeatures(features);
423 
424  const auto& unitSyst = simulator_.vanguard().schedule().getUnits();
425 
426  for (std::size_t i = 0; i < config.n_cells; ++i) {
427  const int cell_idx = config.cell_indices[i];
428  const auto& intQuants = simulator_.model().intensiveQuantities(cell_idx, /*timeIdx*/0);
429  auto fs = intQuants.fluidState();
430 
431  int feature_idx = 0;
432 
433  // Temp variables per cell
434  Scalar sw_val = -1.0;
435  Scalar so_val = -1.0;
436  Scalar sg_val = -1.0;
437  Scalar po_val = -1.0;
438 
439  for (const auto& [name, spec] : features) {
440 
441  auto scaled_value = getValue(output(feature_idx * config.n_cells + i));
442 
443  // Inverse scaling
444  Scalar raw_value = spec.scaler.unscale(scaled_value);
445 
446  // Inverse transform
447  raw_value = spec.transform.applyInverse(raw_value);
448 
449  if (spec.is_delta) {
450  if (spec.actual_name == "PRESSURE") {
451  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::pressure, getValue(fs.pressure(oilPhaseIdx)));
452  } else if (spec.actual_name == "SWAT") {
453  raw_value += getValue(fs.saturation(waterPhaseIdx));
454  } else if (spec.actual_name == "SOIL") {
455  raw_value += getValue(fs.saturation(oilPhaseIdx));
456  } else if (spec.actual_name == "SGAS") {
457  raw_value += getValue(fs.saturation(gasPhaseIdx));
458  } else if (spec.actual_name == "RS") {
459  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::gas_oil_ratio, getValue(fs.Rs()));
460  } else if (spec.actual_name == "RV") {
461  raw_value = raw_value + unitSyst.from_si(UnitSystem::measure::oil_gas_ratio, getValue(fs.Rv()));
462  } else {
463  OPM_THROW(std::runtime_error, "Unknown delta feature: " + name);
464  }
465  }
466 
467  if (spec.actual_name == "PRESSURE") {
468  po_val = unitSyst.to_si(UnitSystem::measure::pressure, raw_value);
469  } else if (spec.actual_name == "SWAT") {
470  sw_val = raw_value;
471  } else if (spec.actual_name == "SOIL") {
472  so_val = raw_value;
473  } else if (spec.actual_name == "SGAS") {
474  sg_val = raw_value;
475  } else if (spec.actual_name == "RS") {
476  if constexpr (compositionSwitchEnabled) {
477  raw_value = unitSyst.to_si(UnitSystem::measure::gas_oil_ratio, raw_value);
478  fs.setRs(raw_value);
479  }
480  } else if (spec.actual_name == "RV") {
481  if constexpr (compositionSwitchEnabled) {
482  raw_value = unitSyst.to_si(UnitSystem::measure::oil_gas_ratio, raw_value);
483  fs.setRv(raw_value);
484  }
485  } else {
486  OPM_THROW(std::runtime_error, "Unknown output feature: " + name);
487  }
488 
489  ++feature_idx;
490  }
491 
492  int sat_count = static_cast<int>(flags.has_SWAT) +
493  static_cast<int>(flags.has_SOIL) +
494  static_cast<int>(flags.has_SGAS);
495 
496  if (sat_count >= 2) {
497  Scalar sw = sw_val;
498  Scalar so = so_val;
499  Scalar sg = sg_val;
500 
501  if (!flags.has_SWAT) {
502  sw = 1.0 - so - sg;
503  } else if (!flags.has_SOIL) {
504  so = 1.0 - sw - sg;
505  } else if (!flags.has_SGAS) {
506  sg = 1.0 - sw - so;
507  }
508 
509  sw = max(0.0, min(sw, 1.0));
510  so = max(0.0, min(so, 1.0));
511  sg = max(0.0, min(sg, 1.0));
512 
513  Scalar sum = sw + so + sg;
514  if (sum <= 1e-12) {
515  OPM_THROW(std::runtime_error, "Saturation sum is zero in cell " + std::to_string(cell_idx));
516  }
517 
518  fs.setSaturation(waterPhaseIdx, sw / sum);
519  fs.setSaturation(oilPhaseIdx, so / sum);
520  fs.setSaturation(gasPhaseIdx, sg / sum);
521  }
522 
523  if (flags.has_PRESSURE) {
524  std::array<Evaluation, numPhases> pC;
525  const auto& materialParams = simulator_.problem().materialLawParams(cell_idx);
526  MaterialLaw::capillaryPressures(pC, materialParams, fs);
527 
528  for (unsigned phaseIdx = 0; phaseIdx < numPhases; ++phaseIdx) {
529  if (!FluidSystem::phaseIsActive(phaseIdx)) continue;
530  if (phaseIdx == oilPhaseIdx) {
531  fs.setPressure(phaseIdx, po_val);
532  } else {
533  fs.setPressure(phaseIdx, po_val - pC[phaseIdx]);
534  }
535  }
536  }
537 
538  auto& primaryVars = simulator_.model().solution(/*timeIdx*/0)[cell_idx];
539  primaryVars.assignNaive(fs);
540  }
541 
542  simulator_.model().invalidateAndUpdateIntensiveQuantities(/*timeIdx*/0);
543  }
544 
546  {
547  bool has_SWAT = false;
548  bool has_SOIL = false;
549  bool has_SGAS = false;
550  bool has_PRESSURE = false;
551  };
552 
553  FeatureFlags flagFeatures(const std::vector<std::pair<std::string, FeatureSpec>>& features)
554  {
555  FeatureFlags flags;
556 
557  for (const auto& [name, spec] : features) {
558 
559  if (spec.actual_name == "SWAT") {
560  flags.has_SWAT = true;
561  } else if (spec.actual_name == "SOIL") {
562  flags.has_SOIL = true;
563  } else if (spec.actual_name == "SGAS") {
564  flags.has_SGAS = true;
565  } else if (spec.actual_name == "PRESSURE") {
566  flags.has_PRESSURE = true;
567  }
568  }
569 
570  return flags;
571  }
572 
573 protected:
574  Simulator& simulator_;
575  std::vector<HybridNewtonConfig> configs_;
576  bool configsLoaded_;
577 };
578 
579 } // namespace Opm
580 
581 #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:99
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:417
ML::Tensor< Evaluation > constructInputTensor(const HybridNewtonConfig &config)
Construct the input feature tensor for the Hybrid Newton model.
Definition: HybridNewton.hpp:233
Hybrid Newton solver extension for the black-oil model.
Definition: HybridNewton.hpp:58
This file contains a set of helper functions used by VFPProd / VFPInj.
Definition: blackoilbioeffectsmodules.hh:45
Scalar getScalarFeatureValue(const FeatureSpec &spec)
Retrieve and transform a scalar feature (global across the domain).
Definition: HybridNewton.hpp:286
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:375
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:187
Scalar getPerCellFeatureValue(const FeatureSpec &spec, int cell_index)
Retrieve and transform a per-cell feature value.
Definition: HybridNewton.hpp:317
Hierarchical collection of key/value pairs.
Definition: PropertyTree.hpp:38
Definition: HybridNewton.hpp:545
Configuration for a Hybrid Newton ML model.
Definition: HybridNewtonConfig.hpp:135