Recast Systems Recast Systems How Radar Sees
Recast Radar Tools  /  A hands-on guide

How radar sees

Everything on this page runs on one real scan: the NEXRAD radar at Oklahoma City, KTLX, at 7:02 pm local time on 14 March 2024, with strong storms to its southeast. You can take it apart sweep by sweep, all the way down to the bytes in the file.

1 / The scan

One scan, twenty sweeps

The antenna turns a full circle at one angle, tilts up, and turns again. One volume scan took this radar about six minutes. Each bar below is one full turn, drawn when it happened and at the angle it was aimed.

Volume coverage pattern 212, as recorded
Surveillance: long range, dual-pol Doppler: velocity, shorter range Batch: both in one turn

Low angles come back again and again. The radar returns to 0.48° five times in this one scan (look for the repeats along the bottom). That is MESO-SAILS: the lowest angle, where tornadoes and damaging winds live, gets refreshed every minute or so instead of every six.

Each low angle is scanned twice. The first turn listens for far-away echoes with long gaps between pulses. The second pulses quickly to measure wind speed without folding (chapter 4 explains why). Higher up, one turn does both.

list the sweeps
import recast_radar
volume = recast_radar.read("KTLX20240315_000217_V06")
volume.vcp                      # 212
for s in volume.sweeps:
    print(s["fixed_angle"], s["nrays"], s["fields"])
2 / The sweep

Reading one sweep

This is the lowest turn: 720 rays, one every half degree, each cut into gates 250 m long. Every gate holds a measurement. Move over the picture to read any one of them.

0.48° sweep
Drag to pan · double-click to zoom
–value
–azimuth
–along the beam
–beam above sea level

What the colors mean

A blank gate is not all the same

Where the picture is black, the radar listened and heard nothing above its noise: clear air. That is different from a gate with no data, and different again from a range-folded gate (purple on velocity), where an echo from beyond the radar's listening range arrived late and landed in the wrong place. The library keeps these three apart instead of calling them all NaN.

Try this
  • Switch to ZDR, then CC. The strong core southeast of the radar changes character.
  • Hover near the radar, then far out. The beam climbs from a few hundred metres to several kilometres.
  • Switch to Velocity. Green is wind blowing toward the radar, red is away.
read a gate
tree = recast_radar.open("KTLX20240315_000217_V06")
dbz = tree["sweep_0"]["DBZH"]          # dBZ, NaN where no echo
dbz.sel(azimuth=140, range=94_000, method="nearest").item()
recast_radar.render(tree, "dbz.png", sweep=0, field="DBZH")
3 / The beam

Where the beam goes

The radar sees in straight-ish lines while the Earth curves away underneath. So the farther out a storm is, the higher up the radar is looking at it. Here are all eleven angles of this scan, drawn to scale.

Side view, 4/3 Earth radius, 0.95° beam width
–lowest beam, bottom
–lowest beam, centre
–highest beam, top
–beam width

Below the lowest beam, the radar is blind. At the storm 94 km away, the bottom of the lowest beam is already about a kilometre up. Anything lower, like the base of a tornado, is out of sight. This is why a radar network needs radars close together.

Above the highest beam is the cone of silence. Near the radar, even the top tilt passes under tall storms, so their tops go unseen.

The beam gets wide. A 0.95° beam is over a kilometre and a half across at 100 km. Small features smear out with distance.

MathsThe beam height formula

h = √(r² + (k a)² + 2 r k a sinθ) − k a, with slant range r, elevation θ, Earth radius a = 6371 km and k = 4/3 for standard refraction (Doviak and Zrnić). Ground distance is s = k a · asin(r cosθ / (k a + h)).

In the library: beam_height_above_radar_m and beam_ground_range_m. For a real atmosphere, trace_refracted_beam takes a refractivity profile and propagation_regime flags ducting.

beam geometry
use recast_radar_tools::model::{beam_ground_range_m, beam_height_above_radar_m};

let h = beam_height_above_radar_m(94_000.0, 0.48);  // metres above the antenna
let s = beam_ground_range_m(94_000.0, 0.48);        // metres along the ground
4 / Velocity

Wind that folds, and unfolding it

A Doppler radar measures how fast things move toward or away from it, but only up to a limit called the Nyquist velocity. Faster winds wrap around to the other end of the scale, so a strong wind blowing away can read as a wind blowing toward.

How folding works
–radar reports
–times folded
–Nyquist here
Real velocity: raw ← drag → dealiased
Drag the white divider · drag elsewhere to pan
−60 toward0+60 m/s away

Folded gates show up as sudden jumps: bright green right next to bright red where the real wind is smooth. The library's dealiaser finds those seams and adds or subtracts twice the Nyquist velocity to put each region back where it belongs. The result is a new field, VRADDH, kept beside the raw one.

The dealiased field on the right was made by the library in the same run as everything else on this page.

dealias
result = recast_radar.process("KTLX20240315_000217_V06", ["VRADDH"], band="s")
result.report["inserted"]         # [[1, "VRADDH"], [3, "VRADDH"], ...]
tree = result.volume.to_datatree()
tree["sweep_1"]["VRADDH"]
5 / Dual polarization

What is it made of?

The radar sends pulses in two directions at once, horizontal and vertical, and compares what comes back. Raindrops, hailstones and bugs each return a different mix. Drag a box over part of the storm to see what the three fields say about it.

Drag a box on any panel
Reflectivity, dBZ
ZDR, dB
Correlation, CC
–gates selected
–median reflectivity
–median ZDR
–median CC
–

The three fields

Reflectivity is how much comes back. More and bigger targets, more signal.

ZDR compares horizontal with vertical. Big raindrops flatten as they fall, so they return more horizontally: positive ZDR. Tumbling hail looks round on average: ZDR near zero.

CC (correlation coefficient) says how alike the targets in a gate are. Pure rain or pure snow is above 0.97. A mix of rain and hail drops lower. Birds, bugs and debris are far lower.

The verdict under the chart is a rule of thumb for learning, not a classifier. Operational hydrometeor classification weighs more fields, temperature and texture.

select gates by their fields
s = recast_radar.open("KTLX20240315_000217_V06")["sweep_0"]
hail_like = (s["DBZH"] > 55) & (s["ZDR"] < 1) & (s["RHOHV"] < 0.95)
rain_like = (s["RHOHV"] > 0.97) & (s["ZDR"] > 0.3)
int(hail_like.sum()), int(rain_like.sum())
6 / Slice

Cut the storm open

Stack all eleven tilts and you can slice straight down through a storm. Draw a line across it on the map and the section below shows each beam where it crossed that line, at its true height.

Drag the ends, or draw a new line
Section line: A → B
5 dBZ2035506575

Every stripe in the section is one beam. Low tilts are close together. Higher up they spread apart, and the gaps between them are places the radar never looked. A section is only as good as the scan's vertical sampling.

The empty wedge along the bottom is the ground the lowest beam overshoots. It grows with distance, as chapter 3 showed.

Try this
  • Draw across the strongest core. Look for the tall column of high reflectivity: strong updrafts hold hail aloft.
  • Draw a line toward the radar, then away. The section stretches or shrinks vertically because the beams are closer to the ground near the radar.
vertical section
sec = recast_radar.cross_section(
    "KTLX20240315_000217_V06",
    start_km=(40, -55), end_km=(85, -95),   # (east, north) from the radar
    field="DBZH", top_m=16_000)
sec.plot()                                  # an xarray DataArray
7 / Looking down

Composite, echo tops and VIL

Column products look down through every tilt above each spot on the map and boil the column into one number. Hover a spot to see the column the number came from.

From all eleven tilts
The column under your cursor
–composite
–echo top
–VIL

Composite reflectivity is the strongest value in the column. Echo top is the highest point still above 18.3 dBZ, a sign of how tall the storm is. VIL (vertically integrated liquid) adds up the water the column holds; big values often mean big hail.

column products
result = recast_radar.process("KTLX20240315_000217_V06", ["CREF", "ET", "VIL"])
recast_radar.render(result.volume, "cref.png", field="CREF")
8 / The file

Inside the file

These are the real first bytes of KTLX20240315_000217_V06. Click any byte to see what it is. The file is a short header, then blocks of compressed messages; one message holds one ray.

Volume header, 24 bytes

Start of the second record: size, then bzip2

The first ray (Message 31), after decompression

Click a byteColoured underlines group the bytes into fields.

Reflectivity is stored as one byte per gate. The moment header says how to turn a byte into dBZ: dBZ = (byte − offset) / scale. Two codes are special: 0 means below the noise threshold, and 1 means range folded.

The library never expands these to floats unless you ask. It keeps the byte and the scale and offset, which is a big part of why it reads files quickly and in little memory.

see the raw values
recast-radar dump KTLX20240315_000217_V06                 # every header value
recast-radar dump --json --data --sweep 0 --field DBZH KTLX20240315_000217_V06