Skip to content

User guide

Cases and results

Keep the Julia case and result when you need geometry, model parameters, plots or gradients. Convert the result when working with NumPy or Python plotting:

import jutuldarcy as jd

case = jd.setup_case_from_data_file("MODEL.DATA")
result = jd.simulate_reservoir(case)
output = jd.convert_to_pydict(result, case, units="field")
Converted entry Contents
FIELD Summary names such as FOPR, mapped to NumPy arrays
WELLS Well names, each mapped to a dictionary of summary arrays
DAYS Output times in days
STATES One dictionary of reservoir arrays per output time

units accepts "si", "metric", "field" or "lab" for summaries. State arrays retain SI units. simulate_data_file(..., convert=True) combines setup, simulation and conversion. Its native result retains the case; for simulate_reservoir results, pass the case when converting separately.

To reuse a simulator, select the backend when constructing it:

simulator, config = jd.setup_reservoir_simulator(case, mode="ka")
result = jd.simulate_reservoir(case, simulator=simulator, config=config)

See the Julia high-level API for timestep, solver and output options.

Python and Julia values

  • Wrapper inputs accept lists, tuples, dictionaries, NumPy arrays and Paths. Numerical arrays are copied into Julia and retain their shape. Multiphase arrays normally have shape (phases, cells).
  • Cell indices, well coordinates and regions are one-based, as in Julia. Indexing JuliaCall arrays from Python and converted NumPy arrays is zero-based: result.states[0] is the first reporting step.
  • Dictionary keys become Julia Symbols recursively. Use jd.to_julia(data, symbols=False) for string-keyed dictionaries. DATA/GRDECL parsing wrappers handle this automatically.
  • Models, domains, wells and cases remain Julia objects. jd.to_python(value) recursively copies Julia dictionaries and arrays into Python/NumPy.
state = jd.to_julia({"Pressure": [1e7, 1e7, 1e7]})
pressure = state[jd.jl.Symbol("Pressure")]
python_state = jd.to_python(state)

Functions without a wrapper are available through jd.jl.JutulDarcy, jd.jl.Jutul, jd.jl.GeoEnergyIO and jd.jl.MultiComponentFlash. Direct calls may require explicit conversions. Access names ending in ! with getattr:

getattr(jd.jl.Jutul, "replace_variables!")(model, PhaseMassDensities=density)
# Wrapped equivalent:
jd.replace_variables(model, PhaseMassDensities=density)

After changing property definitions, recreate any separately stored parameters with jd.setup_parameters(model). See JuliaCall conversions for direct interoperation.

CPU and GPU execution

Selection Execution
Default, mode="ka" or mode="ka_cpu" KernelAbstractions CPU kernels
mode="ka_cpu_static" CPU kernels with static scheduling
mode="default" Traditional CPU simulator; use for adjoints
gpu=True or mode="gpu" First activated, functional GPU: CUDA, then AMDGPU
gpu="cuda" or mode="ka_cuda" CUDA
gpu="amd" or mode="ka_amd" AMDGPU

GPU packages are optional. Activate them explicitly before requesting a GPU:

if jd.activate_cuda():  # Or jd.activate_amd(). Installs the Julia package if absent.
    result = jd.simulate_reservoir(case, gpu=True)

jd.available_backends() lists activated, functional GPUs. An unavailable requested GPU raises an error. jd.remove_cuda() / jd.remove_amd() remove their requirements; restart Python to unload Julia modules. Custom Julia devices can be passed as ka_backend with mode="ka". See Julia GPU support for hardware and model restrictions.

Plotting

For static images on servers or in CI, install CairoMakie once:

jd.install_plotting("CairoMakie")
figure = jd.plot_reservoir(case.model, result, display=False, key="Pressure")
jd.jl.CairoMakie.save("pressure.png", figure)

For interactive windows, use jd.install_plotting() to install GLMakie. This requires an OpenGL-capable display. jd.plot_reservoir(case.model, result) returns immediately in a REPL or running asyncio loop. Use block=True in a script to keep the window open until closed. activate_plotting(backend) loads an already installed backend. See the executed plotting example and Julia plotting guide.

Gradients

An objective has signature objective(model, state, dt, step, forces). Use Julia arithmetic and reductions inside Python objectives to preserve automatic differentiation values; converting intermediates to float or NumPy loses derivatives. The coupled state includes reservoir and wells, whereas result.states contains only reservoir states.

With output_substates=True, result.states and result.time also include internal solver steps. Pass the full result to evaluate_objective so its time integration matches the adjoint.

Use mode="default", output_substates=True when reusing a forward result for reservoir_sensitivities. convert=True returns a dictionary of NumPy gradients, including "porosity" and "permeability". The gradient example demonstrates the complete workflow.

optimization_config and setup_reservoir_parameter_optimization expose Julia optimization callbacks; access F! with getattr(problem, "F!"). See the Julia adjoint guide for objectives, scaling and optimization algorithms.

Upgrading from 1.2

  • Python 3.10 and Julia 1.11 or newer are required. JuliaPkg now selects the registered JutulDarcy 0.4 series, Jutul 0.5 and MultiComponentFlash 2.
  • Simulations default to KernelAbstractions on the CPU. Pass mode="default" to use the traditional simulator, including for adjoint workflows.
  • Plotting returns a figure immediately. Scripts that should wait for a window to close must pass block=True. Backend loading errors retain their Julia diagnostics; install the backend before activating it.
  • Numerical input arrays are copied into Julia. Rebuild or update the Julia model explicitly after changing Python input arrays.

Configure Julia

Set configuration before importing jutuldarcy or juliacall:

import os

os.environ["PYTHON_JULIAPKG_EXE"] = "/absolute/path/to/julia"  # Optional
os.environ["PYTHON_JULIACALL_THREADS"] = "4"
import jutuldarcy as jd

PYTHON_JULIAPKG_PROJECT selects a Julia project by absolute directory path; JuliaPkg manages dependencies there. Import jutuldarcy first so it can enable Julia's signal handling. See JuliaCall configuration.