Unit Detection and Conversion
On this page
Summary
Logs arrive in different unit systems, and a calculation that assumes one system silently misreads another. This page gives the conversions between Bulk density (SI) and Bulk density, Compressional slowness (SI) and Compressional slowness, Caliper (millimetres) and caliper in inches, Neutron porosity (percent) and Neutron porosity, and Formation conductivity and resistivity, and the value-range heuristics that detect the unit when the header is blank or wrong. Use it before any curve is used in a calculation.
Inputs and outputs
| Item | Units | |
|---|---|---|
| Input | Bulk density (SI) | kg/m³ |
| Input | Compressional slowness (SI) | µs/m |
| Input | Caliper (millimetres) | mm |
| Input | Neutron porosity (percent) | pu |
| Input | Formation conductivity | mS/m |
| Output | Bulk density | g/cm³ |
| Output | Compressional slowness | µs/ft |
| Output | Caliper | in |
| Output | Neutron porosity | v/v |
| Output | True formation resistivity | ohm·m |
Equations
Each conversion is a fixed factor, except the conductivity, which is a reciprocal.
Density, from kg/m³ to g/cm³:
Compressional slowness, from µs/m to µs/ft (a foot is 0.3048 m, so a foot takes fewer microseconds than a metre):
Caliper, from mm to in:
Neutron porosity, from porosity units (percent) to a volume fraction:
Resistivity from conductivity (mS/m is the same number as mmho/m):
The factor 1000 appears because 1 S/m is 1000 mS/m, and resistivity in ohm·m is the reciprocal of conductivity in S/m.
| Symbol | Variable | Units | Typical range |
|---|---|---|---|
| \(\rho_{b,\mathrm{SI}}\) | Bulk density (SI) | kg/m³ | 1500 to 3200 |
| \(\rho_b\) | Bulk density | g/cm³ | 1.8 to 3.0 |
| \(\Delta t_{\mathrm{SI}}\) | Compressional slowness (SI) | µs/m | 130 to 660 |
| \(\Delta t\) | Compressional slowness | µs/ft | 40 to 140 |
| \(d_{c,\mathrm{mm}}\) | Caliper (millimetres) | mm | 100 to 760 |
| \(d_c\) | Caliper | in | 6 to 20 |
| \(\phi_{N,\mathrm{pu}}\) | Neutron porosity (percent) | pu | -2 to 60 |
| \(\phi_N\) | Neutron porosity | v/v | -0.02 to 0.60 |
| \(\sigma\) | Formation conductivity | mS/m | 1 to 5000 |
| \(R_t\) | True formation resistivity | ohm·m | 0.2 to 2000 |
Single-value calculator
Behavior
The first four conversions are straight lines, so the interesting behaviour is in the conductivity. Resistivity is the reciprocal of conductivity, so it falls steeply at low conductivity and flattens at high conductivity: 10 mS/m is 100 ohm·m, 50 mS/m is 20 ohm·m, 100 mS/m is 10 ohm·m, 500 mS/m is 2 ohm·m and 1000 mS/m is 1 ohm·m. Equal steps in conductivity are therefore very unequal steps in resistivity, which is why resistivity is averaged, filtered and normalised in logarithms and conductivity is averaged linearly. At the default inputs the calculator gives 2.650 g/cm³, 67.06 µs/ft, 8.50 in, 0.120 v/v and 10.00 ohm·m.
Parameter guidance
The conversion factors are exact definitions. The practical work is deciding which conversion to apply. Read the unit string in the header first, accepting the common spellings (for example G/C3, G/CC and g/cm3 for density, US/F and USEC/FT for slowness, MMHO and MS/M for conductivity), then confirm it against the values. Apply a value heuristic only to confirm or, when the header is blank, to propose.
| Curve | Unit A | Unit B | Heuristic on non-null values |
|---|---|---|---|
| Bulk density | g/cm³ (1.5 to 3.2) | kg/m³ (1500 to 3200) | Median above 100 means kg/m³. The gap between the ranges is very wide, so this is robust. |
| Compressional slowness | µs/ft (about 40 to 200) | µs/m (about 130 to 660) | The ranges overlap between 130 and 200. A 2nd percentile below 120 means µs/ft, a 98th percentile above 250 means µs/m, anything else is ambiguous and goes to a person. |
| Caliper | in (about 4 to 30) | mm (about 100 to 760) | Median above 40 means mm. |
| Porosity-like fractions | v/v (0 to about 0.6) | percent or pu (0 to about 60) | 99th percentile above 1.5 means percent, at or below 1.0 means fraction. Tight, low-porosity intervals can sit entirely below 1 percent, so apply the rule to a whole well and not a short interval. |
| Conductivity or resistivity | ohm·m | mS/m (mmho/m) | Value ranges overlap and cannot decide. Use the header and the mnemonic first, then check the sign of the correlation of the logarithm with the gamma ray in a clastic section: resistivity falls where gamma ray rises, conductivity rises. |
| Gamma ray | gAPI | other (counts, dose rate) | Values far outside 0 to 400 gAPI are a different unit. Converting from a non-API unit needs a tool-specific calibration, so do not guess a factor. |
The thresholds above are starting points and not standards. Set them from your own data. Apply the detection to the whole curve after null handling (see Null and Invalid Value Handling), because a sentinel value of -999.25 shifts every statistic. Shared choices are on the step page.
Worked example
Detect the unit of curves from their values on synthetic data, and convert a few sample values. The code is the detector used as the reference implementation of this page:
import numpy as np
def _clean(x):
x = np.asarray(x, dtype=float)
return x[np.isfinite(x)]
def detect_density(x):
"""Bulk density: rock is 1.5 to 3.2 g/cc, so 1500 to 3200 in kg/m3."""
med = np.median(_clean(x))
return "kg/m3" if med > 100 else "g/cc"
def detect_sonic(x):
"""Slowness: us/ft is 40 to 200 and us/m is about 130 to 660. Only the tails separate them."""
v = _clean(x)
lo, hi = np.percentile(v, 2), np.percentile(v, 98)
if lo < 120:
return "us/ft" # nothing real reads below about 130 us/m
if hi > 250:
return "us/m" # nothing real reads above about 200 us/ft
return "ambiguous"
def detect_caliper(x):
"""Hole diameter: 4 to 30 in, or 100 to 760 mm."""
return "mm" if np.median(_clean(x)) > 40 else "in"
def detect_fraction(x):
"""Porosity: fraction (0 to ~0.6) or percent (0 to ~60)."""
p99 = np.percentile(_clean(x), 99)
if p99 > 1.5:
return "percent"
return "fraction" if p99 <= 1.0 else "ambiguous"
def detect_conductivity(x, gr):
"""A resistivity falls where gamma ray rises in clastics; a conductivity rises. Weak but useful."""
ok = np.isfinite(x) & np.isfinite(gr) & (np.asarray(x) > 0)
r = np.corrcoef(np.log10(np.asarray(x)[ok]), np.asarray(gr)[ok])[0, 1]
return ("conductivity" if r > 0 else "resistivity"), r
if __name__ in ("__main__", "worked_example"):
rng = np.random.default_rng(5)
n = 400
gr = np.clip(70 + 45 * np.sin(np.linspace(0, 12, n)) + rng.normal(0, 6, n), 10, 200)
rt = 10 ** (1.2 - 0.9 * (gr - 70) / 45 * 0.5 + rng.normal(0, 0.05, n)) # ohm.m, falls as GR rises
curves = {
"RHOB (kg/m3 file)": (detect_density, 1000 * (2.55 + rng.normal(0, 0.05, n))),
"RHOB (g/cc file) ": (detect_density, 2.55 + rng.normal(0, 0.05, n)),
"DT (us/m file) ": (detect_sonic, (75 + 12 * np.sin(np.linspace(0, 9, n))) / 0.3048),
"DT (us/ft file)": (detect_sonic, 75 + 12 * np.sin(np.linspace(0, 9, n))),
"CALI (mm file) ": (detect_caliper, 25.4 * (8.6 + np.abs(rng.normal(0, 0.4, n)))),
"NPHI (percent) ": (detect_fraction, np.abs(rng.normal(14, 5, n))),
"NPHI (fraction) ": (detect_fraction, np.abs(rng.normal(0.14, 0.05, n))),
}
for name, (fn, data) in curves.items():
print(f"{name} -> {fn(data)}")
print("sonic, narrow window 60 to 100 us/ft:", detect_sonic(np.linspace(60, 100, n)))
print("sonic, same rock in us/m (197 to 328):", detect_sonic(np.linspace(60, 100, n) / 0.3048))
kind, r = detect_conductivity(rt, gr)
print(f"curve of resistivity values -> {kind} (corr with GR {r:+.2f})")
kind, r = detect_conductivity(1000.0 / rt, gr)
print(f"curve of conductivity values -> {kind} (corr with GR {r:+.2f})")
print()
print("conversions of a few values:")
print(f" 2650 kg/m3 -> {2650 / 1000:.3f} g/cc")
print(f" 220 us/m -> {220 * 0.3048:.2f} us/ft")
print(f" 216 mm -> {216 / 25.4:.2f} in")
print(f" 12 pu -> {12 / 100:.2f} v/v")
print(f" 100 mS/m -> {1000 / 100:.1f} ohm.m")
Output
RHOB (kg/m3 file) -> kg/m3
RHOB (g/cc file) -> g/cc
DT (us/m file) -> us/m
DT (us/ft file) -> us/ft
CALI (mm file) -> mm
NPHI (percent) -> percent
NPHI (fraction) -> fraction
sonic, narrow window 60 to 100 us/ft: us/ft
sonic, same rock in us/m (197 to 328): us/m
curve of resistivity values -> resistivity (corr with GR -0.99)
curve of conductivity values -> conductivity (corr with GR +0.99)
conversions of a few values:
2650 kg/m3 -> 2.650 g/cc
220 us/m -> 67.06 us/ft
216 mm -> 8.50 in
12 pu -> 0.12 v/v
100 mS/m -> 10.0 ohm.m
Assumptions and limitations
- The curve has been aliased to the right family, so that the heuristic for the right quantity is applied.
- Null values were removed before the statistics were computed. A -999.25 in a density curve gives a negative median and a wrong answer.
- A single unit applies to the whole curve. A merged curve can have a different unit in each section, in which case detect on each run before merging.
- The curve has enough valid samples. A handful of samples in a narrow interval can sit in the overlap zone of the ranges.
- For the conductivity test, the section includes clastic beds with a gamma ray contrast. In clean carbonates the correlation is weak and the test is not usable.
QC checks
- After conversion, the median and the 2nd and 98th percentiles of each curve are inside the plausible range of its family.
- A cross-check between curves holds: density porosity, neutron porosity and sonic porosity of a clean water zone agree within a few porosity units.
- Resistivity does not decrease with depth in a way that contradicts the geology only because of a conductivity error. Compare with the offset well.
- The result of automatic detection agrees with the header whenever the header is present. Disagreements are listed for a person.
- Unit labels on the converted curves were updated, so the same conversion is never applied twice.
Going Deeper
The unit systems come from different regions and eras. Imperial units are standard in North American wireline work, while SI units appear in other regions, in some LWD data and in processed products from geophysical software. Conductivity is not a regional choice: induction tools measure conductivity directly, so older logs often carry it, and it is converted to resistivity for interpretation. Because the reciprocal is nonlinear, a conductivity log that is displayed or filtered as resistivity (or the reverse) gives different values for the same operation. Automatic detection is useful for large data sets, but it is a proposal; for curves whose header and values disagree, a person with the log header in front of them is the better test.
References
- Canadian Well Logging Society, 1992 (revised 1999). LAS Version 2.0: A Digital Standard for Logs. Update February 1992 (CWLS Log ASCII Standard).
- 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.
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.