One Python class: the step-4 design¶
Step 4 of the consolidation (#2752, API taxonomy §6.4). Step 3 left one C object with seven constructors (correlator.md). Python still sees seven classes in two modules:
doppler.spectral:Corr,Corr2D,DelayDoppler,CorrDetector,CorrDetector2D;doppler.acquire:Acquisition,BurstAcquisition.
Step 4 makes them one class, doppler.correlation.Correlator. The knobs are
its arguments, burst or continuous framing included. No Acquisition or
BurstAcquisition class survives (owner, 2026-10-31), and the burst_acq
C forwarder goes with them (step 1, folded here).
This page also settles three linked issues that step 4 needs:
- #2683: the Python batch contract (§10);
- #2900: bench rows that keep their history across a rename (§11);
- #2901: the flavor recorded twice (§12).
The owner's two rules still hold: no private copy survives, and speed only
goes up. Step 4 is mostly binding and documentation. Its C changes are a
constructor dispatcher (§3), BurstCapture calling the engine directly
(§8), and #2901 (§12). This follows
Adding an algorithm from phase 1.
§16 lists the decisions, accepted on review, and §17 what they left open.
1. Who calls it, and with what¶
| caller | needs |
|---|---|
| A user correlating a block against a template | maps: one per call, or a batch reduced in C |
| A user searching a code over delay x Doppler | maps, a hypothesis bank, the peak per map |
| A user detecting a reference in a stream | detections from push, threshold set as k x noise |
| A user acquiring a DSSS signal or a burst | detections from push, threshold set from Pfa/Pd and a C/N0 design point |
BurstCapture, DsssReceiver, AsyncDsssReceiver (C) |
the engine, composed in C; Python never sees it |
doppler.dsss.orchestrator (Python) |
a burst search per channel, built as BurstAcquisition today |
The last row is a fourth consumer, which the taxonomy's table
(§6.4) now lists too. It
builds one BurstAcquisition per channel (orchestrator.py:214) and hands
it out through an acquisition property.
2. Today's seven surfaces¶
Measured from the stubs on main (0bb4824d0), excluding the context-manager and state-blob members every class has:
| class | module | constructor | methods | properties |
|---|---|---|---|---|
Corr |
spectral | ref, dwell, nthreads, n_out |
execute, reset |
4 |
Corr2D |
spectral | ref (2-D), dwell, nthreads, ny_out, nx_out, col_out |
execute*, bank_* (2), *_max_out |
8 |
DelayDoppler |
spectral | code, ny, dwell, nthreads, ny_out |
as Corr2D |
7 |
CorrDetector |
spectral | ref, dwell, noise_lo, noise_hi, noise_mode, threshold, nthreads |
push, reset |
8 |
CorrDetector2D |
spectral | as CorrDetector, 2-D ref |
push, reset |
10 |
Acquisition |
acquire | code (uint8), sample_rate, chip_rate, symbol_rate, cn0_dbhz, doppler_uncertainty, pfa, pd, noise_mode, code_only_epochs, doppler_rate |
push, surface* (4), block_* (2), setters (4), configure_search_raw |
33 |
BurstAcquisition |
acquire | preamble, reps, fs, cn0_dbhz, doppler_uncertainty, pfa, pd, noise_mode, doppler_rate, rate_hypotheses |
push, set_max_peaks, configure_search_raw |
24 |
Six of them are the correlator object today: Corr2D its base class,
the other five its views. BurstAcquisition
is the seventh: its own object, burst_acq, a C forwarder onto the same
engine.
Two inconsistencies a reader meets today, which one class removes:
- One quantity, several names. The sample rate is
sample_rateonAcquisitionandfsonBurstAcquisition. The reference isref,codeorpreamble. pushreturns two shapes.CorrDetector*return 5-field tuples;AcquisitionandBurstAcquisitionreturn 9-field tuples of the same C record,dp_correlator_hit_t.
3. One constructor¶
The constraint. A jm object has one constructor (create_fn). Views
give a second constructor, but each view is a separate class with its own
name. Alternate constructors (factories) exist only for kind = "handle"
modules, not objects (just_makeit/_keys.py). And a module's
__init__.py is re-export only, so a Python factory function is out.
So "one class whose knobs are its arguments" means one C constructor that takes every knob and dispatches to the flavor's own constructor. The dispatch lives in C, once. The seven flavor constructors stay public for C callers, who name the flavor they want.
Proposed C entry point (correlator_core.h):
dp_correlator_state_t *dp_correlator_create (
const float _Complex *ref, size_t ny, size_t nx,
dp_correlator_framing_t framing, /* NONE, CONTINUOUS, BURST */
size_t doppler_rows, /* > 0: a code row, delay x Doppler */
int detect, /* maps or detections (framing NONE) */
size_t dwell, size_t ny_out, size_t nx_out, int col_out,
float threshold, size_t noise_lo, size_t noise_hi, int noise_mode,
double sample_rate, double chip_rate, double symbol_rate, size_t reps,
double cn0_dbhz, double doppler_uncertainty, double pfa, double pd,
double doppler_rate, size_t rate_hypotheses, size_t code_only_epochs,
int nthreads, const char **why);
It is positional because jm's init_params map one to one onto the C
constructor's arguments; the Python face is the keyword call below. C
callers are pointed at the seven flavor constructors, and the
dispatcher's own doc in correlator_core.h says so: it is the binding's
entry point, not one a C caller should write by hand.
How the knobs pick the flavor:
framing |
ref |
doppler_rows |
detect |
flavor | replaces |
|---|---|---|---|---|---|
none |
1-D | 0 | no | 1D |
Corr |
none |
1-D | 0 | yes | DETECTOR_1D |
CorrDetector |
none |
2-D | 0 | no | 2D |
Corr2D |
none |
2-D | 0 | yes | DETECTOR_2D |
CorrDetector2D |
none |
1-D | ny > 0 | no | DELAY_DOPPLER |
DelayDoppler |
continuous |
1-D chips | - | implied | SEARCH_CONTINUOUS |
Acquisition |
burst |
1-D | - | implied | SEARCH_BURST |
BurstAcquisition |
Any other combination is refused with a ValueError naming the knob. For
example, a 2-D ref with framing = "burst", or doppler_rows with
detect. A value the flavor would not honor is refused, never
silently ignored: a knob the flavor does not read, set away from its
default, is one. The dispatcher writes the sentence through its trailing
why, which the binding raises (create_why, just-makeit#2552; C callers
pass NULL).
Python spelling (defaults as §17 rules them):
Correlator(ref, *, framing="none", doppler_rows=0, detect=False,
dwell=1, ny_out=0, nx_out=0, col_out=-1,
threshold=0.0, noise_lo=0, noise_hi=<last bin>,
noise_mode="mean",
sample_rate=0.0, chip_rate=0.0, symbol_rate=0.0, reps=1,
cn0_dbhz=nan, doppler_uncertainty=0.0, pfa=1e-3, pd=0.9,
doppler_rate=0.0, rate_hypotheses=1, code_only_epochs=1,
nthreads=1)
One name for each quantity (§2): ref for every reference,
sample_rate on both search flavors, and nx_out for the padded lag
axis at any row count. fs, preamble and Corr's n_out retire.
Three things jm must express, to confirm on a scratch project in phase 2 before any code:
- One
refthat is 1-D or 2-D. TodayCorr2D'srefisfloat _Complex[][]andCorr's isfloat _Complex[]. One init-param has one type. Fallback:refis 2-D always, and a 1-D reference isref[None, :](ny = 1). That is a worse spelling for the commonest case, so it is the fallback, not the plan. - The continuous search's chips.
dp_correlator_create_continuoustakesuint8_tchips at the chip rate; every other flavor takes cf32 samples. Recommended: the dispatcher takes the chips as cf32 NRZ (+1/-1, cvt's mapper), as the burst flavor already takes its preamble. Then "the reference is always samples" holds, andDsssReceiver's C path keeps calling the uint8 constructor directly. The dispatcher refuses any chip that is not exactly +1 or -1 (acreate_errornamingref), so a shaped or noisy reference cannot quantize silently. - A string-enum
framing. jm hasstring_enum:init params (noise_modeuses one), so this is expected to work as-is.
If (1) needs a jm feature, the ask is filed and fixed upstream first (owner: fix it where it is defined), never a hand-written fragment.
4. Members a flavor does not serve¶
One class carries every flavor's members: about 20 methods and 40
properties once duplicates merge. Most belong to some flavors only:
execute* and bank_* to the map flavors, push to detectors and
searches, surface*, eta, pd_predicted and the rest to searches.
- Methods already refuse: the C returns
SIZE_MAXor an error code on a flavor that does not serve the call, and the binding raises. The docstring names the flavors that serve it. - Properties on the search record read through
search, which isNULLfor a map or detector flavor. They raise there, anAttributeErrornaming the flavors that serve the property, through a C accessor that refuses off a search flavor. A NaN or a 0 would be a silent wrong answer:Correlator(ref).etareading NaN looks like a measurement. Whether a jm property getter can raise is checked in phase 2 with the items in §3. If it cannot, that is a jm ask, filed and fixed upstream before the class ships (the path just-makeit#2516 took), never a NaN fallback. - A
flavorproperty (string) says which flavor the knobs built, so a caller can ask before reading a member, instead of catching the error.
5. What push returns¶
One record for every flavor: dp_correlator_hit_t, returned as a NumPy
structured array (record_dtype), the shape BurstCapture.detections()
already returns. Two reasons:
- One shape for one record, instead of 5-field tuples on detectors and 9-field tuples on searches.
- It removes the stack cap. Today the binding calls into a stack array of
max_results = 1024hits, and a push that would make more loses the input past the last one (#1992, just-makeit#2184). Avariable_outputrecord array sizes from a*_max_outinstead.
The fields keep their C names: row, col, peak_mag, noise_est,
test_stat, cn0_dbhz_est, samples_consumed, chip_phase,
doppler_rate_hz_s, test_stat_peak. Off a search flavor, the search-only
fields read 0, as the C fills them today.
To confirm in phase 2: jm renders record_dtype + variable_output on a
method that also takes a cf32 input array. BurstCapture's record methods
take no input (arg_type = "void").
6. State serialization¶
correlator.toml already declares serializable = "true" once for every
flavor, and each flavor's blob names its flavor, so a blob from one is
refused by another (step 3.4b). Step 4 keeps that one declaration; what
changes is the Python side.
- The state matrix (
src/doppler/tests/test_state_serialization.py) gains one(make, feed)entry per flavor with running state, spelledCorrelator(...), replacing the retired classes' entries one for one:Corr,Corr2D,DelayDoppler,CorrDetector,CorrDetector2DandBurstAcquisition (Zadoff-Chu).Acquisitionhas no entry today (#2078); the continuous flavor gets one, a split mid-stream throughpush, so it is held like the other six. - The #2022 mutator gate (
test_mutator_state.py) sees the merged members: every mutator is in the blob or onscripts/.mutator-state-exempt, and that list only shrinks. Its rows move from the retired names toCorrelator's (Acquisition.keep_surfacebecomesCorrelator.keep_surface), and the sevenAcquisition.* NO_RECIPErows, which exist only becauseAcquisitionhad no matrix entry, are probed once the continuous flavor has one. Each then leaves the list or stays with its own reason. - Cross-flavor refusal through Python: one test, a blob from each
flavor offered to every other, each refused with
ValueErrorand the target unchanged, the Python twin of the C_search_flavor_cross_load_check.
7. The module¶
- New module
doppler.correlation:modules/correlation.toml, objects["correlator"], and a[domain.correlation]entry indocs/domains.toml. The C object stays atnative/{inc/doppler,src}/correlator/; only its Python module moves. doppler.spectrallosesCorr,Corr2D,DelayDoppler,CorrDetector,CorrDetector2D, and its module docstring stops naming them.doppler.acquirelosesAcquisitionandBurstAcquisition. It keepsCarrierAcquisition,BurstCapture,PersistentBurstCaptureandbin_to_signed, and its docstring points atCorrelatorfor the search.- No aliases. The old names are not re-exported, deprecated or kept
as subclasses (owner: none survives). This is step 4's Python break,
released once, with the changelog entry naming each old class and its
Correlator(...)spelling.
Correlator's docstring and guide say it is "also called acquisition when
it is part of a detector or receiver", so a search for the word lands on it.
8. Retiring burst_acq¶
What goes:
native/inc/doppler/burst_acq/,native/src/burst_acq/;native/src/acquire/acquire_ext_burst_acq.c,objects/burst_acq.toml;native/tests/burst_acq/,native/benchmarks/burst_acq/;src/doppler/acquire/tests/test_burst_acq.py,src/doppler/acquire/benchmarks/bench_burst_acq.py.
BurstCapture calls the engine directly. It makes eight burst_acq_*
calls (create x2, destroy, reset, push, consumed,
configure_search_raw, and the state type in its header). Each becomes the
dp_correlator_* call the forwarder made. underpowered already lives on
the search record. burst_capture.toml's depends_on names correlator
instead of burst_acq.
Oracle: BurstCapture's C and Python tests and its validator
(src/doppler/acquire/tests/validation/burst_capture/), unchanged and
green; and its state blob byte-identical, since the forwarder's blob
already was the engine's.
9. The objects that compose it¶
| consumer | today | step 4 |
|---|---|---|
BurstCapture |
C, through the burst_acq forwarder |
C, dp_correlator_* directly (§8) |
DsssReceiver |
C, dp_correlator_state_t |
unchanged |
AsyncDsssReceiver |
C, dp_correlator_state_t |
unchanged |
dsss.orchestrator |
Python, BurstAcquisition(preamble, ...) |
Correlator(preamble, framing="burst", ...); its acquisition property returns that |
All three receivers forward configure_search_raw with their own
docstrings. Those docstrings name BurstAcquisition.doppler_bins and
Acquisition; they are rewritten to name Correlator.
10. #2683: the Python batch contract¶
What #2683 asked for and where it stands:
| ask | state |
|---|---|
| a batch interface with documented axes | done in C and Python: execute_all, execute_coherent, execute_power_sum, execute_power_max, execute_peaks, bank_power_max, bank_detect |
| coherent / independent / power sum / power max | done, each a fixed C reducer |
| a detector-specific statistic without a Python callback | C only: dp_correlator_execute_fold and bank_fold take a C reducer |
| value tests for every reducer | partly: see below |
Recommended: the fixed reducers are the Python contract.
execute_fold's reducer is a C function pointer called once per map. A
Python callable there is the inner-loop callback #2683 forbids, and a
pointer from ctypes or cffi is a foreign object no other binding
accepts. A caller who needs a statistic the fixed reducers lack takes
execute_all (every map, no accumulation: #2683's "streaming
independent-map output") and reduces in NumPy, or writes the reducer in C.
The Correlator docstring says so, and #2683 closes on that.
Value tests still missing, each against a NumPy reference built from
execute_all's maps:
execute_power_sumandexecute_power_maxvalues, including the originating map index for power max and its tie rule;- max of per-map statistics differs from the statistic of a pointwise-max map (the cancellation case #2683 names);
- a batch called mid-dwell, for each reducer beyond
all,coherentandpeaks: the temporal accumulator is unchanged after it; reset()before and after a batch;- an
out=buffer reused across calls, poisoned per run.
They move with the class into src/doppler/correlation/tests/. Each is
proved by sabotaging its reducer in a copy.
11. #2900: bench rows keep their history across a rename¶
Step 4 renames rows again: py:corr2d::*, py:acq::* and
py:burst_acq::* become py:correlator::*, and c:burst_acq::* becomes
c:correlator::*. Without #2900 each one starts a new trend line and its
labels are carried twice for a release, as c:corr:: and c:corr2d:: are
now.
Proposed: one table, benchmarks/renames.toml.
Three readers, one declaration (owner: one declaration, both directions):
bench_reportjoins a row's history across the table, so the trend line continues.- The label gate accepts a label under the new ID for a published row
under the old one, so
labels.tomlholds one key per row. make bench-abpairs renamed rows through the table;ALIAS=is gone.
Its gate: a rename whose old prefix names no published row, or whose
new names no current bench, fails make lint. Proved by sabotage like
every gate. Rows are prefixes, so a rename entry may also rename a single
row when the bench IDs change shape.
2900 lands as its own PR before step 4's code, so step 4's renames are¶
entries in the table from the start. Its follow-up stays as #2900 names it:
the old c:corr::/c:corr2d:: label keys go once the next release
publishes the new rows.
12. #2901: the flavor recorded twice¶
dp_correlator_state_t has flavor and the older delay_doppler flag,
which is 1 exactly when flavor == DP_CORRELATOR_DELAY_DOPPLER.
Recommended: drop delay_doppler and branch on flavor. The
dispatcher (§3) makes flavor the one fact every path reads, and a
compare of one enum against a constant costs the hot path nothing a
flag-read did not. The struct-layout risk #2901 names is measured, not
assumed: its own PR, make bench-ab against main on the pinned rows, and
the pool row that #2886 diagnosed among them. If any row is slower, the
other fix (keep the flag, set it only from flavor in one place) takes
its place.
13. Docs, examples, validation and benches¶
Guides and gallery, rewritten for Correlator:
- guides:
docs/guide/acquisition.md,delay-doppler.md,dsss-acquisition.md; a newCorrelatorguide replaces theCorrandCorrDetectorexemptions (docs/.user-docs-exempt, lines 90-91); - gallery:
corr.md,delay-doppler.md,detection2d.md,dsss-acq-characterization.md; - API pages: a new page for the
correlationmodule; thespectralandacquirepages lose the seven classes.
Examples ported, not dropped (13):
acq_surface_demo,burst_capture_demo,corr_demo,delay_doppler_bank_demo,delay_doppler_batch_demo;detection2d_demo,detector2d_acq_demo,dsss_burst_ber_demo,dsss_burst_demo,dsss_realtime_file_demod;dsss_receiver_demo,mpsk_doppler_profile_demo,mpsk_receiver_performance_demo.
Each keeps its physical asserts. The C twins (native/examples/corr_demo.c,
delay_doppler_bank_demo.c, delay_doppler_batch_demo.c) already call the
C flavor constructors, which stay; they are unchanged. Phase 9 owes a C and
a Python example for the one class, and these are them. A gallery PNG it draws is regenerated and
must not change beyond its own tolerance. Every retired class's docstring
example is ported into Correlator's, or into a guide whose fences are
tested.
Validation. The one report from step 3.5
(src/doppler/spectral/tests/validation/correlator/) moves to
src/doppler/correlation/tests/validation/correlator/ and its Python face
switches to Correlator. The verdicts are the oracle (§14); the report is
not re-certified unless a verdict moves.
Benches. bench_corr.py, bench_correlator.py, bench_acq.py and
bench_burst_acq.py become one bench_correlator.py in the
correlation module's benchmarks, each row renamed through §11's table.
14. Oracle and speed gate¶
- Behaviour. Every test that names a retired class runs against its
Correlator(...)spelling with its expected values unchanged: the composing objects' tests and validators, the correlator's validation verdicts, every ported example and doc fence. A changed value is a finding, not an update. - Retirement. No new gate: each retired class name and the
burst_acq_prefix get a row inscripts/.retired-names(check_retired_names.py) in the commit that retires them, and theburst_acq_*symbols a row forcheck_retired_symbols.py. - Speed.
make bench-ab A=main B=<branch>on the pinned rows of correlator.md §7.1, plus the renamed Python rows. Step 4 changes no hot loop, so every row is expected to tie. The binding's record-array return (§5) is the one change on a measured path; itspushrows are judged against the tuples they replace.
15. Order of work¶
Each item is its own PR. The release of step 3's C break comes first (#2752's order).
- #2900:
benchmarks/renames.tomland its three readers (§11). - #2901: one flavor field, with its own
bench-ab(§12). - Phase 2 on a scratch project: the three jm checks in §3, the property getter in §4 and the record array in §5. Any gap is filed and fixed upstream before 4.
- The class:
dp_correlator_create,modules/correlation.toml, the seven views removed, the record array, the #2683 tests. burst_acqretired:BurstCaptureon the engine directly, the orchestrator onCorrelator(§8, §9).- Docs and examples ported (§13).
- Release step 4's Python break.
Items 4 to 6 may land as one PR if splitting them would leave main with a class whose examples do not run.
16. Decisions for review¶
| # | decision | recommended |
|---|---|---|
| D1 | how one class gets every flavor | one C dispatcher, dp_correlator_create, with every knob (§3) |
| D2 | the continuous search's reference | cf32 NRZ chips, anything not exactly +-1 refused; C keeps the uint8 constructor (§3) |
| D3 | a member its flavor does not serve | methods and properties raise; a getter jm cannot raise from is a jm ask, fixed first (§4) |
| D4 | push's return |
one structured array of dp_correlator_hit_t, no stack cap (§5) |
| D5 | old class names | none kept, no aliases (§7) |
| D6 | #2683's extensibility | fixed reducers + execute_all are the Python contract; fold stays C (§10) |
| D7 | #2900's shape | benchmarks/renames.toml, read by report, label gate and bench-ab (§11) |
| D8 | #2901 | drop delay_doppler, branch on flavor, if bench-ab ties (§12) |
D1 to D8 were accepted as recommended (reviewer, 2026-11-08); §17 lists what they left open.
The unknowns, checked on a scratch project with just-makeit 0.123.0 (2026-11-08), each the shape step 4 uses:
| unknown | answer |
|---|---|
a 1-D or 2-D ref in one init param (§3) |
yes: rank = [1, 2] on a T[][] init param (just-makeit#2537). A 1-D array arrives as one row, (1, n), and a 3-D one is refused |
| a property getter that raises (§4) | yes: getter_status = true with error = "AttributeError" (just-makeit#2538). The C getter returns non-zero, the read raises with the message, hasattr reads False |
| a record array from a method with input (§5) | yes: record_dtype + variable_output on a cf32 push, sized by push_max_out; one call returned 10,000 records, with no stack cap |
framing as a string enum (§3) |
yes, as noise_mode |
17. What one constructor must choose¶
Merging seven constructors into one leaves one default per knob, and the seven classes disagree on some of them today. A knob a flavor reads takes the one default, and a value the flavor would not honor is refused (§3). Measured from the manifests on main (84049024d); every row accepted on review (2026-11-08):
| knob | today | recommended |
|---|---|---|
a one-row ref |
Corr (1-D) and Corr2D at ny = 1 are two flavors |
the 1-D flavor, given 1-D or (1, n); nx_out pads it, n_out retires |
noise_hi |
the detectors: the SIZE_MAX sentinel, clamped to the last bin |
that sentinel; §3's 0 would make the floor one bin |
reps |
BurstAcquisition: 1 |
1 |
sample_rate |
BurstAcquisition's fs: 1.0; Acquisition: 4 MHz |
0.0, not given: a search flavor refuses it |
chip_rate |
Acquisition: 1 MHz |
0.0, not given: the continuous search refuses it |
symbol_rate |
Acquisition: 1 kHz |
0.0, no known data clock: the continuous search honors it (a diagnostic, never sizing) |
cn0_dbhz |
BurstAcquisition: NaN, no design point; Acquisition: 50 dB-Hz, and a finite one only |
NaN, no design point: the burst search's meaning today; the continuous search refuses it |
detect on a search framing |
a search always detects | True accepted: a search honors it, so the rule accepts it |
nthreads on a search framing |
a search sizes its own pool at create; set_threads() sets it after |
refused away from 1, with a sentence pointing at set_threads(): the search honors a count, but not through this knob, whose 1 would mean another request |
A one-row reference is the 1-D flavor. rank = [1, 2] hands C a 1-D
array as one row, so C cannot tell ref from ref[None, :].
dp_correlator_create_1d is dp_correlator_create_2d at one row, its
n_out passed as nx_out, so the two differ only in their output's shape
(1-D against (1, n)) and in col_out. The one-row detector already is
the 2-D one at one row (correlator.md §2.1). So a one-row reference builds
the 1-D flavor. Zero-padding it stays reachable under the one padding
name, nx_out: Corr's n_out was the same fact under a second name,
and it retires. ny_out and col_out are refused at one row. No Python caller on
main builds a one-row 2-D correlator: the col_out callers
(test_corr2d_batch.py, the correlator validator) use four rows.
A rate is never a default. fs = 1.0 and the 4 MHz / 1 MHz / 1 kHz
triple are facts about one caller's signal, and a search sized by
omission with the wrong one detects at the wrong Doppler without a word.
A search flavor refuses a rate it reads that is still 0, naming the knob;
a flavor that does not read a rate is not asked for it. symbol_rate is
not such a rate: the continuous search reads it only for a diagnostic
(epochs_per_symbol), never to size the search, and its own 0 means no
known data clock, so 0 is honored there. Normalized units
stay one stated argument away, sample_rate=1.0. Every port states its
rates, and the changelog names both old defaults.
cn0_dbhz defaults to no design point. One default cannot be both
NaN and 50 dB-Hz. NaN keeps the burst search as it is. The continuous
search sizes its dwell from a finite design point and refuses NaN today,
so it still does: Acquisition's 50 dB-Hz was a design point chosen by
omission, and a caller now names one.