File lockdet_core.h¶
FileList > inc > lockdet > lockdet_core.h
Go to the source code of this file
Portable lock detector — level + time hysteresis over any scalar lock metric, embeddable in every loop that makes a lock decision. More...
#include "clib_common.h"#include "dp_state.h"#include "jm_perf.h"#include "util/util_core.h"#include <math.h>
Classes¶
| Type | Name |
|---|---|
| struct | lockdet_state_t Lock-detector state (embeddable by value; pointer-free POD). |
Public Functions¶
| Type | Name |
|---|---|
| void | lockdet_configure (lockdet_state_t * state, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down) Re-tune thresholds and verify counts; preserve the decision. |
| lockdet_state_t * | lockdet_create (double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down) Create a lockdet instance. |
| void | lockdet_destroy (lockdet_state_t * state) Destroy a lockdet instance and release all memory. |
| void | lockdet_get_state (const lockdet_state_t * state, void * blob) Serialize the detector state into blob . |
| void | lockdet_init (lockdet_state_t * state, double up_thresh, double down_thresh, uint32_t n_up, uint32_t n_down) Initialise a lock detector in place (no allocation). |
| void | lockdet_reset (lockdet_state_t * state) Drop the lock and clear the verify counter; keep the config. Returns the detector to the unlocked state with an empty verify run, as if freshly constructed with the same thresholds. Call it at a segment boundary so a decision made on one capture does not leak into an unrelated next one. |
| int | lockdet_set_state (lockdet_state_t * state, const void * blob) Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects. |
| size_t | lockdet_state_bytes (const lockdet_state_t * state) Serialized-state byte size. |
| JM_FORCEINLINE JM_HOT int | lockdet_step (lockdet_state_t * state, double x) Feed one look of the lock metric; return the current decision. |
| void | lockdet_steps (lockdet_state_t * state, const double * x, int * out, size_t n) Run a block of lock-metric looks through the detector. Applies lockdet_step() to each look in turn, so the decision flag and the in-flight verify run carry across the block exactly as they would look by look — a signal can be processed in frames of any size with no seam. |
Macros¶
| Type | Name |
|---|---|
| define | LOCKDET_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc)('L', 'K', 'D', 'T') |
| define | LOCKDET_STATE_VERSION 1u |
Detailed Description¶
A tracking loop that computes a lock statistic (a CFAR ratio, a coherence metric, an error variance) still needs a decision rule: when is the statistic "high enough, long enough" to declare lock, and "low enough, long enough" to drop it? This component is that rule, factored out once:
- Level hysteresis: separate declare (
up_thresh) and drop (down_thresh) thresholds. Withup_thresh >= down_threshthe band between them is sticky in both directions — a metric wobbling around a single threshold cannot chatter the flag. - Time hysteresis:
n_upconsecutive looks aboveup_threshto declare,n_downconsecutive looks belowdown_threshto drop. A single contrary look resets the run (consecutive, not cumulative), so the verify counts compose probabilistically. At per-look false-alarm rate p the false-declare rate per look isp^n_up * (1 - p) / (1 - p^n_up), whose reciprocal is exactly det_verify_delay(p, n_up), the mean looks to a declare.p^n_upalone is the p -> 0 limit of that, and is what det_verify_count() sizes against correct to 0.001% at p = 1e-5, 10% at p = 0.1, and +87% at p = 0.5 with n_up = 4. Use it as the budget (it errs high, so it over-provisions n_up) and det_verify_delay() for the number a caller actually observes. Measured across p from 0.1 to 0.5 and n_up from 1 to 4: native/validation/lockdet_verify.c.(Both the formula and those ranges are written without an indented block or square brackets on purpose: mkdoxy renders this comment into markdown, where an indented line is swallowed into the paragraph before it and a barep in [0.1, 0.5]parses as a link reference and fails the strict docs build.) - Non-finite looks: a NaN look is a miss in both states — it never advances a declare, and while locked it advances the drop run like any other miss, so a metric that goes NaN drops the lock after
n_downrather than holding it lit. An unknown lock is not a lock. The policy is not implemented here: the look is passed through util_core.h's saturate(), whosenan_toparameter documents a lock statistic as the caller that wants the floor. Only NaN is unordered — the infinities are ordinary looks (+inf a hit, -inf a miss), and the exclusive edges are unchanged.
The state struct is public so a tracker embeds it by value (no heap) and drives it with lockdet_init()/lockdet_step() — e.g. the DLL steps one on its CFAR statistic each N-look decision, the MPSK receiver steps one on the carrier lock metric each recovered symbol. lockdet_create() is the heap path used by the Python wrapper. Pointer-free POD: it rides an embedding composer's whole-struct state snapshot with no extra packing.
Lifecycle: create -> (step / steps / configure / reset)* -> destroy
lockdet_state_t d;
lockdet_init (&d, 1.5, 1.2, 2, 3); // declare: 2 looks > 1.5
lockdet_reset (&d); // cnt = 0, locked = 0
int locked = lockdet_step (&d, metric); // one look -> current flag
Public Functions Documentation¶
function lockdet_configure¶
Re-tune thresholds and verify counts; preserve the decision.
void lockdet_configure (
lockdet_state_t * state,
double up_thresh,
double down_thresh,
uint32_t n_up,
uint32_t n_down
)
The current locked flag survives (a live lock is not dropped by a re-tune); the in-flight verify counter is cleared so the next run is counted entirely under the new config.
Parameters:
stateMust be non-NULL.up_threshDeclare threshold (hit when metric > up_thresh).down_threshDrop threshold (miss when metric < down_thresh).n_upConsecutive hits to declare; clamped to >= 1.n_downConsecutive misses to drop; clamped to >= 1.
function lockdet_create¶
Create a lockdet instance.
lockdet_state_t * lockdet_create (
double up_thresh,
double down_thresh,
uint32_t n_up,
uint32_t n_down
)
Parameters:
up_threshDeclare threshold (hit when metric > up_thresh).down_threshDrop threshold (miss when metric < down_thresh).n_upConsecutive hits to declare; clamped >= 1 (default 1).n_downConsecutive misses to drop; clamped >= 1 (default 1).
Returns:
Heap-allocated state, or NULL on allocation failure.
Note:
Caller must call lockdet_destroy() when done.
function lockdet_destroy¶
Destroy a lockdet instance and release all memory.
Parameters:
stateMay be NULL.
function lockdet_get_state¶
Serialize the detector state into blob .
function lockdet_init¶
Initialise a lock detector in place (no allocation).
void lockdet_init (
lockdet_state_t * state,
double up_thresh,
double down_thresh,
uint32_t n_up,
uint32_t n_down
)
Stores the thresholds and verify counts (each count clamped to >= 1; a count of 1 means no time hysteresis on that side). Does not touch cnt / locked, so it doubles as a reconfigure that preserves the current decision. Use this for a lockdet_state_t embedded by value; lockdet_create() is calloc + lockdet_init().
Parameters:
stateMust be non-NULL.up_threshDeclare threshold (hit when metric > up_thresh).down_threshDrop threshold (miss when metric < down_thresh); choose <= up_thresh for level hysteresis.n_upConsecutive hits to declare; clamped to >= 1.n_downConsecutive misses to drop; clamped to >= 1.
function lockdet_reset¶
Drop the lock and clear the verify counter; keep the config. Returns the detector to the unlocked state with an empty verify run, as if freshly constructed with the same thresholds. Call it at a segment boundary so a decision made on one capture does not leak into an unrelated next one.
Parameters:
stateMust be non-NULL.
function lockdet_set_state¶
Restore state; DP_OK, or DP_ERR_INVALID if the envelope rejects.
function lockdet_state_bytes¶
Serialized-state byte size.
function lockdet_step¶
Feed one look of the lock metric; return the current decision.
Unlocked: a hit (x > up_thresh) advances the verify run and the n_up-th consecutive hit declares lock; any miss resets the run. Locked: a miss (x < down_thresh) advances the run and the n_down-th consecutive miss drops the lock; any hit (x >= down_thresh) resets it. A metric inside the [down_thresh, up_thresh] band is sticky — it neither advances a declare nor a drop.
A non-finite look is a miss in both states: it never advances a declare, and while locked it advances the drop run like any other miss. An unknown lock is not a lock, which is the rule util_core.h states for lock statistics generally. So a metric that goes NaN drops the lock after n_down looks rather than holding it lit indefinitely.
Parameters:
stateMust be non-NULL.xLock metric for this look. Non-finite counts as a miss.
Returns:
Decision after this look (1 = locked, 0 = not).
>>> from doppler.detection import LockDet
>>> d = LockDet(up_thresh=1.5, down_thresh=1.2, n_up=2, n_down=3)
>>> [d.step(2.0), d.step(2.0)] # declared on the 2nd straight hit
[0, 1]
>>> d.step(1.3) # in the hysteresis band: stays up
1
>>> [d.step(1.0), d.step(1.0), d.step(1.0)] # 3rd straight miss drops
[1, 1, 0]
function lockdet_steps¶
Run a block of lock-metric looks through the detector. Applies lockdet_step() to each look in turn, so the decision flag and the in-flight verify run carry across the block exactly as they would look by look — a signal can be processed in frames of any size with no seam.
Parameters:
stateComponent state (mutated). Must be non-NULL.xLock-metric looks, one scalar per look (length >= n).outPer-look decision output, 0 or 1 (length >= n).nNumber of looks to process.
Macro Definition Documentation¶
define LOCKDET_STATE_MAGIC¶
define LOCKDET_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/lockdet/lockdet_core.h