Skip to content

Configuration

Overview

The Config class holds every parameter that controls how a BORES simulation runs. It specifies the timer (time stepping behavior), the rock-fluid tables (relative permeability and capillary pressure models), the wells, the numerical scheme, the solvers, the preconditioners, the convergence tolerances, and all the physical constraints that keep the simulation stable and accurate.

Config is a frozen (immutable) attrs class. Once you create a Config, its fields cannot be changed in place. To modify a configuration, you create a new one using the copy() or with_updates() methods. This immutability prevents accidental modification of simulation parameters during a run and makes configurations safe to pass between functions without defensive copying.

Every simulation in BORES requires a Config. You pass it (along with a reservoir model) to bores.run() to start the simulation. The Config is the single point of control for all numerical behavior. If two simulations use different schemes, solvers, or convergence criteria, those differences are captured entirely in their respective Config objects.


Creating a Config

At minimum, a Config requires a Timer and a RockFluidTables:

import bores

rock_fluid_tables = bores.RockFluidTables(
    relative_permeability_table=bores.BrooksCoreyThreePhaseRelPermModel(
        irreducible_water_saturation=0.25,
        residual_oil_saturation_water=0.30,
        residual_oil_saturation_gas=0.15,
        residual_gas_saturation=0.05,
        water_exponent=2.5,
        oil_exponent=2.0,
        gas_exponent=2.0,
    ),
    capillary_pressure_table=bores.BrooksCoreyCapillaryPressureModel(
        irreducible_water_saturation=0.25,
        residual_oil_saturation_water=0.30,
        residual_oil_saturation_gas=0.15,
        residual_gas_saturation=0.05,
    ),
)

config = bores.Config(
    timer=bores.Timer(
        initial_step_size=bores.Time(days=1),
        maximum_step_size=bores.Time(days=10),
        minimum_step_size=bores.Time(hours=1),
        simulation_time=bores.Time(years=3),
    ),
    rock_fluid_tables=rock_fluid_tables,
)

This creates a valid configuration with all defaults. The IMPES scheme, BiCGSTAB solver with ILU preconditioning, and standard convergence tolerances are used automatically.

Adding Wells

Most simulations include wells. Pass them through the wells parameter:

wells = bores.wells_(injectors=[injector], producers=[producer])

config = bores.Config(
    timer=bores.Timer(
        initial_step_size=bores.Time(days=1),
        maximum_step_size=bores.Time(days=10),
        minimum_step_size=bores.Time(hours=1),
        simulation_time=bores.Time(years=3),
    ),
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
)

Adding Well Schedules

For simulations with time-varying well controls, use well_schedules:

config = bores.Config(
    timer=timer,
    rock_fluid_tables=rock_fluid_tables,
    well_schedules=schedules,
)

When well_schedules is provided, the simulator automatically switches well controls at the scheduled times. See the Well Scheduling page for details.

Adding Boundary Conditions

Boundary conditions (aquifer support, constant pressure boundaries, etc.) are specified through the boundary_conditions parameter:

config = bores.Config(
    timer=timer,
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    boundary_conditions=boundary_conditions,
)

All Config Parameters

Required Parameters

Parameter Type Description
timer Timer Time stepping manager (initial/max/min step sizes, simulation time)
rock_fluid_tables RockFluidTables Relative permeability and capillary pressure models

Optional Model Parameters

Parameter Type Default Description
wells Wells None Well configuration (injectors and producers)
well_schedules WellSchedules None Dynamic well control schedules
boundary_conditions BoundaryConditions None Boundary conditions (aquifers, constant pressure, etc.)
pvt_tables PVTTables None Tabulated PVT properties (alternative to correlations)
constants Constants Default Physical and conversion constants

Numerical Scheme

Parameter Type Default Description
scheme str "impes" Evolution scheme: "impes", "explicit", or "implicit"
use_pseudo_pressure bool True Use pseudo-pressure formulation for gas

See Schemes for detailed information on each evolution scheme.

Solver Configuration

Parameter Type Default Description
pressure_solver str or list "bicgstab" Solver(s) for the pressure equation
saturation_solver str or list "bicgstab" Solver(s) for the saturation equation
pressure_preconditioner str or None "ilu" Preconditioner for pressure solvers
saturation_preconditioner str or None "ilu" Preconditioner for saturation solvers
pressure_convergence_tolerance float 1e-6 Relative convergence tolerance for pressure
saturation_convergence_tolerance float 1e-4 Relative convergence tolerance for saturation
maximum_solver_iterations int 250 Maximum solver iterations per step (capped at 500)
task_pool ThreadPoolExecutor None Thread pool for concurrent solver matrix assembly

See Solvers and Preconditioners for details.

Explicit Scheme Controls

Parameter Type Default Description
saturation_cfl_threshold float 0.7 Maximum saturation CFL number (explicit scheme only)
pressure_cfl_threshold float 0.9 Maximum pressure CFL number (explicit scheme only)

Time Step Saturation Controls

Parameter Type Default Description
maximum_oil_saturation_change float 0.6 Maximum oil saturation change per step
maximum_water_saturation_change float 0.6 Maximum water saturation change per step
maximum_gas_saturation_change float 0.5 Maximum gas saturation change per step

Time Step Pressure Controls

Parameter Type Default Description
maximum_pressure_change float 1000.0 Maximum pressure change per step (psi)

Pressure Change Limits

The default maximum_pressure_change of 1000 psi is appropriate for most field-scale models. For lower-pressure reservoirs or simulations with rapid pressure transients (well shutins, gas breakthrough), you may need a tighter limit. A useful rule of thumb is to keep maximum_pressure_change below 10 to 15% of the initial reservoir pressure. For example, a shallow reservoir at 1,500 psi might use maximum_pressure_change=150.0, while a deep HPHT reservoir at 12,000 psi can use higher values like maximum_pressure_change=1200.0.

See Time Step Control for guidance on adjusting these.

Physical Controls

Parameter Type Default Description
capillary_strength_factor float 1.0 Scale factor for capillary effects (0 to 1)
disable_capillary_effects bool False Completely disable capillary pressure
disable_structural_dip bool False Disable gravity/structural dip effects
miscibility_model str "immiscible" Miscibility model: "immiscible" or "todd_longstaff"
freeze_saturation_pressure bool False Keep bubble point pressure constant

Fluid Mobility

Parameter Type Default Description
relative_mobility_range PhaseRange See below Min/max relative mobility per phase
total_compressibility_range Range (1e-24, 1e-2) Min/max total compressibility
phase_appearance_tolerance float 1e-6 Saturation below which a phase is absent

The default relative mobility ranges are:

  • Oil: \(10^{-12}\) to \(10^{6}\)
  • Water: \(10^{-12}\) to \(10^{6}\)
  • Gas: \(10^{-12}\) to \(10^{6}\)

These ranges prevent division by zero and numerical overflow in mobility calculations. You rarely need to change them.

Implicit Solver Configuration

These parameters control Newton-Raphson solvers in implicit and sequential-implicit schemes.

Parameter Type Default Description
jacobian_assembly_method str "analytical" Jacobian assembly: "analytical" or "numerical"
maximum_newton_iterations int 15 Maximum Newton-Raphson iterations per solve
newton_tolerance float 1e-6 Relative residual tolerance for Newton convergence
maximum_line_search_cuts int 4 Maximum line search bisections per Newton step
maximum_saturation_change float 0.05 Maximum per-cell saturation change per Newton iteration
newton_saturation_change_tolerance float 1e-4 Saturation change tolerance for dual convergence check
newton_stagnation_patience int 3 Consecutive non-improving iterations before stagnation
newton_stagnation_improvement_threshold float 0.01 Minimum fractional residual reduction per iteration (1% default)

Sequential Implicit Outer Iteration Configuration

These parameters control outer loop convergence for the sequential-implicit scheme.

Parameter Type Default Description
pressure_outer_convergence_tolerance float 1e-3 Relative pressure inter-iterate tolerance
saturation_outer_convergence_tolerance float 1e-2 Absolute saturation inter-iterate tolerance

Additional Numerical Controls

Parameter Type Default Description
normalize_saturations bool True Normalize saturations so Sw + So + Sg = 1.0 after each step
log_interval int 5 Log progress every N steps
output_frequency int 1 Yield a state every N steps
warn_well_anomalies bool True Warn about anomalous well flow rates

Hysteresis

Parameter Type Default Description
residual_oil_drainage_ratio_water_flood float 0.6 Oil drainage residual ratio (waterflood)
residual_oil_drainage_ratio_gas_flood float 0.6 Oil drainage residual ratio (gas flood)
residual_gas_drainage_ratio float 0.5 Gas drainage residual ratio

Output and Logging

Parameter Type Default Description
output_frequency int 1 Yield a state every N steps
log_interval int 5 Log progress every N steps
warn_well_anomalies bool True Warn about anomalous well flow rates

Parallel Assembly with Task Pools

During each timestep, the pressure and saturation solvers assemble coefficient matrices from fluid properties, transmissibilities, and well contributions. In the IMPES scheme, the pressure solver has three independent assembly stages (accumulation terms, face transmissibilities, and well contributions) and the saturation solver has two independent stages (flux contributions and well rate grids). By default, these stages run sequentially on the calling thread. For larger grids, you can run them concurrently using a thread pool.

The task_pool parameter accepts a ThreadPoolExecutor that BORES uses to submit independent assembly stages in parallel. The calling thread blocks until all submitted stages complete, so the effective assembly time approaches the duration of the slowest stage rather than their sum. This can reduce assembly time by 30 to 50% for grids with 50,000 or more cells.

BORES provides the new_task_pool() context manager as the standard way to create and manage a pool. It creates the pool, yields it for use, and shuts it down cleanly when the block exits, whether normally or due to an exception.

import bores

with bores.new_task_pool(concurrency=3) as pool:
    config = bores.Config(
        timer=timer,
        rock_fluid_tables=rock_fluid_tables,
        wells=wells,
        task_pool=pool,
    )

    run = bores.Run(model=model, config=config)
    with bores.StateStream(run, store=store) as stream:
        for state in stream:
            process(state)
# Pool shuts down cleanly here

When to Use a Task Pool

The break-even point depends on grid size. Below about 10,000 cells, the overhead of thread synchronisation and future creation exceeds the time saved by concurrent execution. The benefit becomes noticeable around 50,000 cells and clearly measurable above 200,000 cells.

Grid Size Recommendation
< 10,000 cells Leave task_pool as None (sequential is faster)
10,000 to 50,000 Marginal benefit, profile before committing
50,000 to 200,000 Noticeable benefit, 3 workers recommended
> 200,000 Clearly beneficial, assembly cost approaches solve cost

Concurrency Settings

Pass concurrency=3 for IMPES simulations. The pressure solver submits at most 3 tasks per call and the saturation solver submits at most 2, so 3 workers covers both without waste. If your machine has only 2 physical cores, use concurrency=2. Higher values provide no additional benefit because the current assembly design never submits more than 3 tasks per solver call.

Conditional Pool Based on Grid Size

If you want your code to work efficiently across different model sizes, you can conditionally create a pool:

import bores
from contextlib import nullcontext

nx, ny, nz = 50, 50, 20
cell_count = nx * ny * nz  # 50,000

if cell_count > 10_000:
    pool_context = bores.new_task_pool(concurrency=3)
else:
    pool_context = nullcontext(None)

with pool_context as pool:
    config = bores.Config(
        timer=timer,
        rock_fluid_tables=rock_fluid_tables,
        task_pool=pool,  # None for small grids, `ThreadPoolExecutor` for large
    )
    # ... run simulation

Why Threads, Not Processes

BORES uses ThreadPoolExecutor rather than ProcessPoolExecutor because the assembly functions operate on large numpy arrays that are shared by reference between threads. A process pool would need to pickle and copy those arrays into child processes on every call. For a 100x100x30 grid, this is 30 to 50 MB of serialisation overhead per timestep, which eliminates any parallelism gain. Numba JIT-compiled functions release the Python GIL during execution, so threads achieve true parallel CPU utilisation without GIL contention.

Pool Lifetime

Do not share a single pool between concurrent simulation runs. Each simulation submits up to 3 tasks per solver call, so two concurrent runs would need 6 workers to avoid queuing, and the assembly functions are not designed for that usage. Create one pool per simulation run.


Modifying a Config

Since Config is immutable, you cannot modify fields directly. Use copy() or with_updates() to create modified versions:

copy()

# Create a new config with a different scheme
implicit_config = config.copy(scheme="full-sequential-implicit")

# Multiple changes at once
tuned_config = config.copy(
    scheme="full-sequential-implicit",
    pressure_solver="gmres",
    pressure_preconditioner="amg",
    maximum_newton_iterations=20,
)

with_updates()

with_updates() works the same way as copy() but validates that all provided keys are valid Config attributes:

# This works
updated = config.with_updates(scheme="full-sequential-implicit")

# This raises AttributeError because "schemee" is not a valid field
updated = config.with_updates(schemee="full-sequential-implicit")  # AttributeError

Use with_updates() when you want protection against typos in parameter names. Use copy() when you prefer the shorter name and are confident in the parameter names.


Freeze Saturation Pressure

The freeze_saturation_pressure flag controls whether the oil bubble point pressure (Pb) is recomputed at each time step or held constant at its initial value.

# Keep Pb constant (standard black-oil assumption)
config = bores.Config(
    timer=timer,
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    freeze_saturation_pressure=True,
)

When freeze_saturation_pressure=True, the following properties are computed using the initial bubble point pressure rather than a dynamically updated value:

  • Bubble point pressure (Pb) itself
  • Solution gas-oil ratio (Rs)
  • Oil formation volume factor (Bo)
  • Oil compressibility (Co)
  • Oil viscosity (indirectly through Rs)
  • Oil density (indirectly through Rs and Bo)

This is appropriate for natural depletion and waterflooding where oil composition remains constant. Set it to False (the default) for miscible injection or any process where dissolved gas content changes significantly during the simulation.


Capillary Strength Factor

The capillary_strength_factor scales capillary pressure effects without changing the capillary pressure model itself. It ranges from 0.0 (no capillary effects) to 1.0 (full capillary effects).

# Reduce capillary effects by 50% for numerical stability
config = bores.Config(
    timer=timer,
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    capillary_strength_factor=0.5,
)

Capillary gradients can become numerically dominant in fine meshes or at sharp saturation fronts, causing oscillations or overshoot. Reducing the capillary strength factor damps these effects without removing them entirely. This is a common technique for improving convergence in difficult models while preserving the qualitative influence of capillary pressure on fluid distribution.

Setting disable_capillary_effects=True is equivalent to capillary_strength_factor=0.0 but is more explicit in intent.


Example Configurations

Simple Depletion Study

config = bores.Config(
    timer=bores.Timer(
        initial_step_size=bores.Time(days=2),
        maximum_step_size=bores.Time(days=30),
        minimum_step_size=bores.Time(days=1),
        simulation_time=bores.Time(years=10),
    ),
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    freeze_saturation_pressure=True,
)

High-Resolution Waterflood

config = bores.Config(
    timer=bores.Timer(
        initial_step_size=bores.Time(days=0.5),
        maximum_step_size=bores.Time(days=5),
        minimum_step_size=bores.Time(hours=1),
        simulation_time=bores.Time(years=5),
    ),
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    pressure_solver="gmres",
    pressure_preconditioner="amg",
    maximum_water_saturation_change=0.15,
    maximum_pressure_change=50.0,
)

Miscible Gas Injection

config = bores.Config(
    timer=bores.Timer(
        initial_step_size=bores.Time(hours=6),
        maximum_step_size=bores.Time(days=3),
        minimum_step_size=bores.Time(minutes=30),
        simulation_time=bores.Time(years=3),
    ),
    rock_fluid_tables=rock_fluid_tables,
    wells=wells,
    miscibility_model="todd_longstaff",
    freeze_saturation_pressure=False,
    maximum_pressure_change=75.0,
)