File carrier_nda_core.h¶
FileList > carrier_nda > carrier_nda_core.h
Go to the source code of this file
Non-data-aided (NDA) M-th-power carrier-tracking loop. More...
#include "boxcar/boxcar_core.h"#include "clib_common.h"#include "dp_state.h"#include "jm_perf.h"#include "lo/lo_core.h"#include "lockdet/lockdet_core.h"#include "loop_filter/loop_filter_core.h"#include "dp_tlm/dp_tlm_core.h"#include <math.h>#include "telemetry/telemetry_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | carrier_nda_state_t NDA M-th-power carrier loop state. |
| struct | carrier_nda_tlm_t Telemetry attachment: a borrowed context + this object's probe ids. NULL ctx (the default) means detached — the probe site is then a single predicted-not-taken branch per block loop. Zeroed in state blobs and preserved across set_state (DP_DEFINE_POD_STATE_TLM). |
Public Functions¶
| Type | Name |
|---|---|
| JM_FORCEINLINE JM_HOT int | carrier_nda_arm_step (carrier_nda_state_t * s, float _Complex d, double * pe, double * lock) Slide the moving-average arm by one sample; discriminate the output. |
| void | carrier_nda_configure_lock (carrier_nda_state_t * state, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down) Re-tune the carrier lock detector's geometry directly. |
| carrier_nda_state_t * | carrier_nda_create (double bn, double zeta, double init_norm_freq, size_t sps, int n, int m) Create an NDA carrier loop instance. |
| void | carrier_nda_destroy (carrier_nda_state_t * state) Destroy an NDA carrier loop instance and release all memory. |
| JM_FORCEINLINE void | carrier_nda_disc (float _Complex z, int m, double * pe, double * lock) The M-th-power discriminator on an arm sample, normalized by its own amplitude law. |
| double | carrier_nda_get_bn (const carrier_nda_state_t * state) |
| double | carrier_nda_get_last_error (const carrier_nda_state_t * state) |
| double | carrier_nda_get_lock (const carrier_nda_state_t * state) |
| int | carrier_nda_get_locked (const carrier_nda_state_t * state) Current lock decision (1 = locked, 0 = not), with the configured verify-count / hysteresis rule applied (see carrier_nda_configure_lock). |
| int | carrier_nda_get_m (const carrier_nda_state_t * state) |
| int | carrier_nda_get_n (const carrier_nda_state_t * state) |
| double | carrier_nda_get_nco_freq (const carrier_nda_state_t * state) Instantaneous NCO frequency command = centre + full loop-filter output (integ + kp*e), cycles/sample. Mean rides a ramp with no lag; variance is the loop stress. See the impl for the estimator-vs-command distinction. |
| double | carrier_nda_get_norm_freq (const carrier_nda_state_t * state) |
| size_t | carrier_nda_get_sps (const carrier_nda_state_t * state) |
| void | carrier_nda_get_state (const carrier_nda_state_t * state, void * blob) Serialize the full loop state into blob . |
| void | carrier_nda_init (carrier_nda_state_t * s, double bn, double zeta, double init_norm_freq, size_t sps, int n, int m) Initialise an NDA carrier loop in place (no allocation). |
| void | carrier_nda_reset (carrier_nda_state_t * state) Re-seed the loop to its create-time frequency/phase; keep config. |
| void | carrier_nda_set_bn (carrier_nda_state_t * state, double val) |
| void | carrier_nda_set_norm_freq (carrier_nda_state_t * state, double val) |
| int | carrier_nda_set_state (carrier_nda_state_t * state, const void * blob) Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects. |
| int | carrier_nda_set_telemetry (carrier_nda_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim) Attach (or detach) a telemetry context and register the carrier loop's probes on it. Registers four probes, emitted once per input sample (this is a sample-rate loop — use decim to thin the stream): "<prefix>.lock" (the lock-signal EMA, ~1 when phase-locked), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier frequency, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). Passing NULL detaches. Setup path, never hot: call before the producer thread starts stepping; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ). |
| size_t | carrier_nda_state_bytes (const carrier_nda_state_t * state) Serialized-state byte size. |
| JM_FORCEINLINE JM_HOT void | carrier_nda_steer (carrier_nda_state_t * s, double pe) Steer the shared NCO with a phase error through the loop filter. |
| size_t | carrier_nda_steps (carrier_nda_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out) De-rotate a cf32 block with the recovered carrier and return the de-rotated stream (one output per input sample). |
| size_t | carrier_nda_steps_max_out (carrier_nda_state_t * state) |
| void | carrier_nda_tlm_flush (const carrier_nda_state_t * s) Emit the carrier loop's telemetry records for the current sample. |
| JM_FORCEINLINE JM_HOT float _Complex | carrier_nda_wipeoff (carrier_nda_state_t * s, float _Complex x) Per-sample carrier wipe-off: de-rotate x by the NCO, advance it. |
Macros¶
| Type | Name |
|---|---|
| define | CARRIER_NDA_EPS 1e-12 |
| define | CARRIER_NDA_INV_2PI 0.15915494309189535 /\* 1 / (2\*pi) \*/ |
| define | CARRIER_NDA_LOCK_ALPHA 0.05 |
| define | CARRIER_NDA_LOCK_NORM_SD 0.11322770341445956 |
| define | CARRIER_NDA_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('C', 'N', 'D', 'A') |
| define | CARRIER_NDA_STATE_VERSION /* multi line expression */ |
Detailed Description¶
A carrier-recovery loop that locks without data and without symbol timing — the cold-start / acquisition counterpart to the decision-directed carrier_mpsk_state_t loop. Per sample it de-rotates the input with the integer-phase lo_state_t NCO (carrier wipe-off); it filters the de-rotated samples through a free-running I/Q boxcar moving average of sps/n samples (one output per input sample — no rate change), and on every sample runs the M-th-power phase discriminator, filters the error through an embedded loop_filter_state_t, and steers the NCO frequency + phase.
Raising the arm sample z to the Mth power strips the M-PSK data modulation, leaving M times the carrier phase — so the discriminator is independent of the data symbols and of symbol timing. That is what lets it acquire a bare/unmodulated carrier, or a modulated carrier before timing lock. It is the M-fold-ambiguous acquisition aid; a decision-directed loop gives the low-jitter steady state (resolve the M-fold ambiguity downstream).
The M-th power is computed by repeated complex squaring (z²→z⁴→z⁸) of the unit-magnitude sample z/|z|. Each level yields a phase error and a lock signal:
* phase_error = Im((z/|z|)^M) scaled by 1, ½, ¼ for M = 2, 4, 8 — the scale normalizes the phase-detector gain so the S-curve slope at lock is 2 for every M (one bn behaves identically across M).
* lock_signal = Re((z/|z|)^M) — the M-th power of a limited sample, so it is bounded in ±1 and its H0 variance is 1/2 for every M. ~1 when phase-locked, zero-mean with no carrier. That M-independence is what makes one lock_thresh mean one Pfa at every order; the threshold chain is derived above CARRIER_NDA_LOCK_ALPHA. Its EMA (lock) is the carrier lock metric. See docs/design/mpsk.md §4.2, "Limiting — what makes the
threshold a Pfa", for the derivation.
The block API (carrier_nda_steps) is the Python face and emits the de-rotated sample stream; the JM_FORCEINLINE carrier_nda_wipeoff()/_arm_step()/_steer() are the C composition API a receiver inlines into its own sample loop (it can also steer the shared NCO with its own decision-directed error on handover).
Note:
The input SCALE does not matter, and there is no AGC here. The discriminator divides out its own amplitude law (|z|^M) exactly, so both outputs — and with them the loop gain — are invariant to input scale over the whole float range: measured identical to 6.5e-7 relative from an amplitude of 1e-5 to 1e15, at every M a few float eps, which is the rounding floor a float detector has and not a level dependence. The measurement is test_carrier_nda_core.c section 9; this file carried a tighter 5e-7 that the test itself had already corrected, so the number here now comes from the thing that runs it. This loop used to embed a slow arm AGC whose only job was to manufacture |z| = 1 so a raw Im(z^M) would behave; that condition no longer has to be manufactured, and the AGC is gone. A receiver needs exactly one AGC, for its own signal path, and not one per detector (docs/design/mpsk.md §3.2, "The NDA discriminator +
lock signal").
SCALE is not Es/N0, and only the first of them is invariant here. Section 9 scales a clean phasor, so it holds signal and noise in the same ratio; that is homogeneity of degree zero, plus — the reason the test earns its place — a float-RANGE gate, since forming |z|^M at the end instead returns 0 below |z| = 0.032 and NaN above 1e4 at M = 8. It says nothing about level relative to NOISE, and it should not be read as though it did: per-sample division by the instantaneous |s+n| is a hard limiter, so the S-curve slope genuinely does depend on Es/N0. That dependence is measured, as loop SNR against the un-normalised form across 0–20 dB and every M, in docs/design/mpsk.md §3.2 — the penalty is real at 0–3 dB, where the link cannot be closed anyway, and from ~6 dB up normalising is equal or better. This distinction was read the other way once (gh-795), which is why it is spelled out rather than implied.
** **
A caller must start the residual carrier inside a window, and there are two separate limits on that window. Which one binds decides whether a violation is obvious or silent, so both belong here rather than only in the design of whatever composes this.
Loop capture, roughly k * bn / M. Where pull-in is prompt and predictable. This is the tighter bound at every shipped setting and it is the one a caller normally meets. Violating it is LOUD: the tracked frequency walks and locked never asserts, which is unmistakable in telemetry.
The aliasing ceiling, 1/(2M) cycles per sample. The discriminator runs once per input sample, so an M-th power folds when the residual advances more than half a cycle per update i.e. at a residual of 1/(2M) cyc/sample, with stable false locks spaced 1/M apart. The M-fold ambiguity is therefore a FREQUENCY ambiguity as well as a phase one. Violating this is SILENT and it is this object's worst failure mode: the loop sits still at the wrong frequency and reports a healthy lock statistic measured on QPSK at a residual of one quarter the update rate, tracked frequency 2e-6 against a true 0.03125 and a lock statistic of +0.83 against the ~1.0 a real lock reads. Nothing self-referenced detects it: the constellation is stationary, so EVM and blind M2M4 both look clean. It takes an external frequency reference, a sync word, or a coarse estimate seeded through init_norm_freq.
Both scale as 1/M, so an 8PSK caller needs four times tighter tuning than BPSK at the same bandwidth. See docs/design/mpsk.md §3.4 and §3.5.
** **
The ceiling is linear in the rate the discriminator updates at, and nothing else in the loop depends on that rate so it is free margin for whoever picks it. Stepping this object per input sample, which is what carrier_nda_steps does, is the widest setting available and puts the ceiling tens of times beyond loop capture. A composer that instead taps a DECIMATED stream inherits a proportionally tighter ceiling: at two samples per symbol it falls to one symbol rate over M, which is the regime the QPSK false lock above was measured in. The rule for a composition is to choose the tap so the ceiling stays clear of loop capture at every M it supports, which makes the silent failure structurally unreachable and leaves only the loud one.
** **
n sets the arm window to sps / n samples, and the window is not free: the arm averages across data transitions, so its coherent gain after the M-th power falls as the window widens. For the half-symbol arm (n = 2) the gain is 1/2 + 1/(M+1) 5/6 at BPSK, 7/10 at QPSK, 11/18 at 8PSK against unity for a constant-modulus input. A caller choosing n is choosing a sensitivity loss, and the validation report is the evidence for what each choice costs.
// QPSK NDA carrier loop, 8 samples/symbol, 2-sample moving-average arm
carrier_nda_state_t *c = carrier_nda_create(0.01, 0.707, 0.0, 8, 4, 4);
float _Complex derot[1024];
size_t k = carrier_nda_steps(c, rx, rx_len, derot, 1024);
double f = carrier_nda_get_norm_freq(c); // tracked carrier (cyc/sample)
carrier_nda_destroy(c);
Public Functions Documentation¶
function carrier_nda_arm_step¶
Slide the moving-average arm by one sample; discriminate the output.
JM_FORCEINLINE JM_HOT int carrier_nda_arm_step (
carrier_nda_state_t * s,
float _Complex d,
double * pe,
double * lock
)
The arm is a free-running boxcar moving average of the last arm_len de-rotated samples — one output per input sample, no rate change (not a decimating integrate-and-dump). It updates the running window sum in O(1) (add d, subtract the sample leaving the window), runs the M-th-power discriminator on the window average, writes pe and lock, and returns 1 every call.
Parameters:
sCarrier loop state. Must be non-NULL.dOne de-rotated sample (from carrier_nda_wipeoff).peReceives the phase error.lockReceives the lock signal.
Returns:
Always 1 (one discriminator output per input sample).
function carrier_nda_configure_lock¶
Re-tune the carrier lock detector's geometry directly.
void carrier_nda_configure_lock (
carrier_nda_state_t * state,
double up_thresh,
double down_thresh,
uint32_t n_up,
uint32_t n_down
)
Full lockdet control, mirroring costas_configure_lock(): a split declare/drop threshold pair on the lock-signal EMA (level hysteresis) and both verify counts (time hysteresis). Defaults (0.5/0.4, 64 up / 32 down) start from MpskReceiver's own pre-existing acquisition<-> tracking handover thresholds, but size n_up independently: lock is a fast per-sample EMA, so consecutive looks are highly autocorrelated and MpskReceiver's own n_up=8 does not compound the false-declare rate the way it would for independent looks (direct Monte Carlo against a noise-only, no-carrier input found real false locks at n_up=8; n_up=64 was the smallest verify count that reliably eliminated them see carrier_nda_core.c's CARRIER_NDA_LOCK_DEFAULT_* comment for the exact trial data). A live lock survives the re-tune; the in-flight verify run restarts.
Parameters:
stateMust be non-NULL.up_threshDeclare threshold on the lock-signal EMA.down_threshDrop threshold; choose <= up_thresh for level hysteresis.n_upConsecutive above-threshold samples to declare; clamped >= 1.n_downConsecutive below-threshold samples to drop; clamped >= 1.
function carrier_nda_create¶
Create an NDA carrier loop instance.
carrier_nda_state_t * carrier_nda_create (
double bn,
double zeta,
double init_norm_freq,
size_t sps,
int n,
int m
)
Parameters:
bnLoop noise bandwidth (default 0.01).zetaDamping factor (default 0.707).init_norm_freqSeed carrier frequency, cycles/sample (default 0.0).spsSamples per symbol (default 8).nMA window divisor: window = sps/n (default 4; spsn==0).mConstellation order M, 2/4/8 (default 4 = QPSK).
Returns:
Heap-allocated state, or NULL on invalid args / allocation failure.
Note:
Caller must call carrier_nda_destroy() when done.
function carrier_nda_destroy¶
Destroy an NDA carrier loop instance and release all memory.
Parameters:
stateMay be NULL.
function carrier_nda_disc¶
The M-th-power discriminator on an arm sample, normalized by its own amplitude law.
Runs the repeated-squaring recursion z²→z⁴→z⁸ on the unit- magnitude sample z/|z| and writes the phase error (= scaled Im((z/|z|)^M)) and the lock signal (Re((z/|z|)^M)). Both outputs are therefore invariant to the input's scale, which is what lets this loop run with no AGC in front of it — see the amplitude note at the top of this file, and docs/design/mpsk.md §3.2, "The NDA discriminator + lock
signal", for the squaring-loss measurement that says normalizing is equal or better from ~6 dB Es/N0 up: loop SNR against the raw form, 4e5 samples per point, tabulated over 0–20 dB and M = 2/4/8.
Parameters:
zArm moving-average sample (any scale; only its phase is used).mConstellation order (2, 4, 8).peReceives the phase error.lockReceives the lock signal.
function carrier_nda_get_bn¶
function carrier_nda_get_last_error¶
function carrier_nda_get_lock¶
function carrier_nda_get_locked¶
Current lock decision (1 = locked, 0 = not), with the configured verify-count / hysteresis rule applied (see carrier_nda_configure_lock).
function carrier_nda_get_m¶
function carrier_nda_get_n¶
function carrier_nda_get_nco_freq¶
Instantaneous NCO frequency command = centre + full loop-filter output (integ + kp*e), cycles/sample. Mean rides a ramp with no lag; variance is the loop stress. See the impl for the estimator-vs-command distinction.
function carrier_nda_get_norm_freq¶
function carrier_nda_get_sps¶
function carrier_nda_get_state¶
Serialize the full loop state into blob .
function carrier_nda_init¶
Initialise an NDA carrier loop in place (no allocation).
void carrier_nda_init (
carrier_nda_state_t * s,
double bn,
double zeta,
double init_norm_freq,
size_t sps,
int n,
int m
)
Parameters:
sState to initialise. Must be non-NULL.bnLoop noise bandwidth, cycles/sample (per-sample loop).zetaDamping factor (0.707 = critically damped).init_norm_freqSeed carrier frequency, cycles/sample.spsSamples per symbol.nMA window divisor: window = sps/n samples (sps % n == 0, sps/n <= BOXCAR_MAX_LEN).mConstellation order M (2, 4, 8).
function carrier_nda_reset¶
Re-seed the loop to its create-time frequency/phase; keep config.
Restores the object to its post-create state: the carrier NCO is reset to the seed frequency it was constructed with (init_norm_freq) with zero phase, the moving-average arm, the loop-filter integrator and the lock EMA are cleared, and the lock detector is dropped. The configured (bn, zeta), the arm geometry (sps, n) and the constellation order m are preserved, so the same object can re-acquire a fresh capture.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.track import CarrierNda >>> c = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0, ... sps=8, n=4, m=4) >>> rng = np.random.default_rng(0) >>> k = np.arange(40000) >>> x = (np.exp(2j * np.pi * 0.001 * k) + 0.05 * ( ... rng.standard_normal(k.size) ... + 1j * rng.standard_normal(k.size))).astype(np.complex64) >>> _ = c.steps(x) >>> round(c.norm_freq, 4), round(c.lock, 2) # acquired the carrier (0.001, 0.99) >>> c.reset() >>> round(c.norm_freq, 4), round(c.lock, 2) # back to seed, unlocked (0.0, 0.0)
function carrier_nda_set_bn¶
function carrier_nda_set_norm_freq¶
function carrier_nda_set_state¶
Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.
function carrier_nda_set_telemetry¶
Attach (or detach) a telemetry context and register the carrier loop's probes on it. Registers four probes, emitted once per input sample (this is a sample-rate loop — use decim to thin the stream): "<prefix>.lock" (the lock-signal EMA, ~1 when phase-locked), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier frequency, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). Passing NULL detaches. Setup path, never hot: call before the producer thread starts stepping; the context is borrowed and must outlive the attachment (SPSC rules indp_tlm/dp_tlm_core.h ).
int carrier_nda_set_telemetry (
carrier_nda_state_t * state,
dp_tlm_t * tlm,
const char * prefix,
uint32_t decim
)
Parameters:
stateMust be non-NULL.tlmTelemetry context to attach, or NULL to detach.prefixProbe-name prefix, e.g. "car" or "rx.car".decimEmit every decim-th sample; >= 1.
Returns:
DP_OK, or DP_ERR_INVALID when the probe table cannot take all four probes (the attach fails whole; everything stays detached).
>>> import numpy as np
>>> from doppler.track import CarrierNda
>>> from doppler.telemetry import Telemetry
>>> tlm = Telemetry(1 << 14)
>>> c = CarrierNda(bn=0.01, sps=8, n=4, m=4)
>>> c.set_telemetry(tlm, "car", decim=8)
>>> sorted(tlm.probe_names)
['car.e', 'car.freq', 'car.lock', 'car.locked']
>>> x = np.exp(2j * np.pi * 0.005 * np.arange(4096)).astype(
... np.complex64)
>>> _ = c.steps(x)
>>> recs = tlm.read()
>>> len(recs[recs["probe"] == tlm.probe_id("car.e")]) == 4096 // 8
True
function carrier_nda_state_bytes¶
Serialized-state byte size.
function carrier_nda_steer¶
Steer the shared NCO with a phase error through the loop filter.
Filters pe and updates the NCO frequency (per sample) + a proportional phase nudge. Shared by the NDA acquisition path and a composing receiver's decision-directed tracking path (handover writes the same NCO).
Parameters:
sCarrier loop state. Must be non-NULL.pePhase error (NDA discriminator, or a decision-directed error).
function carrier_nda_steps¶
De-rotate a cf32 block with the recovered carrier and return the de-rotated stream (one output per input sample).
size_t carrier_nda_steps (
carrier_nda_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
Runs the non-data-aided carrier loop over the block: each sample is wiped off by the integer-phase NCO, the de-rotated sample slides the I/Q moving-average arm, and the M-th-power discriminator (which strips the M-PSK data modulation) steers the NCO frequency and phase. Because the discriminator is data- and timing-independent, this acquires the carrier with no symbol timing and no data present — a bare carrier, or a modulated carrier before timing lock. It resolves to one of m carrier phases (M-fold ambiguity, resolved downstream). Read norm_freq for the tracked carrier (cycles/sample) and lock for the carrier lock metric.
Parameters:
stateMust be non-NULL.xInput samples (average power at or below unity).x_lenNumber of input samples.outDe-rotated samples, one per input.max_outCapacity ofout.
Returns:
Number of de-rotated samples written to out (equals x_len).
>>> import numpy as np
>>> from doppler.track import CarrierNda
>>> c = CarrierNda(bn=0.01, zeta=0.707, init_norm_freq=0.0,
... sps=8, n=4, m=4)
>>> rng = np.random.default_rng(0)
>>> k = np.arange(40000)
>>> x = (np.exp(2j * np.pi * 0.001 * k) + 0.05 * (
... rng.standard_normal(k.size)
... + 1j * rng.standard_normal(k.size))).astype(np.complex64)
>>> y = c.steps(x) # de-rotated toward DC
>>> y.shape[0]
40000
>>> round(c.norm_freq, 4) # tracked carrier, cycles/sample
0.001
>>> c.lock > 0.5 # carrier lock metric, ~1 at lock
True
function carrier_nda_steps_max_out¶
function carrier_nda_tlm_flush¶
Emit the carrier loop's telemetry records for the current sample.
Out-of-line on purpose: the emit machinery must not inline into the per-sample hot loop (inlined ring-write expansions bloat the loop body and an extern call site forces per-iteration state reloads — both measured ~20% slower detached on other loops). Callers gate on s->tlm.ctx. This loop updates every sample, so the natural call rate is per sample — decim (set at attach) is the throttle. Records "<prefix>.lock" (the lock-signal EMA), "<prefix>.e" (the M-th-power phase discriminator — the loop stress), "<prefix>.freq" (the tracked carrier, NCO centre + integrated correction, cycles/sample) and "<prefix>.locked" (the verify-counted lockdet decision, 0/1). A composing receiver (the MPSK receiver) calls this once per recovered symbol instead.
Parameters:
sState with a non-NULL tlm.ctx (caller-checked).
function carrier_nda_wipeoff¶
Per-sample carrier wipe-off: de-rotate x by the NCO, advance it.
JM_FORCEINLINE JM_HOT float _Complex carrier_nda_wipeoff (
carrier_nda_state_t * s,
float _Complex x
)
Parameters:
sCarrier loop state. Must be non-NULL.xOne input sample.
Returns:
The de-rotated sample to feed the moving-average arm.
Macro Definition Documentation¶
define CARRIER_NDA_EPS¶
define CARRIER_NDA_INV_2PI¶
define CARRIER_NDA_LOCK_ALPHA¶
define CARRIER_NDA_LOCK_NORM_SD¶
define CARRIER_NDA_STATE_MAGIC¶
define CARRIER_NDA_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/carrier_nda/carrier_nda_core.h