Curve Splicing and Merging
On this page
Summary
A Curve splice merges the same curve from several runs or passes into one continuous curve. The procedure is to depth-match the runs, check and treat level differences in the overlap, choose the splice zone and blend. Use it whenever a well was logged in more than one run, or when the best curve for a family exists only in pieces.
Inputs and outputs
| Item | Notes | |
|---|---|---|
| Input | Two or more runs of the same curve | Each with its own depth scale |
| Input | An overlap interval | Where the runs both have good data |
| Input | A reference run | The one whose depth is trusted |
| Output | A single merged curve on one depth scale | With no gaps in the covered interval |
| Output | The depth shift and level offset applied to each run | Recorded, not hidden |
Equations
The method has four operations. Take run A as the depth reference and run B as the run to be merged.
1. Depth shift. The shift \(s\) that best aligns B with A in the overlap \(\mathcal{O}\) is the one that maximises the correlation of the two curves:
where \(x_B(z - s)\) is the run-B curve plotted \(s\) lower on the depth scale. In practice \(s\) is searched over a few feet in steps of a fraction of the sampling step, and the correlation is taken on the overlap with a margin removed at both ends. This is done with a feature-rich curve such as gamma ray, and the result is applied to every curve of the run.
2. Level offset. After the shift, the median difference in the overlap measures the level offset between the runs:
For a resistivity use the difference of logarithms. The offset is subtracted from B only if the cause is understood.
3. Splice zone. Choose \(z_1 < z_2\) inside the overlap, in a zone that is gauge hole, away from bed boundaries and away from casing effects.
4. Blend. In the zone the output is a linear weighted mean:
so the output is \(x_A\) above \(z_1\) and the corrected \(x_B\) below \(z_2\). A hard splice is the case \(z_1 = z_2\).
Single-value calculator
No calculator: this method is a procedure, not a single equation.
Behavior
The synthetic example plants a 1.5 ft depth error and a 4 gAPI level offset in run B. The correlation search finds a shift of -1.50 ft with a correlation of 0.996, and the RMS difference in the overlap drops from 22.0 gAPI before the shift to 5.1 gAPI after it. The median level offset is +4.5 gAPI, close to the planted 4.0. The offset estimate is not exact because noise and bed edges leave a residual. The merged curve has no gaps and an RMS error against the true curve of 2.00 gAPI, the same as the noise in the data. Inside the 40 ft blend zone it is slightly lower, 1.90 gAPI, because averaging two independent noisy runs reduces the noise. The depth shift is the dominant correction: the level offset is small by comparison.
Parameter guidance
Reference run. Use the run with the best depth control, normally the one tied to the open-hole reference and run first in the interval. Shift the others to it. Do not shift the reference.
Overlap length. The overlap needs enough features to correlate. A repeat section of 100 ft or more is comfortable. Very short overlaps should be treated with caution, and a shift estimated from fewer than a handful of beds should be accepted only if it is consistent with other curves of the run.
Search window and resolution. Search a window of a few feet either side, in steps of a quarter to a half of the sampling step. A shift at the edge of the window means the search was too narrow or that the curves do not belong together.
Level offset. Two different tools can legitimately read differently (vertical resolution, borehole conditions, calibration). Apply an offset only when the two runs are the same tool type and the offset is a calibration difference. For different tool types, or where the difference is a real borehole effect, do not force agreement: pick one curve per interval. Offsets for resistivity are best handled in logarithms. A normalization applied to the whole well is a separate step.
Blend zone. Between 20 and 50 ft in a quiet zone is usual. A blend over a bed boundary smears it, and a blend that includes a washout mixes good data with bad.
See the step page for how splicing fits with aliasing, and Depth Reference and Sampling Checks for the checks to make before splicing.
Worked example
Two gamma-ray runs: run A covers 1000 to 1400 ft, run B covers 1300 to 1700 ft, and they share a 100 ft overlap. Run B was recorded 1.5 ft too deep and reads 4 gAPI high. The code estimates both, merges the runs and compares the result with the true curve, which is known because the data are synthetic:
import numpy as np
STEP = 0.5 # ft
def make_truth(z, seed=3):
"""Blocky gamma ray: beds of 2 to 12 ft, values 30 to 130 gAPI."""
rng = np.random.default_rng(seed)
gr = np.empty_like(z)
i = 0
while i < len(z):
n = int(rng.integers(4, 24))
gr[i:i + n] = rng.uniform(30, 130)
i += n
return gr
def best_shift(z_ref, x_ref, z_mov, x_mov, lo, hi, search=5.0, resolution=0.25):
"""Depth shift s (ft) that, added to the depths of the moving run, maximises correlation in [lo, hi]."""
zz = np.arange(lo, hi, STEP)
ref = np.interp(zz, z_ref, x_ref)
best = (-2.0, 0.0)
for s in np.arange(-search, search + 1e-9, resolution):
mov = np.interp(zz, z_mov + s, x_mov)
r = np.corrcoef(ref, mov)[0, 1]
if r > best[0]:
best = (r, s)
return best[1], best[0]
def blend(z_out, a, b, z1, z2):
"""a is the upper run, b the lower run: all a above z1, all b below z2, linear weight in between."""
w = np.clip((z_out - z1) / (z2 - z1), 0.0, 1.0)
return (1.0 - w) * a + w * b
# True formation response on a common depth axis
z_all = np.arange(1000.0, 1700.0 + STEP, STEP)
truth = make_truth(z_all)
rng = np.random.default_rng(11)
# Run A (upper) 1000-1400 ft, depth-matched. Run B (lower) 1300-1700 ft: recorded 1.5 ft too deep,
# reads 4 gAPI high (different tool, different hole conditions), same noise level.
za = z_all[z_all <= 1400.0]
a = truth[: len(za)] + rng.normal(0, 2.0, len(za))
zb_true = z_all[z_all >= 1300.0]
b = truth[z_all >= 1300.0] + 4.0 + rng.normal(0, 2.0, len(zb_true))
zb = zb_true + 1.5 # what the header says
ov_lo, ov_hi = 1300.0 + 5, 1400.0 - 5 # trim 5 ft off each end of the overlap
shift, r = best_shift(za, a, zb, b, ov_lo, ov_hi)
print(f"estimated depth shift for run B: {shift:+.2f} ft (correlation {r:.3f})")
zb_fixed = zb + shift
zz = np.arange(ov_lo, ov_hi, STEP)
ia, ib = np.interp(zz, za, a), np.interp(zz, zb_fixed, b)
rms_before = np.sqrt(np.mean((ia - np.interp(zz, zb, b)) ** 2))
offset = float(np.median(ib - ia))
print(f"RMS difference in overlap: {rms_before:.1f} gAPI before the shift, "
f"{np.sqrt(np.mean((ia - ib) ** 2)):.1f} after")
print(f"median level offset (B - A): {offset:+.1f} gAPI")
# Merge: shift B, remove the level offset (only because both tools are known to be the same measurement),
# blend linearly over the central 40 ft of the overlap.
z_out = np.arange(1000.0, 1700.0 + STEP, STEP)
a_out = np.interp(z_out, za, a, left=np.nan, right=np.nan)
b_out = np.interp(z_out, zb_fixed, b - offset, left=np.nan, right=np.nan)
merged = np.where(np.isnan(a_out), b_out, np.where(np.isnan(b_out), a_out, blend(z_out, a_out, b_out, 1320.0, 1360.0)))
err = merged - truth
print(f"merged samples: {len(merged)}, NaN left: {int(np.isnan(merged).sum())}")
print(f"RMS error against the true curve: {np.sqrt(np.mean(err ** 2)):.2f} gAPI "
f"(noise alone is 2.0)")
zone = (z_out >= 1320.0) & (z_out <= 1360.0)
print(f"RMS error inside the 40 ft blend zone: {np.sqrt(np.mean(err[zone] ** 2)):.2f} gAPI")
print("planted values: shift -1.5 ft, offset +4.0 gAPI")
Output
estimated depth shift for run B: -1.50 ft (correlation 0.996)
RMS difference in overlap: 22.0 gAPI before the shift, 5.1 after
median level offset (B - A): +4.5 gAPI
merged samples: 1401, NaN left: 0
RMS error against the true curve: 2.00 gAPI (noise alone is 2.0)
RMS error inside the 40 ft blend zone: 1.90 gAPI
planted values: shift -1.5 ft, offset +4.0 gAPI
Assumptions and limitations
- The runs measure the same quantity and the same formation. A change in the borehole between runs (hole enlargement, invasion) changes the curve for a real reason.
- The depth error is a constant over the overlap. Cable stretch and tension change make errors that grow with depth, and a single shift cannot fix them.
- The overlap contains enough features to correlate, and the same features in both runs.
- The two runs have the same vertical resolution, or the difference is accepted. Correlating a high and a low resolution curve biases the shift.
- A level offset is a calibration difference and not a real signal.
QC checks
- Overlay the runs in the overlap before and after the shift. Features should line up, and the correlation should be high (above about 0.9 for gamma ray in a feature-rich interval).
- The shift is not at the edge of the search window and is consistent between curves of the same run.
- The shift is small compared to the logged interval. Large shifts point to a depth reference or units mismatch rather than a depth error, and are covered on the depth checks page.
- The merged curve has no step at the splice and no repeated or missing section.
- Check the splice on a second curve (density or resistivity) that was not used to find the shift.
- The record of shifts and offsets is stored with the merged curve.
Going Deeper
Depth-matching by cross-correlation is routine, and the only subtlety is which curve to match. Gamma ray is used because every run has it and because it has features at all depths. Where the gamma ray is featureless, or the runs differ, other curves can be used, or the match is made at a few picked markers and interpolated. A depth error that varies along the hole can be handled with several shifts at different depths and an interpolated shift curve, at the risk of stretching the log. Splicing is also where the stage's other checks are put to use: a unit error or a null value in the overlap damages the correlation. For tools of different types, the better practice is often not to splice at all, but to keep both curves and choose in each interval.
References
References will be added once verified.
Python reference implementation
Python reference implementation
The Python reference implementation is available to registered users with a verified email address. Register or sign in to view it.