CamPetro

Log-Quality Flags

On this page

Summary

The logs themselves show when a pad-type measurement has gone bad: the density correction is large, the density or the photoelectric factor falls outside a physical range, or the neutron or sonic reading is impossible. These tests need no caliper and can be applied curve by curve. They are range and consistency rules that flag samples for the repair step, and the thresholds are starting points.

Inputs and outputs

Item Units
Input Density correction, Bulk density, Photoelectric factor g/cm³, g/cm³, b/e
Input Neutron porosity v/v
Input Compressional slowness µs/ft
Output One Bad-hole flag curve per measurement (density and PE, neutron, sonic) 0 or 1

Equations

Each flag is the logical OR of the tests that are switched on for the measurement. For density and the photoelectric factor:

\[ \badFlag_{\rho} = \left[\,\left|\dRhoCorr\right| > \Delta\rho_{\max}\,\right] \lor \left[\,\rhob < \rho_{\min}\,\right] \lor \left[\,\rhob > \rho_{\max}\,\right] \lor \left[\,\peFactor > P_{e,\max}\,\right] \]

For neutron porosity and sonic slowness, with \(\dtc\) the compressional slowness:

\[ \badFlag_{N} = \left[\,\phiN < \phi_{\min}\,\right] \lor \left[\,\phiN > \phi_{\max}\,\right] \qquad \badFlag_{\Delta t} = \left[\,\dtc < \Delta t_{\min}\,\right] \lor \left[\,\dtc > \Delta t_{\max}\,\right] \]

A jump test catches cycle skips and spikes that stay inside the range. It compares each sample with a running median \(\tilde{\Delta t}\) over a short window (5 samples):

\[ \left|\dtc - \tilde{\Delta t}\right| > J \]

A flag is padded by a few samples on each side before the curve is nulled or repaired (see Repair: Null-Out). The thresholds are tool- and formation-dependent: the worked example uses the starting values in the next section.

Symbol Variable Units Typical range
\(\Delta\rho\) Density correction g/cm³ -0.05 to 0.05
\(\rho_b\) Bulk density g/cm³ 1.8 to 3.0
\(P_e\) Photoelectric factor b/e 1.8 to 5.1
\(\phi_N\) Neutron porosity v/v -0.02 to 0.60
\(\Delta t\) Compressional slowness µs/ft 40 to 140
\(F_{bh}\) Bad-hole flag
Bad hole

Single-value calculator

No calculator: this is a set of logical tests on several curves. The worked example applies them to a synthetic table of samples with seeded problems.

Behavior

The tests find different problems. In the worked example, with 30 samples of seeded bad hole out of 400, the density-correction test alone flags 27 of the 30, the density minimum flags 2 samples (both already caught), and the PE maximum flags nothing. The range tests on the neutron and sonic flag nothing because the damaged samples are inside the range, but a jump test on the sonic catches 5 of 6 seeded cycle skips with no other sample flagged, where the sonic range test caught none. A rule on the maximum density flags all six samples of a real heavy-mineral (pyrite-like) streak, which the density-correction test correctly leaves alone: a range rule alone cannot distinguish a very dense rock from a bad reading.

Parameter guidance

The values below are starting points from common practice, not standards. They depend on the tool, the mud and the formation, and they should be checked against the curves.

Test Typical starting value Notes
Density correction, absolute value 0.10 to 0.20 g/cm³ The tool applies the correction for mudcake and standoff; a large correction means poor pad contact. Some projects use 0.05 for high-quality work. The most direct test for density quality.
Bulk density, minimum about 2.0 g/cm³ in clastics and carbonates Lower values are real in coal, some evaporites and very porous or organic rock. Set per formation, or exempt coal.
Bulk density, maximum about 3.0 g/cm³ Dolomite and anhydrite approach this. Heavy minerals such as pyrite and siderite exceed it in real rock, so use as a flag only where heavy minerals are not expected.
Photoelectric factor, maximum 8 to 10 b/e Calcite reads about 5 b/e. Higher values point to barite mud or a poor pad. The PE is of no use with barite mud and is flagged accordingly.
Neutron porosity, range about -0.05 to 0.60 v/v Negative values below a few percent are tool noise; values above about 0.6 are washout or a tool issue in most formations.
Sonic slowness, range 40 to 140 µs/ft Narrow to the formation if known. A jump test of 20 to 30 µs/ft against a 5-sample median catches cycle skips.

Which test for which curve. Density and PE use the density-correction test first. The neutron and sonic are less sensitive to the hole, so their tests are off by default and are switched on where a problem is found. A flag set for one curve does not have to apply to the other: the density can be bad while the sonic is fine. Combine with the caliper tests on Caliper-Based Flags by OR, and exempt real coal and salt as described on Coal and Salt Identification.

Worked example

A synthetic table of 400 samples with 30 seeded bad-hole samples (low density, large density correction, high PE), six sonic cycle skips and a six-sample heavy-mineral streak. The rules are applied one at a time and then combined.

rng = np.random.default_rng(2)
n = 400
# Synthetic sample table: mostly good data plus some density-pad problems and a dense, heavy-mineral streak
rhob = rng.normal(2.55, 0.08, n)
drho = rng.normal(0.01, 0.015, n)
pe = rng.normal(3.2, 0.5, n)
nphi = rng.normal(0.18, 0.04, n)
dt = rng.normal(75, 6, n)

bad = rng.choice(n, 30, replace=False)               # rough hole: low density, large correction
rhob[bad] -= rng.uniform(0.2, 0.5, bad.size)
drho[bad] = rng.uniform(0.12, 0.4, bad.size)
pe[bad] += rng.uniform(0.5, 4.0, bad.size)
skip = rng.choice(np.setdiff1d(np.arange(n), bad), 6, replace=False)   # sonic cycle skips
dt[skip] += rng.uniform(40, 70, skip.size)
pyr = np.arange(300, 306)                             # real pyrite-rich streak
rhob[pyr] = rng.uniform(3.0, 3.3, pyr.size)

limits = {
    "DRHO > 0.15 g/cm3 (abs)": np.abs(drho) > 0.15,
    "RHOB < 2.0 g/cm3": rhob < 2.0,
    "RHOB > 3.0 g/cm3": rhob > 3.0,
    "PE > 8 b/e": pe > 8.0,
    "NPHI < -0.05 or > 0.60": (nphi < -0.05) | (nphi > 0.60),
    "DT < 40 or > 140 us/ft": (dt < 40) | (dt > 140),
}
print(f"{'rule':28s} {'samples':>8}")
for k, f in limits.items():
    print(f"{k:28s} {f.sum():8d}")

density_flag = limits["DRHO > 0.15 g/cm3 (abs)"] | limits["RHOB < 2.0 g/cm3"] | limits["PE > 8 b/e"]
print(f"\ndensity-pad flag (DRHO or RHOB min or PE max): {density_flag.sum()} samples")
print(f"seeded bad-hole samples caught: {np.isin(bad, np.where(density_flag)[0]).sum()} of {bad.size}")
print(f"RHOB > 3.0 flags the pyrite streak ({limits['RHOB > 3.0 g/cm3'][pyr].sum()} of {pyr.size}): a range rule alone cannot tell heavy minerals from a bad reading")
dr = limits["DRHO > 0.15 g/cm3 (abs)"]
print(f"DRHO test on the streak: {dr[pyr].sum()} of {pyr.size} flagged, so DRHO is the better discriminator there")
cs = limits["DT < 40 or > 140 us/ft"]
print(f"sonic range rule catches {cs[skip].sum()} of {skip.size} seeded cycle skips")
# A jump rule compares each sample with a running median of 5 samples
w = np.lib.stride_tricks.sliding_window_view(np.pad(dt, 2, mode="edge"), 5)
jump = np.abs(dt - np.median(w, axis=1)) > 25.0
print(f"sonic jump rule (> 25 us/ft from the 5-sample median) catches {jump[skip].sum()} of {skip.size}, "
      f"with {jump.sum() - jump[skip].sum()} other samples flagged")

Output

rule                          samples
DRHO > 0.15 g/cm3 (abs)            27
RHOB < 2.0 g/cm3                    2
RHOB > 3.0 g/cm3                    6
PE > 8 b/e                          0
NPHI < -0.05 or > 0.60              0
DT < 40 or > 140 us/ft              0

density-pad flag (DRHO or RHOB min or PE max): 27 samples
seeded bad-hole samples caught: 27 of 30
RHOB > 3.0 flags the pyrite streak (6 of 6): a range rule alone cannot tell heavy minerals from a bad reading
DRHO test on the streak: 0 of 6 flagged, so DRHO is the better discriminator there
sonic range rule catches 0 of 6 seeded cycle skips
sonic jump rule (> 25 us/ft from the 5-sample median) catches 5 of 6, with 0 other samples flagged

Assumptions and limitations

  • The thresholds are appropriate for the tool and the rock. They are not universal.
  • The density correction is recorded and reliable. Some processing chains omit or recompute it.
  • Each curve is flagged independently, so that a bad density does not force the sonic to be flagged without reason.
  • A flagged sample is bad and an unflagged sample is good. Neither is certain: the tests have both false positives (real dense or light rock) and misses (moderately affected samples).
  • PE is valid only in a mud that does not contain barite.

QC checks

  • Flags plotted next to the caliper and the density correction: they cluster where the hole is bad and not in clean, in-gauge intervals.
  • Flagged samples are not concentrated in a single lithology (that would suggest a range test is flagging real rock).
  • The share flagged per curve is plausible and consistent from well to well.
  • Real extremes, such as a pyrite-rich zone or a coal, are not removed unless intended.
  • After nulling, the histogram of the curve loses its low-density tail and nothing else.

Going Deeper

The density correction is a built-in quality curve: the density tool has a short-spaced and a long-spaced detector, and the difference between the two apparent densities is converted to a correction that compensates for mudcake and standoff. A correction beyond the range for which the compensation was designed means that the compensation is no longer valid. Range tests on the readings themselves are blunter. They are useful for catching impossible values, and for catching the gross errors of other curves, but they mistake real extremes for errors. A well-designed workflow starts from the direct quality curves and uses range tests as a backup.

References

  1. Asquith, G. and Krygowski, D., 2004. Basic Well Log Analysis, 2nd edition. AAPG Methods in Exploration Series 16, American Association of Petroleum Geologists, Tulsa, OK.
  2. Rider, M. and Kennedy, M., 2011. The Geological Interpretation of Well Logs, 3rd edition. Rider-French Consulting Ltd, Sutherland, UK.

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.