examples.well_path

Well paths: a well completed in many cells rather than one.

well_path walks a polyline through the grid and returns the cells it traverses, their well indices (peaceman_WI, scaled by how much of each cell is actually traversed), and w, the resulting split of the well's rate. Several completions then act as a single well simply by being several wells: ResSim.assemble_wells superimposes them.

Here a point injector in the SW corner is replaced by a "horizontal" one along the whole west edge, at the same total rate, and the difference in sweep is the point of the exercise.

The rate split, though, is a choice, and the two modes of control make it differently:

  • under rate control, w apportions the rate in proportion to the well index -- a static allocation, exact only if the completions see equal pressure;
  • under BHP control, they instead share a single $ p_\mathrm{bh} $, and each flows whatever its own drawdown implies -- so the allocation is solved for, and follows the pressure field as it evolves.

This is incompressible (ct = 0), so the BHP-controlled path must still inject exactly what the producer takes -- it only gets to choose where along itself. Note also that a BHP well anchors the otherwise pure-Neumann pressure equation.

In the figures:

  • "sweep": the point injector drives one front out of the corner, symmetric about the diagonal (the mean saturation is 0.66 in both the NW and the SE quadrant). The path drives a broad one off the whole west edge, and thereby breaks that symmetry: it floods the NW hard (0.87) at the expense of the SE (0.33), and in fact sweeps less of the field overall (83% against 90% of cells contacted). Longer is not automatically better -- which is the sort of thing one drills a path to find out. The well markers trace the path.
  • "allocation" (left): the static split w is uniform here, this path crossing each cell of the west column fully -- it knows only the geometry. The BHP-solved split is not: the toe of the well, being nearer the producer, takes some 45% more than the heel. That ratio is set mostly by the geometry too, and so barely drifts as the flood develops.
  • "allocation" (right): oil saturation at the producer. The path breaks through at step 21, against the point injector's 27, and its water cut then climbs faster -- the near half of the reservoir having been flooded preferentially.
Well path -- sweep
Well path -- sweep
Well path -- allocation
Well path -- allocation
  1"""Well *paths*: a well completed in many cells rather than one.
  2
  3`well_path` walks a polyline through the grid and returns the cells it
  4traverses, their well indices (`peaceman_WI`, scaled by how much of each
  5cell is actually traversed), and `w`, the resulting split of the well's rate.
  6Several completions then act as a single well simply by *being* several wells:
  7`ResSim.assemble_wells` superimposes them.
  8
  9Here a point injector in the SW corner is replaced by a "horizontal" one along
 10the whole west edge, at the same total rate, and the difference in sweep is the
 11point of the exercise.
 12
 13The rate split, though, is a choice, and the two modes of control make it
 14differently:
 15
 16- under **rate** control, `w` apportions the rate in proportion to the well
 17  index -- a static allocation, exact only if the completions see equal
 18  pressure;
 19- under **BHP** control, they instead share a single $ p_\\mathrm{bh} $, and each
 20  flows whatever its own drawdown implies -- so the allocation is *solved for*,
 21  and follows the pressure field as it evolves.
 22
 23This is incompressible (`ct = 0`), so the BHP-controlled path must still inject
 24exactly what the producer takes -- it only gets to choose *where* along itself.
 25Note also that a BHP well anchors the otherwise pure-Neumann pressure equation.
 26
 27In the figures:
 28
 29- "sweep": the point injector drives one front out of the corner, symmetric
 30  about the diagonal (the mean saturation is 0.66 in *both* the NW and the SE
 31  quadrant). The path drives a broad one off the whole west edge, and thereby
 32  breaks that symmetry: it floods the NW hard (0.87) at the expense of the SE
 33  (0.33), and in fact sweeps *less* of the field overall (83% against 90% of
 34  cells contacted). Longer is not automatically better -- which is the sort of
 35  thing one drills a path to find out. The well markers trace the path.
 36- "allocation" (left): the static split `w` is uniform here, this path crossing
 37  each cell of the west column fully -- it knows only the geometry. The
 38  BHP-solved split is not: the toe of the well, being nearer the producer, takes
 39  some 45% more than the heel. That ratio is set mostly by the geometry too, and
 40  so barely drifts as the flood develops.
 41- "allocation" (right): oil saturation at the producer. The path breaks through
 42  at step 21, against the point injector's 27, and its water cut then climbs
 43  faster -- the near half of the reservoir having been flooded preferentially.
 44"""
 45
 46from mpl_tools.place import freshfig
 47import numpy as np
 48
 49from TPFA_ResSim import ResSim, well_path
 50from TPFA_ResSim.plotting import show
 51
 52## Setup
 53rw = 1e-3
 54nSteps = 28
 55dt = .7/nSteps
 56tt = dt*(1 + np.arange(nSteps))
 57
 58def waterflood(injector):
 59    """A unit square, producing from the NE corner at a rate of 1.
 60
 61    The `injector` is a well record; ref `ResSim.wells`, which is also what
 62    turns its `path` into completions -- there being nothing further to do.
 63    """
 64    model = ResSim(Lx=1, Ly=1, Nx=64, Ny=64,
 65                   wells=[injector, dict(name="Prd", xy=[1, 1], rate=-1)])
 66    SS, PP = model.sim(dt, nSteps, model.swc*np.ones(model.Nxy), pbar=False)
 67    return model, SS
 68
 69# The path, and its discretization into completions
 70proto = ResSim(Lx=1, Ly=1, Nx=64, Ny=64)
 71xy, WI, w = well_path(proto, [[0, 0], [0, 1]], rw)
 72assert len(xy) == proto.Ny, "The west edge is one cell wide, and Ny cells long."
 73
 74## Simulate: a point injector, the same as a path, and the path on BHP.
 75point, SS_point = waterflood(dict(name="Inj", xy=[0, 0], rate=1))
 76path , SS_path  = waterflood(dict(name="Inj", path=[[0, 0], [0, 1]], rate=1, rw=rw))
 77onbhp, SS_onbhp = waterflood(dict(name="Inj", path=[[0, 0], [0, 1]], bhp=3., rw=rw))
 78
 79# The completions of the injector, as grouped by `wells`
 80inj = path.wells.group == 0
 81assert inj.sum() == len(xy) and np.allclose(path.wells.WI[inj], WI)
 82
 83# Incompressible => whatever the control, the injection must match the production
 84for model in [path, onbhp]:
 85    assert np.allclose(model.wells.actual_rates[inj].sum(axis=0), 1)
 86    assert np.allclose(model.wells.rates_by_well, [[1], [-1]])   # (nWell, nSteps)
 87
 88## Plot: the resulting sweeps
 89fig, axs = freshfig("Well path -- sweep", ncols=2, sharex=True, sharey=True,
 90                    figsize=(9, 4))
 91for ax, (name, model, SS) in zip(axs, [("Point injector", point, SS_point),
 92                                       ("Path injector", path, SS_path)]):
 93    model.plt_field(ax, SS[-1], "oil", finalize=False, colorbar=False,
 94                    title=f"{name}, t = {dt*nSteps:.2f}", wells=dict(size=.3))
 95fig.tight_layout()
 96
 97## Plot: how the rate gets allocated along the path, and what is produced
 98fig, (ax1, ax2) = freshfig("Well path -- allocation", ncols=2, figsize=(10, 4))
 99
100yy = xy[:, 1]
101ax1.plot(yy, path.wells.actual_rates[inj, -1], label="Rate-controlled: $w \\propto WI$")
102for k, ls in [(0, ":"), (nSteps - 1, "-")]:
103    ax1.plot(yy, onbhp.wells.actual_rates[inj, k], ls, c="C1",
104             label=f"BHP-controlled, t = {tt[k]:.2f}")
105ax1.set(title="Injection rate per completion", xlabel="y (along the path)",
106        ylabel="q", ylim=0)
107ax1.legend(fontsize="small")
108
109prd = [point.xy2ind(*point.wells.xy[-1])]
110for name, SS in [("Point", SS_point), ("Path", SS_path), ("Path, on BHP", SS_onbhp)]:
111    ax2.plot(tt, 1 - SS[1:, prd[0]], label=name)
112ax2.set(title="Oil saturation at the producer", xlabel="Time", ylabel="1 - s")
113ax2.legend()
114fig.tight_layout()
115
116# Regression values, checked by `tests/test_examples.py`.
117__digest__ = dict(alloc_static = w,
118                  alloc_bhp    = onbhp.wells.actual_rates[inj, -1],
119                  sat_point    = SS_point[-1, ::600],
120                  sat_path     = SS_path[-1, ::600])
121
122if __name__ == "__main__":
123    show()