The Physics of a Bite: Math, Geometry and Light in Bass Fishing

Projectile motion, Snell’s window, spring constants and order statistics: the equations hiding inside a day on the water, with Python you can run yourself.

I fished bass for a long time before I ever thought of it as a physics problem. Then I started fishing tournaments, and the arithmetic changed. A fun day is measured in bites. A tournament day is measured in pounds and ounces across five fish, against a clock, against a field of people who are often better than you on that particular body of water. When the margin between a check and a long ride home is a few ounces, you start asking why things happen instead of just accepting that they do.

Why does a crankbait run deeper on 10-pound line than on 15? Why does a fish that ate a jig at 60 feet come unbuttoned on a hookset that would have been fine at 20? Why can a bass in four feet of clear water see you standing on the deck from 40 feet away, but not at all in a stained creek? Why does culling a 2-pounder for a 2-pound-4-ounce fish feel like progress when it barely moves your expected finish?

Every one of those has a clean answer in physics, geometry or probability. Most anglers know the answers by feel. I did too. Writing them down as equations made me better at the parts of the game I could control, and more honest about the parts I couldn’t.

This piece walks through the cast, the strike zone, what a bass actually sees, how fast a bait falls, what your line does on a hookset, how sonar draws its picture, and the tournament math of a five-fish limit. Every section has a formula, a worked number and Python you can run. I recomputed every figure in code before writing it down, and the sources are at the end.

A fish in the boat is a product, not a sum

The frame I use for everything else is simple. A fish in the livewell is the end of a chain of events, and every link has to happen. Your lure has to pass close enough to a bass for it to notice. The bass has to detect it. It has to decide to hit it. You have to get the hook in. Then you have to get the fish in the boat.

Write that as a rate and you get a proposed model I’ll lean on for the rest of the piece:

λ = E × p_detect × p_strike × p_hook × p_land

Here λ is fish landed per hour, E is the number of bass your lure passes within range of per hour, and each p is the probability that the next link holds given the previous one did. I’m not claiming anyone has measured these terms for a lake. The value is in the structure.

Plug in plausible numbers for a decent day: E = 12, p_detect = 0.5, p_strike = 0.15, p_hook = 0.7, p_land = 0.85. That gives λ ≈ 0.54 fish per hour, or about 4.3 fish over an 8-hour tournament day. If bites arrive as a Poisson process, the chance of catching five or more is about 43%. In other words, on this model you fail to fill a limit more often than not.

Two things fall out of the product form. First, improvements multiply. Ten percent better at each of five links is 1.¹⁵ ≈ 1.61, a 61% higher catch rate, which is why the anglers who beat you every weekend rarely have one secret. They are a little better everywhere.

Second, the payoff from fixing a link is proportional to its relative change, not its absolute one. Raising p_land from 0.85 to 0.95 buys about 12%. Raising p_hook from 0.7 to 0.85 buys 21%, which lifts the chance of a five-fish limit in this example from 43% to 59%. Raising p_strike from 0.15 to 0.25 buys 67%. The weakest link, measured as a ratio, is where the work pays.

Each section below is about one of those links. Casting and geometry set E. Light, vision and fall rate shape p_detect and p_strike. Line, rods and drag set p_hook and p_land. The last section is about what a tournament does with the fish once they’re in the boat.

The cast: why 45 degrees is wrong

Every physics student learns that a projectile goes farthest at a 45-degree launch angle. That result assumes no air. A crankbait is light, blunt and slowed by the line peeling off your spool, so it lives in the regime where drag matters a lot.

The equations of motion for a lure of mass m with velocity v are:

m dv/dt = −½ ρ C_d A |v_rel| v_rel − F_line · v/|v| − m g ŷ

The first term is quadratic air drag, where ρ is air density (about 1.2 kg/m³), C_d is the drag coefficient, A is the lure’s frontal area and v_rel is velocity relative to the air, which is where wind enters. The second term lumps together everything the line does to you: spool friction, brake drag and line slapping the guides. The third is gravity. There’s no closed-form solution, so we integrate.

"""Point-mass cast with quadratic air drag plus a lumped line/spool drag.

Not a validated model of any specific reel. The line-drag term is a
calibration knob, chosen so a 3/8 oz crankbait lands in the range most
of us actually cast it.
"""
import math

import numpy as np
from scipy.integrate import solve_ivp

RHO_AIR = 1.2      # kg/m^3
G = 9.81           # m/s^2
OZ = 0.0283495     # kg per ounce
M_TO_FT = 3.28084

def cast_distance(v0, angle_deg, mass_oz=0.375, cd=0.6, diam_m=0.02,
                  line_drag_n=0.04, release_h=1.8, headwind=0.0):
    """Horizontal distance (m) where the lure hits the water."""
    m = mass_oz * OZ
    area = math.pi * (diam_m / 2) ** 2
    k = 0.5 * RHO_AIR * cd * area

    def rhs(t, s):
        x, y, vx, vy = s
        rvx, rvy = vx + headwind, vy          # velocity relative to air
        vrel = math.hypot(rvx, rvy)
        v = math.hypot(vx, vy)
        ax = (-k * vrel * rvx - line_drag_n * vx / v) / m
        ay = (-k * vrel * rvy - line_drag_n * vy / v) / m - G
        return [vx, vy, ax, ay]

    def hit_water(t, s):
        return s[1]
    hit_water.terminal, hit_water.direction = True, -1

    th = math.radians(angle_deg)
    s0 = [0.0, release_h, v0 * math.cos(th), v0 * math.sin(th)]
    sol = solve_ivp(rhs, (0, 20), s0, events=hit_water, max_step=0.01)
    return sol.y_events[0][0][0]

def best_angle(**kw):
    angles = np.arange(10, 60.5, 0.5)
    dists = [cast_distance(angle_deg=a, **kw) for a in angles]
    i = int(np.argmax(dists))
    return angles[i], dists[i]

if __name__ == "__main__":
    v0 = 28.0
    for oz in (0.25, 0.375, 0.5, 0.75):
        a, d = best_angle(v0=v0, mass_oz=oz)
        d45 = cast_distance(v0, 45, mass_oz=oz)
        print(f"{oz:>5} oz  best angle {a:4.1f} deg  {d*M_TO_FT:5.0f} ft"
              f"  (45 deg: {d45*M_TO_FT:5.0f} ft)")
    for wind in (0.0, 4.5, 9.0):   # m/s headwind (0, ~10, ~20 mph)
        a, d = best_angle(v0=v0, headwind=wind)
        print(f"headwind {wind:3.1f} m/s  best {a:4.1f} deg  {d*M_TO_FT:5.0f} ft")

With a launch speed of 28 m/s, a vacuum would send the lure about 262 feet. With drag, a 3/8-ounce bait with a 2 cm body flies about 128 feet, and its best launch angle is 32 degrees, not 45. Holding the body size fixed and changing only the weight, a 1/4-ounce bait peaks at 27.5 degrees and 101 feet, and a 3/4-ounce bait peaks at 37.5 degrees and 174 feet. The lighter the lure relative to its area, the flatter you should throw it.

Wind makes the same point louder. Into a 10 mph headwind (4.5 m/s) the best angle falls to 29.5 degrees and the cast shrinks to 113 feet. Into 20 mph it’s 26 degrees and 97 feet. Every good caster I’ve fished with lowers the trajectory into the wind without thinking about it. The model says that’s exactly right, because time in the air is time spent losing speed to a headwind.

The launch speed and line-drag values are calibration assumptions, not measurements. The shape of the result holds across any reasonable choice: drag pushes the best angle below 45, and the lighter or windier it gets, the further below.

Backlash is a deceleration mismatch

A baitcaster spool is a flywheel. During the cast it spins at ω = v/r, where v is line speed and r is the radius of the line pack. A backlash happens whenever the spool tries to feed line faster than the lure is taking it. That gives a clean condition. The spool’s angular deceleration has to keep up with the lure’s linear deceleration divided by r:

τ_brake + τ_bearing ≥ I · a_lure / r

I is the spool’s moment of inertia and a_lure is how fast the lure is slowing down. With my assumed numbers (a 3/8-ounce bait, I = 2.6 × 10⁻⁶ kg·m², r = 1.6 cm), the lure is decelerating at about 12 m/s² right after launch, and the spool needs about 1.9 mN·m of braking torque to keep pace.

The interesting part is how the two common brake designs scale. Magnetic brakes work by eddy currents, so their torque grows roughly in proportion to ω. Centrifugal brakes press weights against a ring, so their torque grows with ω². Air drag on the lure grows with v², but the line-drag term doesn’t shrink with speed at all.

"""Overrun check for a casting reel spool.

The spool overruns when it wants to pay out line faster than the lure
takes it. Simplified: ignores line mass, the shrinking line pack and
line on the guides. Spool inertia and friction values are assumptions.
"""
import math

RHO_AIR = 1.2
OZ = 0.0283495

m_lure = 0.375 * OZ                 # kg
area = math.pi * 0.01 ** 2          # 2 cm body
cd = 0.6
line_drag = 0.04                    # N, same lumped term as the cast model
r_spool = 0.016                     # m, effective radius of the line pack
I_spool = 2.6e-6                    # kg m^2, light aluminum spool with line
tau_bearing = 2e-5                  # N m, bearing friction

def lure_decel(v):
    return (0.5 * RHO_AIR * cd * area * v**2 + line_drag) / m_lure

def brake_torque_needed(v):
    """Torque that keeps spool surface speed matched to the lure."""
    alpha = lure_decel(v) / r_spool
    return I_spool * alpha - tau_bearing

# Two brake laws tuned to give the same torque at 15 m/s
v_ref = 15.0
tau_ref = brake_torque_needed(v_ref)
c_mag = tau_ref / (v_ref / r_spool)          # eddy current: tau = c * omega
c_cen = tau_ref / (v_ref / r_spool) ** 2     # centrifugal: tau = c * omega^2

if __name__ == "__main__":
    print(f"{'v m/s':>6} {'needed':>8} {'magnetic':>9} {'centrif':>8}  (mN m)")
    for v in (28, 22, 15, 10, 5, 2):
        w = v / r_spool
        print(f"{v:6.0f} {brake_torque_needed(v)*1e3:8.2f} "
              f"{c_mag*w*1e3:9.2f} {c_cen*w*w*1e3:8.2f}")

Tune both brakes to match at 15 m/s and compare. At launch (28 m/s) the centrifugal brake delivers about 3.4 mN·m against 1.9 needed, so it over-brakes and costs distance. Late in the cast at 5 m/s, the centrifugal brake is down to about 0.11 mN·m and the magnetic brake to 0.33, while the spool still needs about 0.63. Both fall short at the end, which is where a lot of backlashes start, and why your thumb is part of the braking system whether you like it or not. A headwind raises a_lure through the whole cast, which is the physics behind turning the brakes up on a windy day.

This toy model ignores line mass and the shrinking line pack, and the brake constants are illustrative. It’s still the clearest explanation I know of for why a reel that casts perfectly with a heavy jig birds-nests with a light, wind-resistant topwater.

The geometry of the strike zone

The single biggest lesson I took from tournament fishing is that boat position is a geometry problem. Two anglers can throw the same crankbait at the same point and one of them will have it in the zone for ten times as long.

Start with a bank that drops away at slope φ, and say the fish are holding between depths d₁ and d₂. The horizontal width of that band is:

w = (d₂ − d₁) / tan φ

Now cast at an angle α to the contour line, where α = 0 means parallel to the bank and α = 90° means straight out from it. A straight retrieve crosses the band along a path of length:

L_zone = min(L_cast, w / sin α)

Take an 8 to 12 foot band on a 20-degree slope. The band is 11 feet wide. Cast straight out at it with a 120-foot cast and your bait spends 11 feet of the retrieve in the zone. At 45 degrees it’s 15.5 feet. At 20 degrees, 32 feet. At 10 degrees, 63 feet. Parallel, all 120. That’s an elevenfold difference from nothing but where you put the boat.

It explains something I saw over and over in tournaments. The angler in the back of the boat often gets the worse angle because the front deck sets the boat position for the front. When I fished as a co-angler, the best thing I learned to do was cast at a sharper angle to the contour than the guy on the trolling motor, even if it meant throwing at water he’d already hit.

Crankbaits dive on a curve

A crankbait doesn’t reach its running depth at splashdown. It dives along a curve, holds near its maximum, and then the line angle lifts it as it comes back to the boat. A simple proposed model for depth after retrieving x feet, with s feet of line left, is:

d(x) = min( D (1 − e^(−x/λ)),  s · tan θ )

D is the bait’s maximum depth, λ is how quickly it gets there, and θ is the line angle near the boat that caps the depth on the way in. The values below (λ = 25 feet, θ = 30°) are illustrative. Plot your own bait against a sonar track or a bottom contour and refit them.

"""How much of a crankbait retrieve is spent in the depth band you want.
Proposed model, not a manufacturer dive curve."""
import math

import numpy as np

def dive_depth(dist_retrieved, dist_to_boat, D=10.0, lam=25.0, theta=30.0):
    """Depth (ft) after retrieving dist_retrieved ft with dist_to_boat ft left."""
    dive = D * (1 - np.exp(-dist_retrieved / lam))
    ceiling = dist_to_boat * math.tan(math.radians(theta))
    return np.minimum(dive, np.maximum(ceiling, 0))

def zone_length(d1, d2, phi_deg, alpha_deg, cast_len):
    """Horizontal path inside the band [d1, d2] for a straight cast."""
    width = (d2 - d1) / math.tan(math.radians(phi_deg))
    if alpha_deg == 0:
        return cast_len
    return min(cast_len, width / math.sin(math.radians(alpha_deg)))

def fraction_in_zone(D, cast_len, lam=25.0, band=1.5, step=0.5):
    """Fraction of a retrieve where the bait is within `band` ft of D."""
    xs = np.arange(0, cast_len, step)
    depth = dive_depth(xs, cast_len - xs, D=D, lam=lam)
    return float(np.mean(depth >= D - band))

if __name__ == "__main__":
    for a in (90, 45, 20, 10, 0):
        print(f"alpha {a:2d}: {zone_length(8, 12, 20, a, 120):6.1f} ft of 120")
    for L in (60, 90, 120, 150):
        print(f"cast {L:3d} ft: {fraction_in_zone(10, L)*100:4.0f}% near max depth")

For a bait that maxes out at 10 feet, the share of the retrieve spent within 1.5 feet of that depth is 0% on a 60-foot cast, 31% at 90 feet, 48% at 120 feet and 59% at 150 feet. A short cast with a deep crankbait is mostly a trip down and back up. That’s the quantitative case for long rods, light line and a full spool when you’re cranking, and it’s why I never felt bad about spending money on a rod that added 15 feet to a cast.

Line diameter changes D itself. In a 2000 study by Steven Holt and Mark Romanack, summarized by Bassmaster, 8-pound line made a crankbait dive about 20% deeper than 14-pound line, and 20-pound line ran about 10% shallower. A bait that runs 10 feet on 14-pound line runs about 12 feet on 8-pound and 9 feet on 20-pound. The same work found that every foot you push the rod tip under the surface adds roughly a foot of depth, which is the physics behind Paul Elias kneeling and reeling his way to the 1982 Bassmaster Classic title.

What a bass sees: Snell’s window, light and color

Light bends when it crosses from air into water. Snell’s law says n₁ sin θ₁ = n₂ sin θ₂, and with water’s refractive index near 1.333, light arriving at a grazing angle from the horizon bends down to a maximum of:

θ_c = arcsin(1 / 1.333) ≈ 48.6°

So a fish looking up sees the entire above-water world squeezed into a cone 97.2 degrees wide. That cone meets the surface in a circle called Snell’s window, with radius:

r = d · tan(48.6°) ≈ 1.13 d

A bass three feet down has a window about 6.8 feet across. At ten feet it’s about 22.7 feet. Outside the window the surface acts like a mirror, reflecting the bottom back down through total internal reflection.

The practical part is what happens near the edge. Everything between the horizon and the sky directly overhead is mapped into that 48.6-degree cone, and objects low on the horizon get crushed into a thin ring at the rim. Fresnel reflection also strips away more of the light arriving at a grazing angle. Put your head 8 feet above the water (standing on a front deck) at 40 feet from the fish. You sit 11.3 degrees above its horizon, you show up at 47.4 degrees from vertical, just inside the 48.6-degree rim, and about 70% of the light from you gets into the water. Kneel so your head is 4 feet up and you’re at 5.7 degrees of elevation and 46% transmission. Stay low at 80 feet and it’s 2.5 degrees and 24%.

That’s the physics behind every old-timer who told me to keep my profile low on shallow, clear fish. I believed them long before I could compute it.

"""Snell's window, Fresnel losses, and light at depth."""
import math

N_WATER = 1.333
FT = 0.3048

# Pope and Fry (1997), pure water absorption, converted to 1/m
A_WATER = {450: 0.00922, 500: 0.0204, 550: 0.0565,
           600: 0.2224, 650: 0.340, 700: 0.624}

def window_radius(depth, n=N_WATER):
    return depth * math.tan(math.asin(1 / n))

def fresnel_T(incidence_deg, n1=1.0, n2=N_WATER):
    """Unpolarized transmission from air into water."""
    ti = math.radians(incidence_deg)
    tt = math.asin(n1 * math.sin(ti) / n2)
    rs = ((n1 * math.cos(ti) - n2 * math.cos(tt)) /
          (n1 * math.cos(ti) + n2 * math.cos(tt))) ** 2
    rp = ((n1 * math.cos(tt) - n2 * math.cos(ti)) /
          (n1 * math.cos(tt) + n2 * math.cos(ti))) ** 2
    return 1 - 0.5 * (rs + rp)

def angler_in_window(height_ft, dist_ft):
    """Elevation above horizon, apparent angle from vertical, transmission."""
    elev = math.degrees(math.atan2(height_ft, dist_ft))
    incidence = 90 - elev
    apparent = math.degrees(math.asin(math.sin(math.radians(incidence)) / N_WATER))
    return elev, apparent, fresnel_T(incidence)

def light_fraction(depth_ft, secchi_ft):
    kd = 1.7 / (secchi_ft * FT)            # Poole-Atkins, 1/m
    return math.exp(-kd * depth_ft * FT)

if __name__ == "__main__":
    for d in (3, 10):
        print(f"depth {d} ft: window diameter {2*window_radius(d):.1f} ft")
    for h, D in ((8, 40), (4, 40), (3.5, 80)):
        e, a, T = angler_in_window(h, D)
        print(f"head {h} ft at {D} ft: elev {e:4.1f}, seen at {a:4.1f}, T={T:.2f}")
    for s in (2, 8):
        print(f"secchi {s} ft: {light_fraction(10, s)*100:.2f}% of light at 10 ft")
    z = 10 * FT
    for wl, a in A_WATER.items():
        print(f"{wl} nm: {math.exp(-a*z)*100:5.1f}% left after 10 ft of pure water")

How fast the light runs out

Below the surface, light falls off exponentially by the Beer-Lambert law, I(z) = I₀ e^(−K_d z), where K_d is the diffuse attenuation coefficient. Limnologists have a handy approximation that ties K_d to the Secchi disk reading every serious angler has done with a white lure: K_d ≈ 1.7 / Secchi depth (Poole and Atkins, examined by Idso and Gilbert in 1974).

Compare a lake with an 8-foot Secchi reading to a creek with a 2-foot reading. At 10 feet down, the clear lake still has about 11.9% of surface light. The stained creek has about 0.02%. That’s a factor of nearly 600 at the same depth, which is the strongest argument I know for changing baits, colors and depths when you run from the main lake into the back of a creek.

Color fades unevenly. Using Pope and Fry’s absorption measurements for pure water, 10 feet of path leaves about 97% of blue light at 450 nm but only 35.5% of red at 650 nm. After 30 feet, red at 650 nm is down to 4.5%. Real lakes add dissolved organic matter and algae, which absorb blue strongly, so stained water shifts the remaining light toward yellow and orange. Pure-water numbers are the best case for red, not the typical one.

What the bass does with that light

The best behavioral work I’ve read is Mitchem and colleagues’ 2019 study in Current Zoology. They found largemouth bass are dichromats, with green-sensitive single cones and red-sensitive twin cones. Trained bass readily told red and green apart from gray shades of similar brightness. They struggled to tell blue from black, and they had trouble telling green from blue. Because chartreuse stimulates both cone types about equally, it likely looks much like white to a bass.

Put that together with the attenuation numbers and the color question gets clearer. In shallow, clear water, a bass can distinguish red. Deep in clear water, red light is mostly gone and red baits read as dark. In stained water, contrast and silhouette matter more than hue, which matches the old advice to throw black and blue in dirty water even if the blue part is probably wasted on the fish.

When the light runs out, the lateral line takes over. A small vibrating source behaves roughly like a dipole, and dipole flow velocity falls off as 1/r³, so doubling the distance cuts the signal by a factor of 8. Research on lateral line sensing puts the range for locating a source at about one fish length. That’s why the lateral line matters in the last few feet of a strike, not for drawing fish from across a flat. Sound carries farther, but at that point you’re asking a different sense to do the work.

Fall rate: terminal velocity in a worm

A huge share of bass bites happen on the fall. The fall rate is set by a balance you can write on a napkin. Once a bait stops accelerating, its net weight in water equals the drag on it:

(m − ρ_w V) g = ½ ρ_w C_d A v_t²

v_t = √( 2 (m − ρ_w V) g / (ρ_w C_d A) )

Here m is the rig’s total mass, V its total volume, ρ_w the density of water (1,000 kg/m³) and C_d A the drag area. The term in parentheses is the part people forget. A soft plastic with density around 1.12 g/cm³ is almost weightless in water, so nearly all the sinking force comes from the hook and weight, while nearly all the drag comes from the plastic.

"""Terminal fall speed of a rig, and where it lands on a tight line.

Masses, densities and drag areas are assumptions for illustration.
Measure your own fall time and use cda_from_fall() to calibrate.
"""
import math

G = 9.81
RHO_W = 1000.0        # kg/m^3, fresh water
OZ_G = 28.3495        # grams per ounce
FT = 0.3048

def net_mass_g(parts):
    """parts: list of (mass_g, density_g_cc). Returns mass minus buoyancy (g)."""
    return sum(m - m / rho for m, rho in parts)

def terminal_velocity(net_g, cda_cm2):
    """Terminal speed (m/s) from net mass (g) and drag area Cd*A (cm^2)."""
    w = net_g / 1000 * G
    return math.sqrt(2 * w / (RHO_W * cda_cm2 * 1e-4))

def cda_from_fall(net_g, depth_ft, seconds):
    """Back out Cd*A (cm^2) from a timed fall, assuming terminal speed."""
    v = depth_ft * FT / seconds
    return 2 * net_g / 1000 * G / (RHO_W * v * v) * 1e4

def pendulum_landing(cast_ft, depth_ft, rod_h_ft=5.0):
    """Horizontal distance (ft) where a bait on a tight line reaches bottom."""
    inside = cast_ft**2 - depth_ft**2 - 2 * rod_h_ft * depth_ft
    return math.sqrt(inside) if inside > 0 else 0.0

WORM = (4.5, 1.12)          # 6 in soft plastic
HOOK = (0.5, 7.85)          # 3/0 steel hook
RIGS = {
    "weightless stick bait": ([(5.0, 1.15), HOOK], 22.0),
    "1/8 oz tungsten T-rig": ([WORM, HOOK, (OZ_G / 8, 19.3)], 2.5),
    "1/4 oz tungsten T-rig": ([WORM, HOOK, (OZ_G / 4, 19.3)], 2.5),
    "1/4 oz lead T-rig": ([WORM, HOOK, (OZ_G / 4, 11.3)], 2.8),
    "3/8 oz tungsten T-rig": ([WORM, HOOK, (OZ_G * 3 / 8, 19.3)], 2.5),
    "1/2 oz skirted jig + craw": ([(OZ_G / 2, 11.0), (6.0, 1.12)], 9.0),
}

if __name__ == "__main__":
    for name, (parts, cda) in RIGS.items():
        net = net_mass_g(parts)
        v = terminal_velocity(net, cda)
        print(f"{name:26s} net {net:5.2f} g  {v/FT:4.2f} ft/s  "
              f"12 ft in {12*FT/v:4.1f} s")
    for cast, depth in ((60, 15), (40, 15), (30, 20)):
        x = pendulum_landing(cast, depth)
        print(f"cast {cast} ft into {depth} ft: lands {x:4.1f} ft out, "
              f"{cast - x:4.1f} ft closer")

With my assumed drag areas, a weightless stick bait sinks at about 0.32 feet per second and takes 37 seconds to reach 12 feet. A 1/8-ounce tungsten Texas rig falls at 1.9 ft/s, a 1/4-ounce at 2.5 ft/s and a 3/8-ounce at 3.05 ft/s. A 1/2-ounce skirted jig with a craw trailer falls at about 1.8 ft/s, slower than the 1/8-ounce worm, because the skirt and trailer add far more drag than the extra lead adds weight. That matches what I’ve seen in clear water for years: a bulky jig parachutes.

The square root is the useful part. Going from 1/4 to 3/8 ounce adds 50% more weight but only about 20% more speed. If you want to change fall rate a lot, change the drag area (trailer, skirt, body shape) rather than the weight. Lead versus tungsten at the same weight is a smaller effect than people think, about 8% in this model, because the size difference changes buoyancy and drag only a little. Tungsten’s bigger advantages are sensitivity and a smaller profile in cover.

The drag areas are the weak point of this model, so measure your own. Drop a rig beside the boat on a slack line in 12 feet of water, time it to the bottom, and feed the time into cda_from_fall(). A 1/4-ounce tungsten rig that takes 4 seconds implies a drag area of about 1.8 cm².

The pendulum you didn’t plan on

On a slack line, a bait falls straight down. On a tight line, it swings back toward you on an arc whose radius is the line length from your rod tip. If you cast a distance X₀ with your rod tip h feet above the water, the bait reaches a bottom d feet deep at:

X = √( X₀² − d² − 2 h d )

Cast 60 feet into 15 feet of water with the rod tip 5 feet up and you land about 3 feet short of where you aimed. Pitch 30 feet into 20 feet of water and the bait lands 17.3 feet out, 12.7 feet closer than the splash. On a deep, short pitch to a dock post or a brush pile, a tight-line fall pulls your bait away from the target before it ever gets there. That’s the geometric reason to feed slack on the fall when the target is directly below the splash.

Line stretch, hooksets and the fight: two springs in series

I lost a lot of fish on long casts before I understood this section. Your rod and your line are both springs. When you set the hook, they stretch in series, and the hook only feels the tension that’s left after both have absorbed your sweep.

Model the line as a spring with compliance c, the strain per pound of tension, over length L. The rod has stiffness k_rod, in pounds per foot of tip deflection. For springs in series the compliances add, so a hookset that takes up s feet of line produces a peak tension of roughly:

T = s / ( c L + 1 / k_rod )

The line term grows with distance. The rod term doesn’t. That one line of algebra explains most of what anglers argue about with line choice.

The compliance numbers come from published bench tests. TackleTour loaded 2-foot samples with a constant 3 pounds and measured 9.4% stretch for dry Berkley Trilene XL monofilament. Fluorocarbons in the same test ranged from 6.3% to 13%, so the common claim that fluorocarbon barely stretches doesn’t hold across brands. Several fluorocarbons stretched more than the mono. At the breaking point, a Purdue study by Sarah Leach measured about 19% elongation for two nylon monofilaments and 17% for a fluorocarbon. Braided polyethylene is usually advertised near 3% at break, though one rod builder’s bench test found about 12%, which he attributed to the braid construction. I use the pessimistic 12% below.

"""Hookset tension from rod and line acting as springs in series.

Line compliance c is strain per pound of tension at low loads.
mono:  ~9.4% stretch at 3 lb (TackleTour, Trilene XL, dry)
fluoro: 6.3% to 13% at 3 lb across brands (same test)
braid: ~3% at break is the usual claim; one bench test found ~12%
Rod stiffness k_rod is an assumption; measure yours (see text).
"""

LINES = {
    "mono 15 lb": 0.094 / 3,
    "fluoro 15 lb (stiff)": 0.063 / 3,
    "fluoro 15 lb (soft)": 0.13 / 3,
    "braid 50 lb": 0.12 / 50,     # pessimistic: 12% at break
}

def hookset_tension(sweep_ft, line_ft, c_per_lb, k_rod_lb_ft=None, drag_lb=None):
    """Peak tension (lb) at the hook for a sweep that moves sweep_ft of line."""
    compliance = c_per_lb * line_ft                 # ft of stretch per lb
    if k_rod_lb_ft:
        compliance += 1 / k_rod_lb_ft               # springs in series
    t = sweep_ft / compliance
    return min(t, drag_lb) if drag_lb else t

if __name__ == "__main__":
    sweep, k_rod = 4.0, 1.5
    print(f"{'line':22s}" + "".join(f"{L:>8d} ft" for L in (20, 40, 60, 80)))
    for name, c in LINES.items():
        row = [hookset_tension(sweep, L, c, k_rod) for L in (20, 40, 60, 80)]
        print(f"{name:22s}" + "".join(f"{t:8.1f} lb" for t in row))

With a 4-foot sweep and a rod that deflects 8 inches per pound (k_rod = 1.5 lb/ft), 15-pound mono delivers about 3.1 pounds at 20 feet, 2.1 at 40 and 1.6 at 60. A soft fluorocarbon does worse, 1.2 pounds at 60 feet. Braid delivers 5.6 pounds at 20 feet and still 4.9 pounds at 60, because its stretch barely registers next to the rod’s.

This quasi-static model underestimates real peaks, since a snap hookset adds dynamic load. In a 2022 field test for Texas Fish & Game, Lenny Rudow measured hookset force with 15-pound line: 7 pounds for braid versus 5 for mono at 50 feet, and 7 versus 4 at 100 feet. The absolute numbers differ from mine, but the shape is the same: mono loses hookset force with distance and braid mostly doesn’t.

The other way to read the formula is that with braid, the rod becomes the spring that sets your hookset. Line choice and rod action are one decision, not two. A soft-tipped moving-bait rod paired with braid behaves a lot like a stiff rod with mono. That’s the logic behind pairing braid with a rod that has some give, and stretchier line with a faster, stiffer rod.

Measure your own k_rod. Clamp the rod handle level, hang a 1-pound weight from the tip, and measure how far the tip drops. One pound divided by that deflection in feet is k_rod. It’s a two-minute test and it tells you more about a rod than the action label does.

Why fish come off at the boat

Run the same arithmetic at the end of the fight. With 10 feet of braid out, the line stretches about 0.3 inches per pound of tension while the rod gives 8 inches. The rod is doing about 96% of the cushioning. Point the rod at a fish beside the boat and you’ve removed nearly all the spring from the system, so every head shake goes straight to the hook. Keep the rod bent and your hands soft near the boat, especially on braid. Most of the big fish I’ve lost at the boat came off for exactly this reason.

The drag is the last spring. Salt Water Sportsman’s guidance for monofilament up to 20-pound test is a drag set to about 20% of breaking strength, and 15% for braid of the same class. That advice is built for long runs offshore, and bass anglers fishing heavy cover often run much tighter drags to win the first two seconds. Either way, the drag caps T in the formula above, so a loose drag silently costs you hookset force.

Sonar is a cone, and fish are hyperbolas

A traditional transducer sends a pulse down a cone. The width of what it sees on the bottom is plain trigonometry:

footprint = 2 d tan(θ / 2)

With a 20-degree cone, the footprint is 3.5 feet across at 10 feet of depth, 7.1 feet at 20 and 14.1 feet at 40. A 60-degree cone covers 23.1 feet at 20 feet down. When I was idling a ledge looking for a school, that number told me how many passes I needed to actually cover it, and it told me that a narrow cone in shallow water can idle right past fish ten feet off the boat.

Vertical resolution comes from the pulse length τ. Two targets closer together in range than:

Δr = c τ / 2

blur into one, where c is the speed of sound in fresh water, about 1,482 m/s near 20°C. A 200-microsecond pulse gives about 5.8 inches, which is why a bass hugging the bottom can disappear into the bottom return.

The classic fish arch is geometry too. As the boat passes over a fish at depth z, the range from the transducer is:

r(t) = √( z² + (v t)² )

That’s a hyperbola. The fish looks deeper at the edges of the cone than directly underneath, so the screen draws an arch. The arch only lasts while the fish is inside the cone, which works out to a time of 2 z tan(θ/2) / v.

"""Sonar cone footprint, range resolution, and why fish show up as arches."""
import math

C_FRESH = 1482.0      # m/s, fresh water near 20 C
FT = 0.3048

def footprint_ft(depth_ft, cone_deg):
    return 2 * depth_ft * math.tan(math.radians(cone_deg / 2))

def range_resolution_in(pulse_us, c=C_FRESH):
    return c * pulse_us * 1e-6 / 2 / FT * 12

def time_in_beam_s(depth_ft, cone_deg, boat_mph):
    v = boat_mph * 5280 / 3600                     # ft/s
    return footprint_ft(depth_ft, cone_deg) / v

def arch_sag_ft(depth_ft, cone_deg):
    """How much deeper the fish reads at the cone edge than dead center."""
    return depth_ft / math.cos(math.radians(cone_deg / 2)) - depth_ft

if __name__ == "__main__":
    for d in (10, 20, 40):
        print(f"{d} ft: 20 deg {footprint_ft(d, 20):5.1f} ft, "
              f"60 deg {footprint_ft(d, 60):5.1f} ft")
    print(f"200 us pulse: {range_resolution_in(200):.1f} in")
    for cone in (20, 60):
        print(f"fish at 20 ft, {cone} deg, 3 mph: "
              f"{time_in_beam_s(20, cone, 3.0):.1f} s in beam, "
              f"sag {arch_sag_ft(20, cone):.2f} ft")

A fish at 20 feet passing under a 20-degree cone at 3 mph is in the beam for about 1.6 seconds and reads only 0.31 feet deeper at the edges. The arch is nearly flat. Under a 60-degree cone it’s in the beam for 5.2 seconds and sags about 3.1 feet, a clear arch. Narrow cones give you crisp bottom detail and flat fish. Wide cones give you fat arches and less certainty about where the fish actually is. A partial arch usually means a fish crossed the edge of the cone rather than the middle, or the boat was moving too fast to paint the whole curve.

Forward-facing live sonar uses the same physics with a beam that’s narrow in one direction and wide in the other, pointed ahead instead of down. It turned a lot of fish-finding from probability into something close to aiming, which is why tournament organizations now regulate it. For the 2026 Elite Series, B.A.S.S. allowed it at five of nine regular-season events, with one transducer and a 55-inch total screen limit. MLF limited most of its 2026 circuits to a single 3-hour live-sonar window per day, and the Bass Pro Tour to one declared competition period per day. Check your own circuit’s current rules, since they have changed every season lately.

Tournament math: five fish, order statistics and the big bite

A tournament doesn’t score how many fish you catch. It scores the sum of your five heaviest. That turns a fishing day into a problem in order statistics, and it changes which decisions are right.

Start with how length becomes weight. Fisheries scientists use a standard weight equation for largemouth bass, which Anderson and Neumann list in the American Fisheries Society’s Fisheries Techniques:

log₁₀(W_s) = −5.528 + 3.273 · log₁₀(TL)

W_s is in grams and total length TL is in millimeters. The exponent matters. Weight grows with length to the 3.27 power, faster than a cube. A 12-inch bass is about 0.88 pounds, a 15-inch fish 1.83, an 18-inch fish 3.33, a 20-inch fish 4.69 and a 22-inch fish 6.41. Going from 20 to 22 inches, a 10% gain in length, adds about 37% in weight. Those values match the published length-weight tables I checked, which is a nice sanity test on the coefficients.

Now simulate. In my proposed model, the number of keepers you catch in a day is Poisson, each keeper’s length is the minimum length plus an exponential excess, and each fish’s condition varies about 8% around the standard weight. Then sort and keep the top five.

"""Five-fish limit math: length-weight, culling and strategy variance.

Standard weight for largemouth bass (metric):
    log10(Ws_g) = -5.528 + 3.273 * log10(TL_mm)
Keeper lengths and bite rates below are a proposed model, not survey data.
"""
import numpy as np

G_PER_LB = 453.592
MM_PER_IN = 25.4

def standard_weight_lb(length_in):
    tl = np.asarray(length_in) * MM_PER_IN
    return 10 ** (-5.528 + 3.273 * np.log10(tl)) / G_PER_LB

def simulate_day(rng, mean_keepers, mean_excess_in, min_len=12.0,
                 limit=5, n_days=200_000):
    """Return limit weights (lb) for n_days simulated tournament days."""
    counts = rng.poisson(mean_keepers, n_days)
    max_n = counts.max()
    lengths = min_len + rng.exponential(mean_excess_in, (n_days, max_n))
    condition = rng.normal(1.0, 0.08, (n_days, max_n))     # relative weight
    weights = standard_weight_lb(lengths) * condition
    mask = np.arange(max_n)[None, :] < counts[:, None]
    weights = np.where(mask, weights, 0.0)
    top = -np.sort(-weights, axis=1)[:, :limit]
    totals = top.sum(axis=1)
    return totals, counts

def expected_limit_given_n(rng, n, mean_excess_in, reps=100_000, min_len=12.0):
    lengths = min_len + rng.exponential(mean_excess_in, (reps, n))
    w = standard_weight_lb(lengths) * rng.normal(1.0, 0.08, (reps, n))
    return np.sort(w, axis=1)[:, -5:].sum(axis=1).mean()

if __name__ == "__main__":
    rng = np.random.default_rng(7)
    for L in (12, 15, 18, 20, 22, 24):
        print(f"{L} in: {float(standard_weight_lb(L)):5.2f} lb")
    print("expected limit vs keepers caught (mean excess 2 in):")
    for n in (5, 10, 20, 30):
        e = expected_limit_given_n(rng, n, 2.0)
        print(f"  n={n:2d}: {e:5.2f} lb")
    for name, nk, ex in (("numbers", 14, 1.75), ("big fish", 4.5, 3.0)):
        tot, cnt = simulate_day(rng, nk, ex)
        print(f"{name:9s} mean {tot.mean():5.2f}  median {np.median(tot):5.2f}  "
              f"sd {tot.std():4.2f}  P(limit) {np.mean(cnt >= 5):.2f}  "
              + "  ".join(f"P(>={t}) {np.mean(tot >= t):.3f}" for t in (10, 15, 18)))

Culling has steep diminishing returns

With a 12-inch minimum and an average of 2 inches over it, catching exactly five keepers gives an expected limit of 7.92 pounds. Ten keepers gives 10.53. Twenty gives 13.74. Thirty gives 15.94. Doubling from 10 to 20 keepers adds about 3.2 pounds, and the next 10 add about 2.2. Each new fish only helps if it beats the smallest one in the livewell, and the smallest one keeps getting bigger.

Compare that with one kicker. Add a single 22-inch, 6.4-pound fish to a 10-keeper day and the expected limit rises by about 5.0 pounds, because it replaces a fish that averages about 1.4 pounds. One big bite is worth roughly what 20 extra keepers are worth. Every tournament angler knows this in their gut. The math says the gut is right, and by a lot.

Variance is a strategy, not just bad luck

The interesting decision is where to fish. Model two spots. The numbers spot gives an average of 14 keepers with lengths averaging 1.75 inches over the minimum. The big-fish spot gives only 4.5 keepers on average, but they average 3 inches over.

The numbers spot produces a mean limit of 10.61 pounds with a standard deviation of 2.84, and you fill your limit essentially every time. The big-fish spot averages only 9.01 pounds with a standard deviation of 5.75, and you fill a limit just 47% of the time. By expected weight, the numbers spot wins easily.

But nobody gets paid on expected weight. The chance of weighing at least 10 pounds is 52% on the numbers spot and 34% on the big-fish spot. The chance of 15 pounds or more flips: 7.2% versus 11.8%. At 18 pounds it’s 2.0% versus 6.3%, so the big-fish spot triples your odds of a heavy bag.

So the right spot depends on what it takes to win and where you stand. If 10 pounds cashes a check and you need the points, grind the numbers. If it takes 18 to win and you’re there to win, the high-variance water is the rational play even though its average is lower. I made that decision wrong more than once by fishing for a respectable bag when the leaderboard said I needed a long shot.

Where these models break

I want to be straight about the limits, because a fishing article with equations can sound more certain than it is.

The cast model uses a lumped line-drag term I calibrated rather than measured, and it treats the lure as a point that doesn’t tumble. The backlash model ignores line mass and the shrinking spool radius, both of which matter on a full spool. The fall-rate numbers depend on drag areas I assumed, which is why I gave you a function to measure your own. The hookset model is quasi-static, so it understates the peak force of a fast sweep, and it ignores the line bow and slack that eat most of a hookset on a windy day. Nylon also isn’t a perfectly linear spring, and its stretch changes when it soaks up water.

The vision section uses pure-water absorption, which is the best case. Real lakes carry tannins, sediment and algae that change both the amount and the color of light, sometimes by the hour. Mitchem’s study tested trained bass in tanks, which tells us what a bass can discriminate, not what makes one bite. Nobody has measured the terms in my catch-rate chain for a real lake, and they aren’t independent. A bait that’s easy to detect may also be easy to reject.

The tournament simulation is a toy. Real keeper lengths aren’t exponential, real bites cluster in feeding windows rather than arriving as a Poisson process, and real fish move. The ranking of the two strategies is robust to the details. The exact probabilities are not.

None of this replaces time on the water. The best anglers I fished against had solved most of these problems by feel. The math is a way to explain what they already knew, and to catch the cases where feel and physics disagree.

What I’d do with this next season

Measure your gear once. Hang a 1-pound weight from each rod tip and record k_rod. Time your favorite Texas rigs and jigs falling to the bottom in a known depth and back out their drag areas. Ten minutes of measurement turns the models above from illustrations into tools.

Think about angles before you cast. Before you start a stretch of bank, estimate the slope and the depth band the fish are using, and set the boat so your casts run as close to parallel with that band as you can. If you’re in the back of the boat, take the sharper angle even if the water has been hit.

Match line and rod as one spring. If you’re making long casts with mono or soft fluorocarbon, use a rod with enough backbone to make up for the line’s stretch, or switch to braid with a leader. If you’re on braid, keep the rod bent at the boat.

Choose variance on purpose. Before a tournament day, decide what weight it will take to accomplish what you came for, whether that’s a check, points or a win. Then pick water whose distribution of outcomes gives you the best chance of clearing that number, not the best average.

All the scripts above run with Python 3, NumPy and SciPy. Change the assumptions to match your own water and gear, and you’ll learn more from the places the model is wrong than from the places it’s right.

About the Author

Brian James Curry is a Kansas City AI researcher, applied scientist and data scientist with more than twenty years in marketing modeling and measurement. His work today spans marketing mix modeling, causal inference, machine learning and healthcare intelligence. He is the founder of Vector1 Labs, an independent applied AI research lab. His marketing measurement work covers marketing mix modeling, incrementality testing and budget allocation, and his Causal Care series applies causal machine learning to health system decisions: which programs work, for whom, and what they are worth. He also founded KC AI Lab, which began in 2016 as the Kansas City Machine Learning Group and has grown to more than 1,500 members.

His career includes leading analytics, data science and machine learning at Hallmark, Garmin, Koch Industries, Tractor Supply Company and Vail Resorts. His open-source research tools on GitHub include Papilon (marketing mix modeling and complex systems), PyCausalSim (causal discovery through simulation) and tincast (forecasting for AI-critical commodities), along with MeaningFlow and Daedalus. A Kansas State University alum from a small Kansas farming town, he writes about marketing mix modeling, causal inference, agentic AI, healthcare AI and the economics of AI on Medium and Substack. Contact: brian@vector1.ai.

Sources

  1. Mitchem, L. D., Stanis, S., Zhou, M., Loew, E., Epifanio, J. M. and Fuller, R. C. (2019). Seeing red: color vision in the largemouth bass. Current Zoology 65(1): 43 to 52.
  2. Neumann, R. (2019). Another Look at Bass Vision. In-Fisherman.
  3. Pope, R. M. and Fry, E. S. (1997). Absorption spectrum (380 to 700 nm) of pure water. II. Integrating cavity measurements. Applied Optics 36: 8710 to 8723. Data via Oregon Medical Laser Center.
  4. Idso, S. B. and Gilbert, R. G. (1974). On the universality of the Poole and Atkins Secchi disk light extinction equation. Journal of Applied Ecology 11(1): 399 to 401. Summarized at Wikipedia: Secchi disk.
  5. Snell’s window. Wikipedia.
  6. van Hemmen, J. L. Lateral-Line Detection of Underwater Objects: From Goldfish to Submarines. American Physical Society March Meeting, 2010.
  7. Duke, K. (2012). How deep can you go? Bassmaster. Summarizes Holt and Romanack’s 2000 crankbait depth work.
  8. Leach, S. E. (2004). Fishing for the Best Line: Evaluating Polymers used for Sport Fishing. Proceedings of the ASEE Annual Conference.
  9. TackleTour (2007). Fluorocarbon line review, stretch testing.
  10. Shimadzu. Strength Testing of Various Fishing Lines. Application note.
  11. Rod House. Fishing braid. Bench test of braid elongation.
  12. Rudow, L. (2022). The Truth about Braid vs. Mono Line Stretch. Texas Fish & Game.
  13. Poveromo, G. (2019). Set a Drag in a Snap. Salt Water Sportsman.
  14. Anderson, R. O. and Neumann, R. M. (1996). Length, weight, and associated structural indices. In Murphy, B. R. and Willis, D. W. (eds.), Fisheries Techniques, 2nd ed. American Fisheries Society. See also Wikipedia: Standard weight in fish.
  15. Wright, R. A. (2000). Relative Weight: An Easy to Measure Index of Fish Condition. Alabama Cooperative Extension System, ANR-1193.
  16. In-Fisherman (2014). Fish Weight Estimator: Largemouth Bass.
  17. Albanese, J. (2025). B.A.S.S. Announces Major Forward-Facing Sonar Rule Change for 2026 Elite Series. Wired2Fish.
  18. Western Bass. MLF Announces 2026 Forward-Facing Sonar Restrictions Across All Circuits.

Related research