haqlLabs

haql-engine · feature guide

A fault can move the rock. It can also stop the flow.

A fault is not only a break in the layers. It can be a partial barrier to fluid moving from one side to the other. This guide explains the new fault-face multiplier, how to set it, and what the result does — and does not — mean.

new sealing or partial barrier recorded output solver equations unchanged

Feature: fault-face transmissibility barriers · Available in geology.py and run_uncertainty.py · Status: shipped

The idea, in one picture

Imagine two rooms joined by a doorway. Moving the doorway up or down changes the shape of the rooms. Putting a door in the opening changes whether air can pass between them. A geological fault can do both.

The engine already had the first part: a planar fault can shift one side of the grid vertically. That is a fault throw. The new part is the second: the same fault can now scale the flow capacity of the faces it crosses. That is a fault-face transmissibility barrier.

Throw only Structure changes

The rock layers move, but fluid crosses the fault face as easily as before.

Throw + barrier Structure and flow change

The layers move, and the fault face can be open, partly restrictive or sealed.

This matters because fault sealing is one of the largest sources of disagreement in a production forecast. A fault that is open in one model and sealed in another can split a field into compartments and change pressure support, well rates and recovery.

The barrier is an input assumption, not a hidden geological verdict. The engine records the number so the assumption can be reviewed.

What you set

Add transmissibility_multiplier to a fault in the uncertainty input. The existing throw fields still work exactly as before.

"structure": {
  "enabled": true,
  "sigma_ft": 0.0,
  "faults": [
    {
      "axis": "x",
      "position_ft": 3000.0,
      "throw_ft": 12.0,
      "side": "positive",
      "transmissibility_multiplier": 0.0
    }
  ]
}
FieldMeaning
axisThe fault plane is normal to x or y. Only these two orientations are supported.
position_ftThe fault position in feet. The barrier lands on the grid face between the cell centres on either side.
throw_ftVertical displacement of the selected side. Set it to 0 if the fault should only affect flow.
sideWhich block moves for the throw. It does not change which faces are barriers.
transmissibility_multiplierFlow factor on the fault face. 0 is sealed, 1 is open, and values between are partial restrictions.

The aliases transmissibility, transmissibility_factor and barrier_multiplier are accepted for the last field. Use only one of them in a fault object.

If you leave the field out, the fault is open (1.0). Existing inputs therefore keep their previous behaviour.

Set structure.enabled to true. It defaults to true when faults are present; a disabled structure ignores the faults as well as the structural shift.

What the number means

The multiplier scales the original transmissibility of the crossed grid face. Transmissibility is the face's capacity to carry flow for a given pressure difference.

MultiplierEffectPlain-language reading
1.0No changeThe fault is open; only the throw applies.
0.5Half the original transmissibilityThe fault is restrictive, not sealed.
0.1One tenth of the original transmissibilityA strong barrier. This is not “90% sealed” in a geological sense; it is a 10% flow factor.
0.0No cross-fault flowThe face is closed in the numerical model.

The barrier acts on the TPFA face between the two cells that straddle the fault. It does not change the permeability of either cell, the well completions or the porosity. Multiple faults on the same face combine multiplicatively, so a second barrier can only reduce the factor further.

On the SPE1 grid: an x-normal fault at 3,000 ft crosses the x-face between the third and fourth cell columns. In the 10 × 10 × 3 grid that is 30 faces — one for each y cell in each layer. The output reports the count rather than asking you to count it by hand.

Sampling the seal

A single fault multiplier is a single assumption. When the seal strength is uncertain, sample one global factor per realization:

"scalars": {
  "fault_transmissibility_multiplier": {
    "distribution": "uniform",
    "min": 0.0,
    "max": 1.0
  }
}

The effective multiplier is:

fault.transmissibility_multiplier × scalar.fault_transmissibility_multiplier

That means a fault can have a different baseline from another fault, while one sampled scenario factor moves all faults together. The sampled scalar is stored in the realization's scalars object, and the effective face factors are applied before the solver starts.

Why one global factor? It is simple to audit and avoids pretending that every fault has an independent, calibrated seal distribution. If a study needs per-fault uncertainty, use separate fault baselines or separate scenario configurations and label the assumption.

What you get back

The result is recorded in three places, so a reviewer can move from the assumption to the numerical effect:

OutputWhat it contains
realization["structure"]["faults"]The normalized fault — axis, position, throw, side and the baseline multiplier.
realization["summary"]["fault_barrier"]Fault count, affected face count, minimum multiplier, global factor and per-fault detail.
grid.face_multiplierOne applied factor for every TPFA face in the realization. This is the array the solver actually sees.
run["fault_summary"]The same summary in a single run_spe1.py result, alongside the usual material-balance and provenance blocks.

The run's provenance configuration also records the fault block and the scalar override, so changing the seal assumption changes the content-addressed configuration identity.

How it is checked

The feature is tested at several levels:

  • Face mapping. A fault at a known position must affect only the expected grid faces. The test checks that an SPE1 x-fault at 3,000 ft changes 30 faces and leaves the other 710 unchanged.
  • Zero flow. A two-cell problem with a zero-multiplier face produces zero cross-cell residual flux, while the same problem with an open face produces a nonzero flux.
  • End-to-end run. A short SPE1 realization with a fault and a sampled multiplier completes, closes material balance and reports the expected effective multiplier.
  • Configuration validation. Negative multipliers, values above one, duplicate aliases and out-of-range sampled scalars are rejected before a run starts.

These checks prove that the multiplier is applied where it claims to be applied and that a sealed face does not leak in the numerical flux. They do not prove that a particular geological fault should be sealed in the real field. That remains an interpretation and must be stated as one.

How to try it

The checked-in example already includes a fault with a baseline multiplier of 0.5 and a sampled global factor between 0.2 and 1.0.

cd engine
python3 run_uncertainty.py \
  --input uncertainty_input.example.json \
  --out uncertainty_out --dry-run

python3 run_spe1.py \
  --realization uncertainty_out/realizations/real-0000.json \
  --T 2 --json fault_run.json --quiet

python3 -c "import json; print(json.load(open('fault_run.json'))['fault_summary'])"

The dry run is enough to inspect the generated fault and its summary. The short solver run shows the same block in the run artifact and confirms that the case still closes material balance.

Limits you should state

  • It is an input barrier, not a seal calculation. The engine does not compute shale gouge, clay smear, juxtaposition, capillary entry pressure or geomechanical seal capacity.
  • The fault is vertical and planar. It is normal to x or y on the regular SPE1-style Cartesian grid. There is no corner-point fault surface, relay ramp or listric geometry.
  • The grid must be regular in x and y. The fault-to-face mapping uses the same axis coordinates as the existing structural shift.
  • It changes faces, not wells. A well completion on either side is untouched. The barrier does not create a new well or move an existing one.
  • It is not calibrated. A multiplier is a user assumption. The forecast is conditional on that assumption, not a prediction of the real fault's sealing behaviour.
  • It does not add fault permeability. There is no separate fault-zone flow path or cross-fault capillary model.

Do not present the multiplier as a measured seal. Present it as a transparent scenario assumption with a recorded value, and show the range of multipliers the study tested.

Questions

Is a multiplier of 0.1 the same as 90% sealing?

No. It means the fault face keeps 10% of its original transmissibility in this numerical model. “Sealing” is a geological interpretation; the engine is recording a flow factor.

Does the barrier change the fault throw?

No. The throw and the barrier are separate fields on the same fault object. Set throw_ft to 0 for a flow-only fault, or set the multiplier to 1.0 for a structure-only fault.

Can I give each fault its own multiplier?

Yes. Each fault has its own baseline multiplier. The sampled fault_transmissibility_multiplier is a single global factor applied to all faults, so per-fault uncertainty needs separate baselines or separate scenario configurations.

What if the fault position is outside the grid?

The fault is still recorded in the realization, but it affects no faces. The summary reports zero affected faces, which is a useful signal that the position or the grid is wrong.

Does this make the solver unstable?

A zero multiplier removes a connection, which can isolate compartments or make a producer and injector independent. The solver still runs, but the case should be reviewed for pressure and material-balance behaviour like any other scenario. The regression suite includes a short fault run and a separate zero-flux check.

Where is the engineering detail?

The schema, defaults and limitations are in the technical reference. The source-level description is in engine/GEOLOGICAL_UNCERTAINTY.md.