Chapter I
The Pressure-Equalization Loop
Surface station. 0000 metres. You are still in air.
You begin at the surface, where the platform still rocks against the swell and the firmware has not yet been told that it is about to go somewhere cold. Before you write a single instruction for the sonar, you will configure the pressure-equalization loop, because everything downstream — every coherent return, every phase estimate, every depth fix — assumes the hull is breathing correctly. Read this section twice.
The pressure-equalization loop is not a control loop in the textbook sense. It does not stabilise a setpoint. It tracks an external scalar — ambient pressure — and adjusts an internal reservoir so that the strain on the acoustic windows stays within the elastic envelope of the borosilicate. When the platform descends, ambient pressure rises roughly one bar every ten metres. The loop must keep up. If it lags, the windows flex; if the windows flex, the transducer baseline shifts; if the baseline shifts, the sonar phase you so carefully locked in Chapter III drifts out from under you. So we start here.†
Consider the platform as three coupled volumes. There is the wet volume, open to the sea, which simply is whatever pressure the ocean says it is. There is the dry volume, the instrument bay, which you would like to hold near one atmosphere so the boards do not complain. And there is the reservoir, a compressible bladder of inert gas whose job is to absorb the difference. The loop reads a differential transducer across the dry-to-wet boundary and meters gas in or out of the reservoir to null it. That is the whole idea. The difficulty is entirely in the timing.
Here is the timing. A dive profile is roughly piecewise-linear: a fast descent leg, a slow survey leg at depth, a fast ascent. During the descent leg the differential transducer sees a ramp, and a naive proportional response will always trail it by a fixed offset proportional to the descent rate. You can shrink the offset with integral action, but integral action on a ramp that ends abruptly — when the platform levels off at survey depth — produces an overshoot, and an overshoot here means the dry volume briefly goes below one atmosphere, which means the windows bow inward, which is the same disaster in the opposite direction. So you cannot simply tune harder.‡
What you do instead is feed the loop the descent rate directly. The platform already estimates its vertical velocity from the depth sensor; that estimate is the feedforward term. You compute the gas flow that the ramp demands, you apply it open-loop, and you let the feedback loop handle only the residual — sensor noise, bladder nonlinearity, the slow creep of temperature. With the feedforward in place the feedback term is small, the integral never winds up, and the level-off is graceful. This is the single most important design decision in the chapter. Everything else is bookkeeping.
The bookkeeping, briefly. The differential transducer is itself pressure-sensitive in its zero — a second-order effect, but at trench depths the absolute pressure is four hundred bar and a part-per-thousand zero shift is meaningful — so you carry a calibration polynomial indexed by absolute depth and you subtract it before the loop ever sees the signal. The gas valves have a minimum on-time below which they do not actually open, so you accumulate small demands into a delta-sigma-like dither rather than chattering. And the reservoir bladder has a knee in its compliance curve near the bottom of its travel, so you reserve the last fifteen per cent of reservoir volume as an untouchable margin and you alarm if the loop tries to use it. None of this is glamorous. All of it is load-bearing.
Before we move on, internalise the failure mode, because you will see its signature later. If the equalization loop is lagging, the acoustic windows are under cyclic strain at the dive-profile timescale — tens of minutes — and that strain modulates the transducer mounting geometry, and that modulation appears in the sonar data as a slow phase wander correlated with descent rate. When you are debugging a phase-lock failure in Chapter IV and the wander tracks your dive log, come back here. The bug is not in the sonar. The bug is that the hull is not breathing.
/* pressure-equalization loop — runs once per 50 ms tick */
float eq_step(eq_state_t *s, float p_dry, float p_wet, float v_z) {
/* feedforward: gas flow demanded by the descent ramp */
float ff = K_RAMP * v_z; /* v_z in m/s, descending positive */
/* differential, with depth-indexed zero correction */
float dp = (p_wet - p_dry) - zero_poly(s->depth_m);
/* feedback handles only the residual */
s->integ += dp * s->ki; /* tiny; ff carries the load */
s->integ = clampf(s->integ, -I_LIM, I_LIM);
float fb = dp * s->kp + s->integ;
float demand = ff + fb; /* desired gas flow, sccm */
/* reservoir margin guard */
if (s->reservoir_frac < 0.15f && demand > 0.0f)
eq_alarm(EQ_RESERVOIR_LOW);
/* delta-sigma dither: never chatter the valves */
s->accum += demand;
int pulse = (int)(s->accum / VALVE_MIN_STEP);
s->accum -= pulse * VALVE_MIN_STEP;
valve_meter(pulse);
return demand;
}
Chapter II
Sonar Returns in a Moving Frame
Photic descent. 0000 to 0900 metres. The light is leaving.
Now the platform is sinking and the sonar is on. You will spend this chapter establishing what a "return" even means when the receiver is in motion, because the moving-frame correction is the difference between a sonar that maps the trench floor and a sonar that maps a smear of the trench floor.
A sonar return is, at heart, an echo: you emit a pulse, it scatters off something, a delayed and attenuated copy comes back. In a stationary frame the round-trip delay gives you range and that is the end of the story. But the platform is descending at perhaps half a metre per second, which over a two-second round trip is one metre of displacement, which at a carrier of two hundred kilohertz is many wavelengths. The transmit happened at one position; the receive happens at another. If you pretend they happened at the same place, every range is wrong by a slowly varying bias and — worse — the phase you need in Chapter III is scrambled.
The correction has two parts, and you must do both. The first is geometric: the true range is half the round-trip path length, but the path is not a there-and-back along one ray; it is a there along one ray and a back along a slightly different one, because the receiver moved. For a vertical descent and a roughly vertical look angle this is a small triangle and you can solve it in closed form from the descent rate and the round-trip time. Do it per ping. Do not approximate it as constant; the descent rate changes across the dive profile and so does the correction.§
The second part is Doppler. The descent imposes a frequency shift on the return — compressive on the way down toward a target below — and that shift, if uncorrected, walks the matched filter off its peak and degrades the range resolution. You know the descent rate, so you know the shift, so you de-rotate the baseband samples by the corresponding linear phase before matched filtering. This is cheap. It is a complex multiply per sample by a precomputed chirp. Skipping it is the most common reason a moving-platform sonar "loses resolution at speed" — it has not lost anything; it is matched-filtering against the wrong template.
There is a subtlety about which velocity to use. The descent rate from the depth sensor is the velocity of the hull. But the transducer is mounted on the lower hull, which — recall Chapter I — is flexing under the equalization loop's residual error. The velocity that matters for the Doppler correction is the velocity of the transducer face, which is the hull velocity plus the time-derivative of the window flex. In a well-tuned platform this correction is negligible. In a platform whose equalization loop is lagging, it is not, and it appears as exactly the descent-rate-correlated phase wander promised in Chapter I. We will collect on that promise in Chapter IV.
Implement the moving-frame correction as a preprocessing stage that owns the raw baseband stream and emits a "stabilised" stream as if from a stationary receiver at the ping's transmit position. Everything downstream — matched filter, beamformer, phase estimator — then gets to pretend the platform is not moving. This is a clean architectural seam and you should defend it. Resist the temptation to thread descent rate into the beamformer "for efficiency". The seam is worth more than the cycles.
One operational note. The moving-frame correction depends on the depth-sensor velocity estimate, and that estimate is itself filtered, and the filter has lag. During the rapid descent leg the velocity estimate trails the truth, so the correction is slightly stale, so range has a small transient bias at the top of every dive. This is known, it is small, and it is annotated in every dataset. Do not "fix" it by reducing the velocity filter's lag; you will let depth-sensor noise into the range solution, which is a worse trade. Leave it. Annotate it. Move on.
# stabilise a baseband ping to the transmit-position frame
import numpy as np
def stabilise(ping, fs, fc, v_z, rtt):
"""ping: complex baseband samples; v_z: descent rate (m/s);
rtt: round-trip time (s); returns stabilised samples + range bias."""
c = 1500.0 # sound speed, m/s
# --- Doppler de-rotation: remove the descent-induced linear phase
doppler_hz = 2.0 * v_z * fc / c # compressive, target below
n = np.arange(ping.size)
ping = ping * np.exp(-1j * 2*np.pi * doppler_hz * n / fs)
# --- geometric range bias for the vertical-look case
range_bias_m = v_z * rtt / 4.0 # signed: positive => reads short
return ping, range_bias_m
# downstream code may now pretend the receiver was stationary.
Chapter III
The Phase-Lock Condition
Mesopelagic dark. 0900 to 2100 metres. No daylight reaches here.
This is the chapter the volume is named for. You will derive the condition under which successive sonar returns are phase-coherent, you will see why coherence buys you an order of magnitude in along-track resolution, and you will learn the three ways it breaks. By the end you should be able to look at a phase-history plot and say, within a minute, whether the platform is locked.
Coherent processing means combining many returns by adding their complex amplitudes rather than their magnitudes. If the returns are in phase, the amplitudes add constructively and the signal grows linearly with the number of pings while the noise grows as a square root — the familiar coherent gain. But "in phase" is a strong requirement. It means that the phase you would predict for ping number k, from pure geometry — platform position, target position, carrier wavelength — matches the phase you actually measure, to within a small fraction of a cycle, for every k in the integration window. That predicted-equals-measured equality is the phase-lock condition.
Write it out. The predicted phase for ping k is minus two pi over lambda times twice the slant range from the platform's position at ping k to the target. The measured phase is whatever the receiver reports after the moving-frame correction of Chapter II. The phase-lock condition is that the difference between these two — the phase residual — is, across the window, a constant plus possibly a linear trend that you can estimate and remove. Constant: fine, that is just an unknown reference phase. Linear: fine, that is just a target you have slightly mis-located along track, and the slope tells you the correction. Anything beyond linear — curvature, jitter, jumps — is loss of lock, and the coherent gain collapses toward the incoherent one.†
So the algorithm is: form the phase residual versus ping index, fit a line, look at what is left. The residual after the line fit is your lock-quality metric. If its standard deviation is below, say, an eighth of a cycle, you are locked and you may integrate the whole window coherently. If it is above, you are not, and you fall back to shorter coherent sub-windows combined incoherently — graceful degradation rather than a cliff. This fallback is not optional. The trench floor has patches where the medium itself decorrelates the signal and no amount of platform stability will save you; the algorithm must notice and adapt.
The three ways lock breaks. First, platform motion you failed to correct: residual descent-rate-correlated wander from a lagging equalization loop, exactly the thread from Chapters I and II. Its signature is a phase residual whose curvature tracks your dive profile. Second, medium decorrelation: turbulence, thermohaline microstructure, suspended sediment — the propagation path literally changes between pings. Its signature is fast, random, and uncorrelated with anything in your platform logs. Third, target motion: drifting sediment plumes, biological scatterers. Its signature is coherent over short windows and walks away over long ones, and it is the hardest to distinguish from a navigation error because it looks like a target you mislocated, except the "correction" keeps changing.
Practical advice for the lock-quality plot. Plot the post-fit residual in cycles, not radians — your eye calibrates to "an eighth of a cycle is the threshold" much faster than to "0.79 radians". Overlay the dive-profile velocity on the same time axis, scaled to fit; if the residual envelope hugs the velocity curve, your bug is in Chapter I, not here. And keep the line-fit coefficients in the metadata: the slope is a navigation correction you paid for, and throwing it away means re-deriving it every time someone reprocesses the dataset.
Internalise this: phase-lock is not a property of the sonar. It is a property of the whole platform — hull, equalization loop, navigation filter, medium — observed through the sonar. When it fails, the sonar is the messenger. Chapter IV is about reading the message.
# phase-lock check: fit a line to the phase residual, report quality
import numpy as np
def lock_quality(predicted_phase, measured_phase):
resid = np.unwrap(measured_phase - predicted_phase)
k = np.arange(resid.size)
# estimate constant + linear trend (the "allowed" part)
slope, intercept = np.polyfit(k, resid, 1)
detrended = resid - (slope * k + intercept)
rms_cycles = np.std(detrended) / (2.0 * np.pi)
locked = rms_cycles < 0.125 # eighth-of-a-cycle threshold
return {
"locked": bool(locked),
"rms_cycles": float(rms_cycles),
"nav_slope_rad_per_ping": float(slope), # keep this in metadata!
}
# if not locked: split the window, integrate sub-windows incoherently.
Chapter IV
Debugging a Phase-Lock Failure
Bathyal cold. 2100 to 3200 metres. The water is near freezing.
A field dive came back with collapsed along-track resolution over the last third of the survey leg. You have the raw stream, the dive log, the equalization telemetry, and the navigation solution. You will walk through the diagnosis the way you should walk through every one of these: from the symptom inward, never trusting any layer until it has been ruled out.
Start with the lock-quality plot from Chapter III. The post-fit phase residual is flat and small for the first two-thirds of the leg — locked — and then its envelope swells, smoothly, over a few minutes, to several cycles. Not jumps. Not noise. A smooth swell. Already this tells you something: smooth means it is not medium decorrelation, which is fast and random. Smooth means it is platform or navigation. Two suspects remain.
Overlay the dive-profile velocity. The residual envelope hugs it. Where the descent rate is highest, the residual is worst; where the platform levels off briefly mid-leg, the residual relaxes. This is the Chapter I signature, stated three chapters ago and now collected: the equalization loop is lagging, the acoustic windows are under cyclic strain, the transducer baseline is wandering with descent rate, and the wander is corrupting the phase. You did not even need the equalization telemetry yet. The shape told you.
Now confirm it with the equalization telemetry, because "the shape told you" is a hypothesis, not a finding. Pull the differential-pressure error from the eq loop over the same window. It is non-zero and it tracks descent rate — the feedforward term is undersized. Check the reservoir fraction: it dipped near the fifteen per cent margin during the steep leg, which means the loop ran out of authority exactly when it needed it most, which means the feedforward gain K_RAMP was set for a gentler dive profile than this dive actually flew. There is your root cause: a parameter, not a bug. The firmware did exactly what it was told. It was told wrong.§
Before you celebrate, falsify the alternative. Could this be a navigation error masquerading as platform flex? A navigation error would show up as a phase residual that the line fit mostly absorbs — a slope, not a swelling envelope — and the leftover would be small. Here the leftover is large and structured. Could it be target motion? Target motion decorrelates over long windows but is coherent over short ones, and crucially it is not correlated with your descent rate; this is. Both alternatives fail their tests. The equalization explanation passes all of them. You are done diagnosing.
The fix is in three places, in increasing order of permanence. Immediately: reprocess this dataset with a moving-frame correction that uses transducer-face velocity — hull velocity plus the time-derivative of the window flex inferred from the eq error — and recover most of the lost coherence offline. Soon: re-tune K_RAMP for the steeper dive profiles the science team has started flying, and add the dive-profile aggressiveness to the pre-dive checklist so the parameter and the plan cannot drift apart silently. Eventually: make the equalization loop adapt its feedforward gain online from the observed descent rate, so it cannot be mis-parameterised for a profile it has never seen. The first fix recovers this dataset. The third fix prevents the next one.
The lesson, which is the lesson of the whole volume: the bug was reported in the sonar, found in the firmware, and rooted in a parameter set by a human who did not know the dive plan had changed. Three layers down from the symptom. Every layer was behaving correctly given its inputs. Debugging deep systems is the discipline of refusing to stop at the first layer that looks guilty.
# correlate phase-residual envelope against dive-profile velocity
import numpy as np
def diagnose(resid_envelope, descent_rate, eq_error, reservoir_frac):
# 1. smooth swell vs fast noise? (rough roughness measure)
roughness = np.std(np.diff(resid_envelope)) / (np.std(resid_envelope) + 1e-9)
smooth = roughness < 0.3
# 2. does the envelope track descent rate?
r = np.corrcoef(resid_envelope, np.abs(descent_rate))[0, 1]
tracks_velocity = r > 0.7
# 3. did the equalization loop run out of authority?
eq_lagging = np.corrcoef(eq_error, np.abs(descent_rate))[0, 1] > 0.7
ran_out = np.min(reservoir_frac) < 0.16
if smooth and tracks_velocity and eq_lagging:
return "ROOT CAUSE: equalization feedforward undersized for dive profile" \
+ (" (reservoir hit margin)" if ran_out else "")
return "inconclusive — keep digging"
Chapter V
The Trench-Floor Reverberation Model
Hadal trench. 3200 to 3940 metres. The bottom of the chronicle.
You have arrived at the floor. The last task of this volume is to validate everything above against the trench-floor reverberation model — a synthetic environment whose statistics you control — so that when the platform finally images a real trench you can tell a genuine feature from an artefact of your own processing. We close where every careful chronicle closes: with verification.
The reverberation model is a stochastic scattering field. You specify a bathymetry — gentle slopes, the occasional outcrop, a sediment basin — and a scattering strength per facet, and the model returns, for any platform trajectory you give it, the baseband sonar stream that a perfect receiver would record. It includes the moving-frame geometry of Chapter II, so the stabilisation stage has something real to undo. It includes optional medium decorrelation with a tunable timescale, so the phase-lock fallback of Chapter III gets exercised. It does not include hull flex; that you inject yourself, from a chosen equalization-error profile, which is how you reproduce the Chapter IV failure in the lab and confirm your fix actually fixes it.
Run the validation as a ladder. Rung one: a static platform over flat scattering. The sonar should recover the flat floor; if it does not, the matched filter or the beamformer is broken and nothing else matters. Rung two: a descending platform, no flex, no medium decorrelation. The moving-frame correction should make the result indistinguishable from rung one; any residual blur is a bug in Chapter II's stabilisation. Rung three: add medium decorrelation. The phase-lock metric should drop, the fallback to incoherent sub-windows should engage, and the resolution should degrade gracefully — to the incoherent limit, not to noise. Rung four: inject a lagging-equalization flex profile. You should see the Chapter IV signature appear in the lock-quality plot, and you should see your transducer-face-velocity correction remove it. Climb the ladder in order. Do not skip a rung because you are confident; confidence is what the ladder is for.
A note on what "validated" means and does not mean. Passing the ladder means your processing is self-consistent with a model whose assumptions you chose. It does not mean the model is the ocean. The real trench will have scattering statistics you did not anticipate, layered media the model does not represent, and biological scatterers that move in ways no static field captures. Validation against the model buys you the right to be surprised by the ocean rather than by your own code — which is the entire point. When the real data looks strange, you will be able to say "the processing is sound; the strangeness is real" with a straight face, because you climbed the ladder.¶
Keep the model in the repository, versioned, with the exact parameter sets for each rung checked in beside the firmware they validate. When someone changes the beamformer next quarter, the ladder runs in CI and either still passes or tells them precisely which rung they broke. A reverberation model that lives only on one engineer's laptop is not a validation tool; it is a story that engineer tells at lunch. Check it in. Make it run automatically. Let it outlive everyone who built it.
And that is volume seventeen. You configured the equalization loop so the hull breathes; you stabilised the sonar stream so the moving frame stops lying; you derived the phase-lock condition and learned to read its failure; you diagnosed a real failure three layers down to a human-set parameter; and you validated the whole chain against a model you control. Next volume descends further — into the navigation filter that fed the moving-frame correction, and the ways it, too, can lie. Until then: re-read Chapter I. The loop runs once per dive, and the dive is about to end.
— end of vol. 17 — surface in 00:00:00 — log closed at 3940m —
# the validation ladder — runs in CI, fails loudly on the broken rung
RUNGS = [
("static / flat", dict(descend=False, flex=None, medium_tau=None)),
("descend / no flex", dict(descend=True, flex=None, medium_tau=None)),
("descend / medium decorr", dict(descend=True, flex=None, medium_tau=2.0)),
("descend / lagging eq", dict(descend=True, flex="lag_K", medium_tau=None)),
]
def run_ladder(pipeline):
for name, cfg in RUNGS:
stream = reverb_model.simulate(**cfg)
img, lock = pipeline.process(stream)
ok = pipeline.meets_spec(img, lock, cfg)
print(f" rung [{name:>22}] : {'PASS' if ok else 'FAIL — fix this rung first'}")
if not ok:
raise SystemExit(1)
print(" ladder complete — processing chain validated against the model")