File wfm_plan.h¶
File List > inc > wfm > wfm_plan.h
Go to the documentation of this file
/*
* wfm_plan.h — "prepare once, materialize many" stimulus engine.
*
* A composed scene is a sequence of segments, each a linear form
*
* segment = Σ_k gain_k · signal_k + noise
*
* (wfm_resolve_noise() cleans every signal source and appends one
* WFM_SYNTH_NOISE source at the shared floor, for a multi-source segment; a
* lone source carrying its own real SNR keeps it — its AWGN is baked into
* its own synth, "bundled"). The expensive DSP — LFSR spread, RRC
* convolution, transcendental LO — lives entirely in the signal terms,
* which are INVARIANT across a parameter sweep and across a segment's
* `repeats` instances (only the AWGN, and any ranged gap length, vary per
* instance). So a Plan renders each segment's signal ON-time ONCE (via the
* composer's own wfm_compose_build_synth, so a cached render is
* byte-identical to a full compose), caches it, and then re-materializes
* any variation as a cheap re-weighted sum per segment/instance plus a
* regenerated noise synth spanning that instance's delay+on+off:
*
* render(θ) = concat over segments, repeat instances of
* [ noise(delay) | Σ_k g_k(θ)·e^{jφ_k(θ)}·cache_k + noise(on)
* | noise(off) ]
*
* v1 axes (all bit-exact vs a full compose): per-source gain/level, phase,
* enable/disable, global SNR/noise-floor and Monte-Carlo noise-seed —
* applied uniformly across every segment/instance that carries noise.
* Frequency (Doppler) and multipath delay are staged follow-ups on the same
* frame.
*
* Scope: any number of finite segments (no continuous/repeat scene — that
* has no fixed capacity); each segment may declare `repeats` (bounded
* instancing, AWGN fresh per instance, signal fixed) and ranged
* `off_samples`/`delay_samples` (redrawn per instance at materialize time,
* via the same deterministic hash the streaming composer uses). Still out
* of scope: a ranged on-time (`num_samples`) — it would invalidate the
* fixed-length signal cache — and any ranged per-source field
* (freq/snr/level/f_end) — redrawing a source's frequency or SNR would
* invalidate its cached render, defeating the "expensive DSP once"
* guarantee this exists to provide.
*
* `wfm_plan_len()` is a WORST-CASE capacity (every ranged gap at its `hi`
* bound): `render()`/`at()` write up to that many samples but return the
* ACTUAL length of that specific draw — trailing samples beyond the
* return value are zero padding. A `seed` override drives both the noise
* realization AND (for a scene with a ranged gap/delay) the gap/delay
* redraw; the "no-override reproduces wfm_compose to the bit" guarantee is
* scoped to `render()` with no `"seed"` key (or `NULL`/`"{}"` overrides) —
* each segment then draws from its own baked default seed (its first
* source's `seed` field), same as a plain compose. `at()`'s `seed` is
* always an explicit override (parallel to its always-explicit `snr`), so
* it does not carry that baseline guarantee for a ranged-gap scene.
*
* `gains`/`phases`/`enable` override arrays are flat and segment-major
* (segment 0's sources in scene order, then segment 1's, ...) — length
* `wfm_plan_n_sources()`.
*
* The Plan is a re-creatable derived cache, not evolving state. The cheapest,
* most portable persistence is therefore the spec JSON (Composer.to_json())
* plus a rebuild — KB, and always tracks the current DSP. wfm_plan_save() /
* wfm_plan_restore() are the OPTIONAL fast-path alternative: they serialize
* the cached sample buffers alongside the embedded spec so a restore SKIPS the
* expensive DSP (build_synth) that dominates prepare(). This is a versioned
* performance cache, NOT the dp_state.h elastic-state contract — it is gated
* by a build-time DSP fingerprint (wfm_plan_dsp_hash.h), and on any mismatch
* (different DSP build, or a spec/buffer inconsistency) restore transparently
* REBUILDS from the embedded spec rather than reinterpreting stale bytes. So a
* restore is never silently wrong: worst case it is as slow as prepare(); best
* case (matching build) it is a memcpy. Prefer save/restore only when the DSP
* cost is high and the spec-rebuild is undesirable (e.g. fanning one prepared
* Plan across many workers); otherwise persist the spec and rebuild.
*/
#ifndef WFM_PLAN_H
#define WFM_PLAN_H
#include <complex.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
typedef struct wfm_plan wfm_plan_t;
wfm_plan_t *wfm_plan_prepare (const char *spec_json);
size_t wfm_plan_len (const wfm_plan_t *p);
size_t wfm_plan_n_sources (const wfm_plan_t *p);
uint64_t wfm_plan_anchor_seed (const wfm_plan_t *p);
size_t wfm_plan_render (const wfm_plan_t *p, const char *overrides_json,
float _Complex *out);
size_t wfm_plan_at (const wfm_plan_t *p, double snr, uint64_t seed,
float _Complex *out);
size_t wfm_plan_save_bytes (const wfm_plan_t *p);
size_t wfm_plan_save (const wfm_plan_t *p, void *blob);
wfm_plan_t *wfm_plan_restore (const void *blob, size_t n);
int wfm_plan_dump (const wfm_plan_t *p, const char *path);
wfm_plan_t *wfm_plan_load (const char *path);
void wfm_plan_destroy (wfm_plan_t *p);
#ifdef __cplusplus
}
#endif
#endif /* WFM_PLAN_H */