DEV Community

Mezux
Mezux

Posted on

How Ground Penetrating Radar Finds Buried Cables: The Physics and Maths, in Python

Every time a new fibre line is laid in India, someone has to answer a simple question before digging: what is already down there?

Hit a live power cable and someone can get hurt. Cut an existing optical fibre cable (OFC) and a whole area can lose internet. Hit a gas line and it's worse. The trouble is that old utility records are often missing or wrong.

At our company we answer that question with Ground Penetrating Radar (GPR). It's mostly physics and a little maths, and you can understand it with about 30 lines of Python. Here's how it works.

1. What GPR actually does

A GPR unit looks like a lawnmower. It's pushed along the road and works in three steps:

  1. An antenna sends a short radar pulse into the ground.
  2. When the pulse hits something with different electrical properties (a pipe, a cable, a duct, a void), part of it bounces back.
  3. The unit records how long the echo took to come back, in nanoseconds.

Do that every few centimetres along a line and you get a radargram: a picture of the ground's cross-section.

The catch: the radar doesn't measure depth. It measures time. Turning time into depth is where the physics comes in.

2. From echo time to depth

Radar travels slower in soil than in air. How much slower depends on the soil's relative permittivity (εr, also called the dielectric constant):

  • v = c / √εr
  • c is the speed of light, about 0.3 m per nanosecond.
  • Dry sand has a low εr, so the wave stays fast.
  • Wet clay has a high εr, so the wave is slow.

The pulse goes down and back up, so depth is half the distance travelled:

python
import math

C = 0.2998 # speed of light, metres per nanosecond

def velocity(eps_r: float) -> float:
"""Radar wave speed in the ground (m/ns) from relative permittivity."""
return C / math.sqrt(eps_r)

def depth(two_way_time_ns: float, v: float) -> float:
"""Depth in metres: the wave goes down AND back up, so halve it."""
return v * two_way_time_ns / 2

for soil, eps in [("dry sand", 4), ("moist loam", 12), ("wet clay", 25)]:
v = velocity(eps)
print(f"{soil:<11} εr={eps:>2} v={v:.3f} m/ns 20 ns echo → {depth(20, v):.2f} m deep")

Output:

dry sand εr= 4 v=0.150 m/ns 20 ns echo → 1.50 m deep
moist loam εr=12 v=0.087 m/ns 20 ns echo → 0.87 m deep
wet clay εr=25 v=0.060 m/ns 20 ns echo → 0.60 m deep

The same 20 ns echo can mean 60 cm or 1.5 m depending on the soil. If you guess the soil wrong, every depth in your report is wrong. So how do you get the real velocity?

3. The hyperbola trick: calibrating on site

This is my favourite part.

When the GPR cart passes over a pipe that runs across its path, the radar "sees" the pipe before it reaches it and after it has passed, because the beam spreads out like a cone. The echo takes longest when the pipe is off to the side and shortest when the cart is directly on top.

On the radargram that draws a hyperbola, an upside-down "U". Its shape follows this equation:

t(x) = (2 / v) · √( d² + (x − x₀)² )

  • x₀ is where the cart was directly above the pipe.
  • d is the pipe's depth.
  • v is the wave speed in this exact soil, today.

Square both sides and it becomes a straight line:

t² = (4 / v²) · (x − x₀)² + (2d / v)²

So a basic linear fit gives you both the velocity and the depth:

python
import numpy as np

Simulated field data: a pipe 1.2 m deep, true velocity 0.10 m/ns

rng = np.random.default_rng(1)
true_v, true_d, x0 = 0.10, 1.2, 5.0
x = np.linspace(3.5, 6.5, 31) # cart position (m)
t = 2 * np.sqrt(true_d*2 + (x - x0)*2) / true_v # echo time (ns)
t += rng.normal(0, 0.3, t.size) # add measurement noise

Fit t² against (x − x0)²

slope, intercept = np.polyfit((x - x0)2, t2, 1)
v_est = 2 / np.sqrt(slope)
d_est = v_est * np.sqrt(intercept) / 2

print(f"velocity ≈ {v_est:.3f} m/ns, depth ≈ {d_est:.2f} m")

velocity ≈ 0.099 m/ns, depth ≈ 1.19 m

That's why good survey teams calibrate velocity on site instead of using a value from a table. The soil itself tells you its speed, and every buried pipe is a free calibration point.

Simplification alert: real processing also accounts for the distance between transmitter and receiver, the pipe's radius, and the soil changing along the route. But the core idea is exactly this fit.

4. Choosing a frequency: see deep, or see detail?

GPR antennas come in different frequencies, and there's a trade-off:

  • Low frequency (100–250 MHz): reaches deeper but shows less detail.
  • Mid frequency (250–400 MHz): the usual choice for utilities at normal burial depths.
  • High frequency (1.6–2.6 GHz): fine detail but shallow. Used for scanning concrete slabs and rebar.

A rule of thumb: the smallest vertical detail you can separate is about a quarter of the wavelength.

python
def resolution_cm(freq_mhz: float, v: float) -> float:
wavelength_m = v / (freq_mhz / 1000) # v in m/ns, f in GHz
return wavelength_m / 4 * 100

v = velocity(9) # a typical moist soil
for f in (200, 400, 2000):
print(f"{f:>5} MHz → ~{resolution_cm(f, v):.1f} cm vertical resolution")
200 MHz → ~12.5 cm vertical resolution
400 MHz → ~6.2 cm vertical resolution
2000 MHz → ~1.2 cm vertical resolution

Higher frequency gives sharper detail, but the signal also dies faster in the ground. So you pick the antenna for the job.

5. Where GPR struggles (and why honest reports matter)

GPR is not magic, and anyone who says it finds everything is selling something.

  • Wet clay and salty soil conduct electricity and soak up the radar signal. Depth reach drops sharply.
  • Small targets with low contrast can be hard to see, such as a thin cable in soil with similar properties.
  • Non-metallic pipes (HDPE, PVC, empty ducts) can be found by GPR. That's a big advantage over metal detectors. But it depends on soil, size and depth.

That's why a good survey:

  • also uses electromagnetic locators for metal cables;
  • gives a confidence grade for each detected line (quality levels in the style of PAS 128);
  • records no-access areas and limitations in the report;
  • recommends a trial pit at critical crossings.

6. What this looks like in the field

On a typical fibre-route survey along a highway, our team:

  1. agrees a scan plan before going out;
  2. runs parallel lines along the planned cable route;
  3. adds cross-lines at roads, drains, canals and railway crossings, where surprises usually are;
  4. calibrates velocity from hyperbolas, as above;
  5. marks the services on the ground;
  6. has a second engineer, who didn't collect the data, review it the same day;
  7. delivers a signed report with depth by section, plus CAD/GIS files and an L-section depth profile where required.

The raw radar data is archived, so every line on the drawing can be traced back to the echo that produced it. For the developers reading this: think of it as version control for the underground.

Try it yourself

If you want to play with real radar data, look up open-source GPR tools in Python (for example, search "GPR processing Python" on GitHub). Start by fitting a hyperbola from a published sample radargram. It's a satisfying weekend project, and you'll never look at a dug-up road the same way again.

I'm Manas Verma, from Mezux Touch, a GPR survey and underground utility mapping company based in Faridabad, India. Questions about GPR or utility mapping are welcome in the comments.

Top comments (0)