API reference
SinCosLUT.SinCosLUT — Module
SinCosLUTFast SIMD sine/cosine via register-resident byte/word/dword table lookups.
The lookup is a single hardware permute over a table held in vector registers: vpermb/vpermw/vpermd (AVX-512), vpshufb (AVX2, Int8 only), or NEON tbl (AArch64, Int8 and Int16). Output element type is selectable — Int8 (fastest, ~3-7 bit), Int16, or Int32 (more amplitude precision, fewer lanes/entries).
Accuracy is a deliberate trade for speed: a 512-bit register holds 64×Int8 but only 32×Int16 / 16×Int32, so wider elements give finer amplitude but coarser phase resolution (fewer table entries) and lower throughput.
SVE2 is not supported: Julia cannot express LLVM scalable vectors (<vscale x N x T>), see JuliaLang/julia#40308. NEON tbl runs natively on SVE hardware regardless.
SinCosLUT.SinCosLUTSinCosLUT.CarrierEngineSinCosLUT.CarrierStateSinCosLUT.SinCosTableSinCosLUT.backend_nameSinCosLUT.carrier_advanceSinCosLUT.carrier_engineSinCosLUT.carrier_lookupSinCosLUT.carrier_stateSinCosLUT.carrier_widthSinCosLUT.cycles_per_sampleSinCosLUT.default_backendSinCosLUT.generate_carrier!SinCosLUT.generate_carrier_signs!SinCosLUT.generate_carrier_signs_mags!SinCosLUT.lookup_sincos!SinCosLUT.prepare
Tables
SinCosLUT.SinCosTable — Type
SinCosTable(T=Int8; steps=64, amplitude=typemax(T))Build sin/cos lookup tables of steps entries per full cycle, output type T (Int8/Int16/Int32). steps should be a power of two. For the AVX-512 backend steps must be regsize(T) (single permute) or 2*regsize(T) (two-register vpermi2): Int8 → 64 or 128, Int16 → 32 or 64, Int32 → 16 or 32. Other sizes still work on the portable backend.
Carrier generation
SinCosLUT.generate_carrier! — Function
generate_carrier!(sin_out, cos_out, table, freq_word::Integer; phase=0, backend=…)
generate_carrier!(sin_out, cos_out, table, cycles_per_sample::Real; phase=0, backend=…)
generate_carrier!(sin_out, cos_out, table; frequency, sampling_frequency, phase=0, backend=…)Fill sin_out/cos_out (element type T) with a carrier generated by a textbook NCO (numerically-controlled oscillator): a UInt32 phase accumulator is advanced by a fixed frequency word each sample, and the top log2(steps) bits index the table. One full UInt32 is one carrier cycle, so the frequency resolution is uniform and dead-zone-free — sampling_frequency / 2^32 cycles per sample — and the phase never drifts (it is the exact closed form acc[n] = freq_word·n + offset mod 2^32, computed per lane with no division).
The low-level form takes a raw freq_word (a UInt32, 0 ≤ freq_word ≤ 2^32-1); a cycles_per_sample::Real is converted as freq_word = round(cps·2^32) (wraps via two's complement for negative/receding Doppler); the keyword form takes a frequency and sampling_frequency directly (cycles_per_sample = frequency / sampling_frequency). phase is the initial carrier phase, default 0: an Integer is table steps (exact), a Real is cycles.
SinCosLUT.generate_carrier_signs! — Function
generate_carrier_signs!(sin_signs, cos_signs, n, freq_word::Integer; phase=0)
generate_carrier_signs!(sin_signs, cos_signs, n, cycles_per_sample::Real; phase=0)
generate_carrier_signs!(sin_signs, cos_signs, n; frequency, sampling_frequency, phase=0)Generate a 1-bit (hard-limited) carrier: pack the sign bits of the NCO sin/cos for n samples into sin_signs/cos_signs (UInt64, each ≥ cld(n, 64) words). Bit j of word w corresponds to sample 64w + j (0-based); a set bit means that component is negative (i.e. its ±1 hard-limited value is −1). Bits past sample n in the last word are cleared.
This is the same NCO as generate_carrier! — a UInt32 phase accumulator advanced by a fixed frequency word each sample (acc[n] = freq_word·n + offset mod 2³²) — but only the sign is kept, read directly off the accumulator's top bit (sign(sin) = MSB(acc), sign(cos) = MSB(acc + ¼ cycle)), so there is no table and no output-type quantisation. The frequency argument matches generate_carrier!: a raw UInt32 freq_word, a cycles_per_sample::Real (freq_word = round(cps·2³²)), or the frequency/sampling_frequency keyword form. phase is the initial carrier phase in cycles (a Real; default 0).
Fast at any frequency: a 1-bit carrier is a square wave, so for a low residual carrier almost every 64-sample word is a single constant run written with one store; where sign flips fall inside words, each word's 64 signs are packed with a single SIMD sign-mask (no per-sample loop). This is the carrier form a bit-wise correlator consumes, where wipe-off becomes XOR and accumulation becomes popcount.
n = 5000
sin_signs = Vector{UInt64}(undef, cld(n, 64))
cos_signs = Vector{UInt64}(undef, cld(n, 64))
generate_carrier_signs!(sin_signs, cos_signs, n; frequency = 1234, sampling_frequency = 5e6)SinCosLUT.generate_carrier_signs_mags! — Function
generate_carrier_signs_mags!(sin_signs, cos_signs, sin_mags, cos_mags, n, freq_word::Integer; phase=0)
generate_carrier_signs_mags!(sin_signs, cos_signs, sin_mags, cos_mags, n, cycles_per_sample::Real; phase=0)
generate_carrier_signs_mags!(sin_signs, cos_signs, sin_mags, cos_mags, n; frequency, sampling_frequency, phase=0)Generate a 2-bit (sign + magnitude) carrier: pack the sign and magnitude bits of the NCO sin/cos for n samples into four bit-plane buffers (UInt64, each ≥ cld(n, 64) words). Bit j of word w corresponds to sample 64w + j (0-based); a set sign bit means that component is negative, a set magnitude bit means it is large — |component| ≥ sin(π/4) = √2/2, the octant where that component is the larger of the two. Reconstructing value = (sign ? -1 : 1) * (mag ? 2 : 1) gives the classic 2-bit octant waveform sin ≈ {+1,+2,+2,+1,−1,−2,−2,−1} of hardware GNSS correlators (any weight pair — ±1/±2, ±1/±3, … — works; only the bits are produced). Bits past sample n in the last word are cleared.
This is the same NCO as generate_carrier! and generate_carrier_signs! — acc[n] = freq_word·n + offset mod 2³² — and every bit is exact in the phase: sign(sin) = MSB(acc), sign(cos) = MSB(acc + ¼·2³²), mag(sin) = MSB(2·acc + ¼·2³²), and mag(cos) = !mag(sin) (with the threshold on the 45° octant boundary exactly one component is large at a time; both magnitude planes are still filled, for consumers that index them independently). The frequency argument matches generate_carrier!: a raw UInt32 freq_word, a cycles_per_sample::Real (freq_word = round(cps·2³²)), or the frequency/sampling_frequency keyword form. phase is the initial carrier phase in cycles (a Real; default 0).
Fast at any frequency, like the 1-bit kernel: at low frequencies words where all four planes are constant (the magnitude square wave runs at twice the carrier frequency, so its runs are half as long) are written with four plain stores; at high frequencies the fill runs a branch-free SIMD loop — O(1) per word either way, no per-sample loop.
n = 5000
planes = [Vector{UInt64}(undef, cld(n, 64)) for _ in 1:4]
generate_carrier_signs_mags!(planes..., n; frequency = 1234, sampling_frequency = 5e6)SinCosLUT.cycles_per_sample — Function
cycles_per_sample(frequency, sampling_frequency)Normalised frequency frequency / sampling_frequency (cycles per sample) — pass the result as the frequency argument of generate_carrier! or carrier_engine, e.g. generate_carrier!(sin_out, cos_out, table, cycles_per_sample(1000, 2e6)).
Arbitrary-index lookup
SinCosLUT.lookup_sincos! — Function
lookup_sincos!(sin_out, cos_out, phase_indices, table; backend=default_backend(table))Look up sin/cos for each integer phase index in phase_indices (taken mod steps).
SinCosLUT.prepare — Function
prepare(table; backend=default_backend(table)) -> callableMaterialise table into registers once and return a callable p such that p(phase_index::Vec) -> (sin::Vec, cos::Vec) (indices taken mod steps). Analogous to FastSinCos's fast_sincos_*, but a table lookup.
The callable's concrete type can depend on the table's VALUES (the AVX2 backend picks a smaller register layout for the half-wave-symmetric tables the constructor builds), so build it once outside hot loops.
Value-based carrier engine
Allocation-free, register-resident NCO carrier for fusing into a correlation loop: a loop-invariant CarrierEngine plus an isbits CarrierState renewed by value each iteration. One engine/state pair serves any interleave factor K — hold K states carrier_state(eng, (k-1)*W) and advance each by K chunks per iteration. See Fused, array-free generation for the full pattern.
SinCosLUT.CarrierEngine — Type
CarrierEngineImmutable, loop-invariant NCO carrier engine built once by carrier_engine: holds the register-resident sin/cos table and the phase-accumulator frequency word. Pair it with one CarrierState per interleaved stream and drive it with carrier_lookup / carrier_advance. It is isbits, so it costs no allocation.
SinCosLUT.CarrierState — Type
CarrierState{W}Immutable, isbits NCO phase-accumulator state (a Vec{W,UInt32}). Created by carrier_state and renewed by value each iteration via carrier_advance; read with carrier_lookup. Holds no table data, so it stays in registers.
SinCosLUT.carrier_engine — Function
carrier_engine(table, freq_word::Integer; backend=…) -> CarrierEngine
carrier_engine(table, cycles_per_sample::Real; backend=…) -> CarrierEngine
carrier_engine(table; frequency, sampling_frequency, backend=…) -> CarrierEngineBuild the loop-invariant carrier engine for table. The NCO phase advances by a UInt32 frequency word per sample; the low-level form takes it raw (0 ≤ freq_word ≤ 2^32-1), cycles_per_sample::Real converts as round(cps·2^32), and the keyword form derives it from frequency/sampling_frequency. The phase is uniform, dead-zone-free and never drifts.
SinCosLUT.carrier_state — Function
carrier_state(eng::CarrierEngine, start::Integer = 0; phase = 0) -> CarrierStateInitial NCO state for a stream whose first sample is absolute index start (use (k-1)·W for the k-th lane of a W-wide, K-way interleaved loop). phase is the initial carrier phase added to every lane: an Integer is table steps, a Real is cycles.
SinCosLUT.carrier_lookup — Function
carrier_lookup(eng::CarrierEngine, st::CarrierState) -> (sin::Vec{W,T}, cos::Vec{W,T})The (sin, cos) chunk at st's current phase. Pure read — does not advance the state.
SinCosLUT.carrier_advance — Function
carrier_advance(eng::CarrierEngine, st::CarrierState, nchunks::Integer) -> CarrierStateAdvance the stream by nchunks W-wide chunks (nchunks·W samples), returning a new immutable state. A K-way interleaved loop advances every state by K each iteration.
SinCosLUT.carrier_width — Function
carrier_width(eng::CarrierEngine) -> IntSIMD lane count W of the engine (samples per chunk).
Backends
SinCosLUT.default_backend — Function
default_backend(table::SinCosTable) -> Backend
default_backend(T::Type, steps::Integer) -> BackendThe lookup backend automatically selected for output type T and table size steps on the host CPU: AVX512(), AVX2(), or Neon() where the required SIMD permute is available, otherwise Portable() (the always-correct scalar fallback). AVX2 supports only Int8 with steps = 64; NEON supports Int8 (steps = 64) and Int16 (steps = 32 or 64); other combinations fall back to Portable().
The x86 ISA backends (AVX-512/AVX2) are selected only when the LLVM codegen target is native. Under a restricted or multiversioned CPU target (--cpu-target=… / JULIA_CPU_TARGET=…, the latter being how the official binaries build every pkgimage) the CPU-detected features need not match what LLVM can emit, so those backends fall back to Portable() rather than bake un-legalisable ISA into a baseline clone (which would abort codegen). Set JULIA_CPU_TARGET=native to keep the ISA backends under a custom target.
Pass the result (or any Backend) as the backend keyword of generate_carrier!, lookup_sincos!, carrier_engine, or prepare to override the choice. See backend_name for a readable label.
SinCosLUT.backend_name — Function
backend_name(backend) -> StringHuman-readable name of a Backend ("AVX-512", "AVX2", "NEON", or "portable"). Useful for reporting which lookup primitive default_backend selected.