One correlator: the step-3 design¶
Step 3 of the consolidation (#2752,
API taxonomy §6.4).
Six C directories hold seven public classes that compute one thing:
correlate input against a reference, then perhaps threshold it. Step 3 makes
them one C object, native/{inc/doppler,src}/correlator/, and retires
corr/, corr2d/, detector/, detector2d/ and acq/. burst_acq/ goes
in step 4 with the Python classes (taxonomy §6.4, step 1 folded).
Step 2 already put the delay-Doppler search in one place: acq's two paths run
on the map in corr2d_core.c (acq-surface §18).
Step 3 does the same for everything around it: one framing, one detection
layer, one peak picker and one state blob.
The owner's two rules govern every sub-step (2026-11-02):
- No private copy survives. When a sub-step lands, the code it consolidates exists once.
- Speed only goes up. Every benchmark row the sub-step touches is equal
or faster on the bench box, judged by
make bench-abagainst its budget (§7.1).
This is a redesign of certified objects, so it follows Adding an algorithm from phase 1, and the reports are re-certified on the one object.
1. Who calls it, and with what¶
| caller | object today | shape | does with the answer |
|---|---|---|---|
BurstCapture |
Acquisition (burst door) |
stream of samples, code row × Doppler | resolves the period, refines, retains, emits a burst |
DsssReceiver, AsyncDsssReceiver, AsyncDsssPool |
Acquisition (continuous door) |
stream, code row × Doppler | hands the hit to the tracking loops |
CarrierAcquisition |
CorrDetector |
stream of averaged PSD frames, 1-D template | sub-bin carrier estimate |
| Python users | Corr, Corr2D, DelayDoppler |
whole frames | the correlation map, batch reductions, the hypothesis bank |
| Python users | CorrDetector, CorrDetector2D |
stream | detections over a k × noise gate |
Nothing else in native/ correlates by FFT: ppe_core.c is a periodogram.
carrier_acq is coupled to CorrDetector three ways, and sub-step 3.3
changes all three:
- it nests the detector's state blob in its own (
carrier_acq_core.c:368,:388,:423), soCAQR's version bumps when the detector's blob becomes the correlator's; - it reads the detector's correlation output directly,
s->det->out_buf(:207-209), for the parabolic refine; - it sets the detector's threshold on every push (
:298).
Everything else that touches the retired objects, each in the oracle of the sub-step that changes it:
| kind | files |
|---|---|
| examples | native/examples/corr_demo.c; src/doppler/examples/corr_demo.py, which draws docs/gallery/corr.md; src/doppler/examples/dsss_burst_demo.py:210 (Corr and its n) |
| docs | docs/api/python-spectral.md, docs/api/python-detection.md, docs/gallery/corr.md |
| Python suites | src/doppler/spectral/tests/test_corr.py, test_detector.py, test_exact_length.py; the src/doppler/tests/test_state_serialization.py matrix |
| C symbol tests | native/tests/correlator/test_correlator_1d_symbols.c (Corr's, moved in 3.2); the detectors' (deleted in 3.3: the generated test_correlator_symbols.c names every view's functions) |
| bookkeeping | scripts/.mutator-state-exempt, native/inc/.example-arity-ratchet, native/.c-header-code-ignore, scripts/.doc-symbol-exempt |
2. Today, measured¶
Origin/main at ba70f133c, 2026-11-05.
| object | core lines (.h + .c) |
engine | C tests (DP_CHECK) |
certified |
|---|---|---|---|---|
Corr |
241 + 200 | own 1-D FFT plans | 58 | no |
CorrDetector |
324 + 249 | Corr, ring, framer, own argmax |
78 | no |
Corr2D / DelayDoppler |
1,066 + 2,517 (+ 331 corr2d_map.h) |
2-D FFT, the single-row path, the delay-Doppler map, the bank | 260 | yes, 30/30 |
CorrDetector2D |
339 + 237 | Corr2D, ring, framer, det_peak_list |
140 | yes, 15/15 |
Acquisition |
2,324 + 3,510 (+ 119 acq_private.h) |
the map, its own framer, det_private.h's CFAR and peaks |
597 | yes, 22/22 (two findings open) |
BurstAcquisition |
329 + 92 | a forwarder onto Acquisition |
12 | yes, 16/16 |
The shared CFAR is det_private.h (since 3.3,
native/inc/doppler/correlator/correlator_private.h): header-only
statics for the ring (det_ring_create), the noise estimate
(det_noise_chunk, det_noise_estimate), the peaks (det_peak_list,
det_peak_scan, det_peak_zone) and the framed push (det_framed_push).
CorrDetector, CorrDetector2D and acq include it.
2.1 The 1-D pair is the 2-D pair at one row¶
The arithmetic is the same, in the same order. Corr's execute, before
3.2 (corr_core.c:154-194), and Corr2D's single-row path (corr2d_row and
corr2d_row_inverse, corr2d_core.c:428-459) take:
- the same plans (
dp_fft_createignoresnthreads); - the same multiply-accumulate against the conjugated reference spectrum;
- the same
dp_fft_pad_spectrum; - the same scale,
1/nafter the inverse (1/n == 1/nx).
det_peak_scan with no mask is CorrDetector's strict-> argmax.
A throwaway Python prototype agreed:
CorragainstCorr2Dwithny = 1: 32 maps byte-identical, over n = 31, 64, 100 and 1023, dwells 1 and 3, and lag pads 1 and 2.CorrDetectoragainstCorrDetector2Dwithny = 1: 110 hits over 48 streams identical. The streams covered the four noise modes, dwells 1 and 2 and n = 31, 64 and 127, pushed in odd chunks.
The prototype is evidence for this page only; nothing reruns it. Sub-step 3.2 opens by committing it as a C test (§7).
3. What is duplicated, which rule 1 removes¶
| the copies | becomes | |
|---|---|---|
| D1 | Corr's 1-D correlation (corr_core.c:154-194) and Corr2D's single-row path (corr2d_core.c:428-459) |
the single-row path, at ny = 1 (§2.1) |
| D2 | CorrDetector's argmax loop (detector_core.c:49-52) and det_peak_list |
det_peak_list with one peak; NaN by U2's rule |
| D3 | Corr2D's own peak passes: corr2d_fold_peaks (corr2d_core.c:1072-1106) and the bank's 3 × 3 local maximum (corr2d_core.c:1771-1808), beside det_peak_scan and det_peak_zone |
one peak primitive in the correlator's private header, if the measurements agree (U3) |
| D4 | the dwell counter, ++count == dwell (corr_core.c:160, corr2d_core.c:620, :632) |
the single-row path's, with D1 |
| D5 | acq's setup-time n-point transform pairs outside the map: acq_shape_of_template, acq_off_peak, acq_doppler_sidelobe and the sidelobe ratio (dp_correlator_sidelobe_ratio now) |
decided by U4: the first two on the single-row path (3.4a), the last two on the one delay-Doppler map (3.4b-pre), all in cf32 |
Not duplicates:
- the three rings and framers, which all go through
det_ring_createanddp_f32_framer, one primitive already; det_peak_list's two appearances indet_private.h(:191and:317), which are a forward declaration and its definition.
4. The one object¶
One state type, dp_correlator_state_t, in four layers. Each layer is used
by the constructors that need it, and none exists twice.
| layer | holds | from today |
|---|---|---|
| map | the reference spectrum and the transforms: the general 2-D shape, the single-row shape (1-D at ny = 1) and the delay-Doppler map with its bank |
corr2d_core.c and corr2d_map.h |
| dwell | the coherent accumulator and its count; the non-coherent sum | Corr2D's, and acq's nc_surface |
| stream | the ring and framer, block-coherent epochs held raw | det_private.h's ring and framed push, acq's framer |
| detection | magnitude, noise estimate, peak list, the gate (k × noise, or from Pfa and Pd), twins, the decision | det_private.h, acq's CFAR |
acq's search policy sits on top of the four layers and stays in the object:
the two front doors' sizing (acq_auto_config_*), the rate hypotheses, the
carrier and its rebuild, the sidelobe floor, the hand-off, the telemetry and
the surface sink.
The constructors are flavors, the methods shared. Today's constructors become the object's, renamed and otherwise unchanged in their arguments:
| today | becomes | layers |
|---|---|---|
Corr's constructor (renamed in 3.2) |
dp_correlator_create_1d |
map (single row) and dwell |
Corr2D's constructor (renamed in 3.2) |
dp_correlator_create_2d |
map and dwell |
DelayDoppler's constructor (renamed in 3.2) |
dp_correlator_create_delay_doppler |
map (delay-Doppler) and dwell |
CorrDetector's constructor (renamed in 3.3) |
dp_correlator_create_detector_1d |
all four, single row |
CorrDetector2D's constructor (renamed in 3.3) |
dp_correlator_create_detector_2d |
all four |
| acq's two constructors (moved in 3.4b) | dp_correlator_create_continuous, dp_correlator_create_burst |
all four, with the search policy |
execute and its batch family serve the map constructors, and push
serves the detection ones. A method that cannot serve a flavor refuses it
(SIZE_MAX, as execute refuses a short frame today), never falls back.
One hit record (owner, 2026-11-05). The 1-D detector's record (lag),
the 2-D detector's (row, col) and acq's (row, col and the search's
fields) were three records for one event. The correlator has one,
dp_correlator_hit_t, with row = 0 at one row: 3.3 made it the two
detectors', and 3.4 widens it with acq's fields.
One state blob. One magic, CORL, and one version. The flavor is the
blob's first field and a reject key, like the grid today. The payload is
each layer's own fields, packed by that layer once.
5. The one deliberate C break¶
The owner ruled it on 2026-11-05: the C renames and the retired state magics are one break, listed under Breaking in the release that ships them.
- Every
dp_corr_*,dp_corr2d_*,dp_detector_*,dp_detector2d_*anddp_acq_*symbol becomesdp_correlator_*, with no deprecation shims (the doppler#2753 policy: before 1.0, a break happens at once). - The retired magics are refused, never misread.
CORR,CR2D,DDMP,DET1,DET2andACQRhave shipped in a release, anddocs/design/state-serialization.mdholds that a released version names its layout for good (make state-versions-checkreads every release tag). So they are not reused.CORLis a new magic, and a blob saved by any release before the break failsdp_state_validatecleanly. - The composing blobs that nest one bump their own versions:
carrier_acq'sCAQR(in 3.3), and acq's consumers in 3.4,BurstCapture, the receivers and the pool, each named in that sub-step's PR.
The Python classes keep their surfaces until step 4, so a Python user
meets one break, at step 4. One jm object, correlator, carries today's
classes as [[correlator.views]]. Each view has its own class_name,
create_fn and init_params, and exclude_methods and
exclude_properties trim what a flavor does not serve, as ddc does for
MatchedDDC.
A view can also add a member its parent lacks, or override a parent
method under the same Python name with a fn of its own (gh-504; jm
gh-1012, with MpskReceiverR's [[mpsk_receiver.views.methods]] steps
and bits as the precedent).
The rule for each view's surface: each class keeps exactly today's
members, names and signatures. The parent is Corr2D, a public class, so
it carries Corr2D's surface and nothing more: a member on the parent is a
member of Corr2D. A view excludes every parent member its class does not
have today, and adds the ones only it has.
| view | excludes | adds |
|---|---|---|
DelayDoppler |
nx_out, col_out |
nothing |
Corr |
ny, nx, ny_out, nx_out, col_out, and every execute_* and bank_* beyond execute |
n |
CorrDetector2D |
ny_out, nx_out, n_out, col_out, and every execute* and bank_* |
push (five fields), n, ring_cap, noise_lo, noise_hi, threshold, last_corr |
CorrDetector |
as CorrDetector2D, and ny, nx |
as CorrDetector2D, with push's record bound as four fields: (lag, peak, noise, stat) |
Both detectors bind the one C push, which writes the one hit record
(§4). The 1-D view lists four of its fields, the column read as the lag,
so its Python tuple is today's. If jm cannot bind a subset of a record's
fields, that is a jm fix first, not a hand-written second push.
Both detectors read dwell and count the same way, as exprs over
their composed correlator in their own manifests, so since 3.3 those are
fields of the one state for both. set_ref, void in 1-D and int in 2-D, is C-only: no class binds
it, so the C rename (above) covers it and no view does.
Each sub-step's PR lists its views' members as the rule gives them, and
make drift-check and make test-stubs hold the stubs to today's.
6. Goals, and what is not known yet¶
Goals:
- rule 1 and rule 2;
- every retired object's C tests passing against its replacing flavor, and every Python suite, example and doc fence that touches it;
- the certified reports (Corr2D, CorrDetector2D, Acquisition, BurstAcquisition) re-certified on the one object with no limit lost.
Unknown, and measured in the sub-step that needs it. Each names its row or its input:
- U1. The short 1-D rows through the single-row path. §2.1 shows the
bits agree, not that the speed does.
Corr2Dadds per-worker plans and pool dispatch, which a short frame may not amortize. Today's rows run at n = 1024 and 16384 (corr::execute[accumulate],execute[dump]) and chunks of 64 and up (detector::push[chunk=…]), where dispatch is invisible. So the small-n rows of §7.1 land on main first, and U1 is theirbench-abverdict:corrat n = 31, 63 and 127, anddetectoron frames of 31, 63 and 127. - U2. Ties and NaN in the one peak picker. On equal cells,
det_peak_listwith one peak keepsCorrDetector's lowest index (§2.1). On NaN they differ. Throughdet_peak_listwithgate = -1, a NaN map gives an empty list, so the peak is 0 (detector2d_core.c:30), whileCorrDetectorreports NaN at lag 0.- Decided here: NaN is never a peak, the family's rule already
(
acq_rate_better's "a NaN never displaces"). - Today's picker does not keep that rule.
det_peak_scanseeds its pick at the first candidate, and no cell is>a NaN, so a NaN there is the pick: a NaN in cell 0 hides a finite peak elsewhere, on both detectors, and on acq's masked scans too. 3.3 fixes it in the one picker: the seed skips NaN cells (isnan, which the build's-fno-finite-math-onlykeeps), so both paths agree with the masked path's NaN-as--INFINITY. Finite surfaces pick exactly as before. - A hit can change. At
threshold = 0(always fire) no gate stands between a NaN and the hit list: an all-NaN frame gives today a 1-D hit(0, NaN, NaN, 0)and a 2-D hit(0, 0, 0, NaN, 0), and after 3.3(0, 0, 0, NaN, 0)on both. Atthreshold > 0a NaN statistic fails the gate, so no hit changes there. - The noise estimate is not changed. A NaN in the noise window makes
noise_estNaN andtest_stat0. - Pinned in C in 3.3, on both detector flavors: an all-NaN frame at
threshold = 0and above; a NaN in cell 0 beside a finite peak (the finite peak is reported); a NaN beside the peak elsewhere. One more pin holds the masked and unmasked scans to the same pick with a NaN at the first cell.
- Decided here: NaN is never a peak, the family's rule already
(
- U3. The bank's peaks on
det_peak_*. Whethercorr2d_fold_peaksand the bank's local maximum aredet_peak_scan/det_peak_zoneunder other names, or a different rule (a 3 × 3 window, where acq zones by span). Input: the bank's own C test scenes (test_corr2d_core.c, thebank_*cases). If the rule differs, the difference is stated and one rule is chosen with the owner. - U4. acq's setup transforms: precision first, then speed. Three of
D5's pairs ran in
double _Complex, and the single-row path runs in cf32, so moving them is a precision change, not only a refactor. The fourth, the sidelobe ratio (dp_correlator_sidelobe_ratio), was already cf32: its duplicate is its own plans, and 3.4b-pre moved them onto the map.- Measured on: every acq validator's
--checkverdict, and the setup's own outputs (template shape, off-peak floor, Doppler sidelobe) at every call site. - The decision rule: if cf32 moves no verdict and changes no output by more than its stated use tolerates, the pairs that are correlations move onto the single-row path. Otherwise they stay in double, and the page records them as a different precision, which rule 1 does not forbid, with the owner's ruling.
- Measured (3.4a). Both precisions ran side by side at every call
site in every acq-family test, validator and bench: 2,626 calls over
34 template shapes. The zone (an integer) never changed. The worst
changes were 6.2e-6 dB on
psl, 1.1e-6 dB ondsland 1.4e-7 (relative) on the off-peak sums. On the cf32 build every validator's--checkoutput is byte-identical to the double run. - Decided (3.4a).
acq_off_peakand the template shape's autocorrelation run on the single-row path, in cf32. The shape's power spectrum stays a double transform, because the delay-loss model needs a spectrum, not a correlation. The threepslpins against the double brute force moved from 1e-6 dB to a 1e-4 dB tolerance, with the oracle kept, and at one tile with no Doppler uncertaintydsltakes the storedpslrather than recomputing it. The Doppler-sidelobe sweep stayed in double: on the single-row path each tile offset costs a forward and an inverse transform, which made a five-tile receiver's create 2.3x slower. - Done (3.4b-pre). The sweep runs on the one delay-Doppler map, one
forward transform per residual and one row per tile offset, in
cf32: over 372 sweeps the worst
dslchange was 6.4e-7 dB, inside the same 1e-4 dB budget. The sidelobe ratio's three private plans became two maps. Its plans, pad and transforms are bit-identical; the complex product, evaluated in the map's vectorized row loop under-ffast-math, moves the ratio by at most 9.1e-8 (about 1e-7 relative, over 24,482 calls), and no pick changed. No validator's output moved.
- Measured on: every acq validator's
7. The sub-steps¶
Each is its own PR, tracked by an issue of its own under #2752.
-
3.1, this page.
-
3.1b, the small-n rows, on main first. On today's code, so their bench-box numbers are U1's baseline:
bench_corr_core.cgainscorr::execute[accumulate][n=31],[n=63]and[n=127], andcorr::execute[dump][n=31],[n=63]and[n=127];bench_detector_core.cgainsdetector::push[frame=31],[frame=63]and[frame=127], each pushing one frame a call.
Each row checks what it timed, #2832's pattern. Its input carries the reference at a planted lag; after the timing, the row asserts the peak is at that lag (the map's argmax, or the detector's hit), and
exit 1otherwise. The PR proves each check red by sabotage: plant the reference one lag off. -
3.2, the map and the 1-D detector's engine. Nothing is deleted until the evidence is committed and green:
- a C test:
dp_correlator_create_1dagainstcreate_2datny = 1, maps and hits, byte for byte, proven red by sabotage. It runs first against Corr's and Corr2D's own C,corr/andcorr2d/(the equivalence test), then against the flavors; correlator/is created fromcorr2d/, andCorrbecomes the single-row flavor (D1, D4);- Both detectors are repointed in the same PR:
detector_core.c:99built aCorranddetector2d_core.c:83aCorr2D, so they composecreate_1dandcreate_2dbeforecorr/andcorr2d/are deleted. Their blobs,DET1andDET2, then nest aCORLchild and bump, and so doesCAQR, which nestsDET1. 3.3's further change to all three rides the same unreleased versions, so no release is cut between 3.2 and the end of 3.4; if one is, each bumps again.
The oracle:
test_corr_core.c,test_corr2d_core.c,test_corr_symbols.c;test_corr.py,test_exact_length.py, the state matrix;corr_demo.c,corr_demo.py,dsss_burst_demo.py;python-spectral.md's fences.
- a C test:
-
3.3, detection.
CorrDetectorandCorrDetector2Dbecome the detector flavors,det_private.hthe correlator's private header, anddetector/anddetector2d/are deleted (D2, D3).carrier_acqcomposes the 1-D detector flavor: its nested blob, its read of the correlation output and its per-push threshold all move.CAQRnests aCORLchild in place ofDET1and rides the version 3.2 gave it (2, unreleased); if a release is cut first, it bumps again.The design, reviewed on 2026-11-06:
- The state. Two more flavors, the 1-D and 2-D
detectors; the 1-D one is the 2-D one at
ny = 1, as in 3.2. The detection layer's create-time fields are appended above the pad, so no field 3.2 measured moves; the fields a push writes (the framer,threshold, the last dump's result,consumed) go below it, besidecount.push,consumedandset_thresholdrefuse a map flavor, and theexecutefamily refuses a detector flavor. - One hit record,
dp_correlator_hit_t(row,col,peak_mag,noise_est,test_stat); the two detectors' records retire, and 3.4 widens the record with acq's fields rather than adding a second.det_noise_mode_t,det_peak_tanddet_private.h'sdet_*names keep their spelling, since acq uses them until 3.4; the header moves tocorrelator/. - The blob. A detector flavor's
CORLpayload appendsnoise_lo,noise_hiandnoise_mode(reject keys: they decide what the stored statistic means),threshold(a mutator's value, #2022), the framer's child blob and the last dump's result.set_statevalidates all of it, the framer child included, before it commits any of it. Each reject key gets a cross-load pair.DET1andDET2retire. carrier_acq's three couplings: it nests the correlator's blob, reads the 1-D flavor's dump buffer for the parabolic refine (a field read, as today), and sets the correlator's threshold on every push.- U2's fix lands first, on today's two detectors, in its own
commit, with acq's C tests and every acq validator's
--checkunchanged. - The certified report.
validation/detector2d/would tie to no component, so its 15 limits move intovalidation/correlator/as the report's second part, re-run on the 2-D detector flavor. - #2901 stays out: removing a field above the pad would move the offsets 3.2 measured.
The oracle:
- an equivalence run before the switch: a C test pushes the same
streams through
dp_detector_*and throughdp_detector2d_*atny = 1, today's two objects. It asserts every hit identical,(lag, peak, noise, stat)against(0, col, peak, noise, stat), over the four noise modes, dwells 1 and 2, three lengths and odd chunks (§2.1's prototype, committed), proven red by sabotage. It then runs against the two detector flavors; test_detector_core.c's streaming oracle, ported verbatim: chunk invariance,consumedand re-offer,reset, andset_statemid-stream;- the 2-D detector's C tests, and the symbol tests;
carrier_acq's C and Python tests;test_detector.py, the state matrix;python-detection.md's andpython-spectral.md's fences;- U2's NaN pins.
- The state. Two more flavors, the 1-D and 2-D
detectors; the 1-D one is the 2-D one at
-
3.4, the search. acq's engine moves in as the two search flavors, and
acq/is deleted. D5 follows U4's decision.BurstCapture, the receivers and the pool calldp_correlator_*, andburst_acqforwards to the new names until step 4.The design, reviewed on 2026-11-06. acq is larger than everything 3.2 and 3.3 moved together (
acq_core.c3,510 lines,acq_core.h2,322, 34 public functions), so 3.4 is two PRs:-
3.4a, U4 measured, on today's acq. D5's four setup-time transform pairs run in cf32 through the single-row path beside today's double. The PR states, for each pair, the numeric change in its output's units against the budget its use allows. It also states every acq validator's
--checkverdict, andmake validate-checkmust be up to date. If cf32 moves no verdict and keeps every pair within budget, the change lands on today's acq, so 3.4b's bench verdict separates U4's cost from the move's. If it moves a certified number, that is a finding, never a silent re-baseline, and the pairs stay in double with the owner's ruling. Landed:acq_off_peakand the shape's autocorrelation moved; no verdict or certified number moved (U4, above, has the measurement). The Doppler-sidelobe sweep stayed in double, because on the single-row path it made create 2.3x slower, and 3.4b moves it onto the delay-Doppler map. 3.4a also added the receiver-create row (§7.1), so 3.4b has its A. -
3.4b-pre, inside today's acq. The Doppler-sidelobe sweep and the sidelobe ratio's transforms move onto the one delay-Doppler map (U4 has the measurement), so the move carries no transform of its own.
-
3.4b, the move. The consumers switch in the same PR: acq cannot be deleted while one still calls it, and a forwarding shim would be a private copy. It waited on just-makeit#2516 (below). Landed (#2930). The search is the search flavors' section of
correlator_core.c, generated fromacq_core.cby one rename table and one field map: jm owns a component's functions only in its_core.c, and refuses a second source that declares them. The oracle measured all of the design below: the transition commit's 15 scenes were byte-identical, acq against the search flavors, under strict floating point (the library's-ffast-mathmoved onetest_stat_peakby an ulp through different inlining), with every sabotage red.Acquisitionis the view, and two jm gaps leave itsresetandpushshowing the shared docs until they ship (just-makeit#2528, #2529; doppler#2913).keep_surface, a writable property on the search record, gained a C setter.
3.4b's design:
-
The state. acq's fields that the layers already hold become those fields:
- the ring,
ring_cap, the framer andconsumedgo to the stream layer; mag_buf,noise_scratch,noise_lo,noise_hi,noise_modeandthresholdgo to the detection layer;- the delay-Doppler map, the pool and the last pick go to the map layer and the last-dump fields;
max_peaks, the peak list, the masks and the twins become new detection-layer fields.
The rest of the search policy is one struct, the correlator's search record: sizing, the grid, rates, the carrier, the sidelobe cache, the parts, Pfa and Pd, telemetry, the surface sink and the hand-off. The state holds it as a pointer,
search, appended above the pad: NULL for the other five flavors, so no offset 3.2 or 3.3 measured moves and their allocations do not grow. It is allocated once, at create, never per push. Inside it, the fields a push writes sit on their own line behind a pad, as in the state. acq's rows judge the indirection. If they flag it, the fallback (the struct embedded belowconsumed) needs the same placement-pinned and aligned evidence before it is chosen. They flagged it, and the fallback is what landed (#2930). bench-ab flagged four search rows twice; aligned, with heap placement pinned, three tied (placement) and one stayed:burst_acq push[sf=63,spc=4]at 1.031 and 1.025, psd and spectrogram controls at 1.000. Readingsearchonce per function (the push reloaded it 15 times) did not move it; embedding the record, assearch_recafter every measured field, did (1.000 and 1.004).searchstill names it, NULL in the other flavors, and every flavor carries the record's 1.5 KB. - the ring,
-
One hit record, widened.
dp_correlator_hit_tgains acq's hit record's fields in their order:cn0_dbhz_est,samples_consumed,chip_phase,doppler_rate_hz_s,test_stat_peakand the named_pad.doppler_binandcode_phasebecomerowandcol. The record grows from 32 to 64 bytes, acq's record's pinned size. The detectors write the new fields as 0, and their rows are judged again for the memory traffic. Python is unaffected: everypushreturns plain tuples, and each class'sresult_fieldspicks its fields by C name. -
Constructors and symbols. acq's continuous and burst constructors become the two search flavors. Of acq's other
dp_acq_*functions:- those whose name the correlator already has (
push,reset,consumed,set_threshold, the state triplet,destroy) are served by the one function, dispatching on the flavor; - the search-only ones keep their suffix and refuse every other flavor.
acq's record types become
dp_correlator_*_t, andacq_private.hfolds intocorrelator_private.h. - those whose name the correlator already has (
-
The blob.
ACQR(v8, released) is refused. A search flavor's blob isCORL: the header, the flavor (a u64, the reject key every flavor's blob opens with), then acq's payload as it was: the search extra block (the counts that are reject keys, and the stream position), the framer's own snapshot, the non-coherent accumulator whenn_noncoh > 1, the twins, and a tiled block's raw epochs. The framer is the one field it shares with the 3.3 flavors, and it is packed by the same primitive (dp_f32_framer_get_state); the rest is the search's own, so re-packing it the 3.3 way would move bytes for no reuse. ACQR's reject keys stay reject keys, each with a cross-load pair, andset_statechecks the whole blob before it commits.Blobs that nest acq's bump, each confirmed in the PR and held by
make state-versions-check:- directly:
BurstCapture(BCAP6 to 7),DsssReceiver(DSRX3 to 4),AsyncDsssReceiver(ADRX15 to 16), andburst_acq, whose blob is the engine's own and so isCORLnow; - through a receiver's child blob:
AsyncDsssPool(5 to 6) andDsssBurstReceiver(7 to 8).
- directly:
-
Acquisitionbecomes a cross-module view of the correlator. A scratchjm applyon 0.120.2 showed jm cannot point an object at another component's C state: reusing the constructor is refused as a duplicate symbol, and an object with its own constructor gets a state struct of its own. just-makeit#2516 decided the fix: a view may name its module (module = "acquire"). The class, its__module__and its stub stay indoppler.acquire; the view keeps its own constructor, parameters and members, and the parent'sdestroy,resetand state triplet. The correlator carriesset_state's memory-error row and the link dependencies. 3.4b waits on that jm release.BurstAcquisitionstays a forwarder until step 4.
The oracle:
-
an equivalence test, before the switch. acq has no second implementation to pair against, as 3.2 and 3.3 did, so the switch's first commit adds the search flavors while
acq/still builds.test_search_equiv.cdrives both through the same scenes:- both doors;
- one peak and four;
- rate hypotheses;
- the carrier coupled;
- a tiled search with D > 1;
- one thread and four;
- odd chunks;
- a mid-stream
get_state/set_state.
Every hit field and every dwell's statistic surface must be byte-identical, proven red by sabotage. The next commit deletes
acq/and the test's acq half; its scenes stay as the flavors' pins, each against its own plain run (the stream whole, one thread, no restore). The first commit's CI run is the evidence, and the PR links it. A continuous-against-burst pair is pinned only where the header documents that the two doors agree; -
test_acq_core.c,test_acq_regrid_failure.cand the symbol tests, ported; -
every acq validator's
--checkverdict unchanged, andmake validate-checkup to date; -
the composing objects' C and Python tests, and
tracker_through_window.c.
-
-
3.5, re-certification. One report for the one object, its limits the union of today's four reports. acq's two open findings stay open: finding
auto_sizer_short_push_emptyand findingsearched_doppler_unreported. Landed before 3.4b, so the move keeps one report's--checkrather than leaving report folders tied to a component it deletes:src/doppler/spectral/tests/validation/correlator/carries four parts (the map, the 2-D detector, the search, the burst door), 83 limits and 25 findings. A third open finding,continuous_search_thin_python(#2923), records that the continuous search has one limit of its own there.
7.1 The speed rows, with their budgets¶
Every row is timed by make bench-ab on the bench box: A is main before the
sub-step, B its head, K = 5, per-row min-of-K. The unit is µs per call as
the bench reports it, and the number is A's min, written into the PR beside
B's. The budget is no SLOWER verdict on any row, where a verdict counts
only if the same row is flagged twice (step 2's rule). A row that slows
within budget is stated with its ratio, never left unsaid.
The rows, by family, as benchmarks/published/ names them (the 3.1b rows
join once they land):
| sub-step | family | rows |
|---|---|---|
| 3.2 | corr::execute |
[accumulate] and [dump], each at [n=1024] and [n=16384]; 3.1b's [n=31], [n=63], [n=127] |
| 3.2 | corr2d:: single shapes |
single_row_fast_path, multi_row_general_path |
| 3.2 | corr2d::delay_doppler_ |
32x1024_1t, 256x4096_1t, 256x4096_all |
| 3.2 | corr2d::batch_64x512_h48_ |
execute_loop_1t, all_1t, power_max_1t, execute_loop_all, all_all, power_max_all |
| 3.2 | corr2d::bank_64x512_ |
c12_corrected_batch_1t, c12_l4_1t, c6_l2_1t, detect_l2_l4_1t |
| 3.3 | detector::push |
[chunk=64], [chunk=1024], [chunk=16384], [chunk=1024,no-detections]; 3.1b's [frame=31], [frame=63], [frame=127] |
| 3.3 | detector2d::push |
[16x1024], [128x128], [1024x16], [128x128,no-detections] |
| 3.3 | carrier_acq::detect |
[pad=4], [pad=16] |
| 3.3 | async_dsss_pool::push |
[full,n=4,threads=4,carrier], whose path runs through carrier_acq's detector; and every 3.2 row again, since the state grows |
| 3.4 | acq:: |
native_nc90, op5M_U50k_nc254, op2M_U50k_nc57, op5M_U5k_nc225, op2M_U5k_nc51, op5M_U50k_t8_nc254, op5M_U50k_D150_t1_nc1, op5M_U50k_D150_t8_nc1, op2M_U50k_D60_t1_nc1, op2M_U50k_D60_t4_nc1, op5M_U5k_D150_t1_nc1, op5M_U5k_D150_t4_nc1 |
| 3.4 | async_dsss_pool::push |
[native,n=1,threads=1], [native,n=4,threads=1], [native,n=4,threads=4], [full,n=1,threads=1], [full,n=4,threads=1] |
| 3.4 | async_dsss_receiver::steps |
[track,sf=1023,3.069Mcps], [search,sf=1023,3.069Mcps] |
| 3.4 | async_dsss_receiver::create |
[search,sf=1023,3.069Mcps,W=5], one receiver a call, a five-tile deep search: the Doppler-sidelobe sweep's cost (3.4a's A for 3.4b) |
| 3.4 | burst_capture::push |
[4 bursts] |
| 3.4 | burst_acq::push |
[sf=15,spc=2], [sf=63,spc=4] |
| 3.4 | the 3.3 detector rows | every detector::push and detector2d::push row again, since the hit record grows to 64 bytes |
Row suffixes stay through step 3; only the family changes, where jm
renames a bench binary. 3.2 moves
both correlator benches into one binary, bench_correlator_core, because
jm runs a component's bench by its name and corr is no longer a
component: the 1-D bench is a second source of that binary, not an
executable of its own, which jm would build and never run. 3.3 does the
same for the two detector benches, which become sources of
bench_correlator_core with their row suffixes unchanged.
3.2 and 3.3 were judged with ALIAS= pairings (#2884, #2889) that
benchmarks/renames.toml now declares (#2900): c:corr, c:corr2d,
c:detector, c:detector2d and c:acq each map to c:correlator.
make bench-ab keys every row by the page's row ID and pairs a renamed
row with its new ID through that table by itself, so a sub-step's A/B is
plain make bench-ab A=<main> B=<head>. The table refuses a pairing that
would make two rows of one run one. Step 4 renames the rest once, to
correlator:: with each row's flavor in its brackets, adds those entries
to the same table, and relabels them in benchmarks/labels.toml.
acq's carrier and five-tile rows from step 2 (op5M_U10k_D150_t1, the
_fc rows) are on main's bench but not yet in a published set; 3.4's PR
names them with the rest.
8. Not in step 3¶
BurstAcquisition: a second cross-module view of the correlator, made with the burst search flavor's constructor, would deleteburst_acq's C forwarder outright; its blob is already the engine's. Recommended for step 4, where the Python classes are decided (3.4b keeps the forwarder).- The Python
Correlatorclass, and retiringAcquisition,BurstAcquisitionand theburst_acqforwarder: step 4. - Folding
PolynomialPhaseEstimatorin, and renamingCarrierAcquisitiontoCarrierEstimator: they gate on their own checks (taxonomy §6.4).