Scientific validation: TorchSig chirp (comms) emitter¶
Validated with documented limitations.
1. The component¶
TorchSigChirpEmitter is a PyTorch-fronted generator that produces clean baseband in-phase/quadrature (IQ) tensors for two comms-regime chirp waveform classes. A chirp is a signal whose instantaneous frequency (the rate of phase change at a given moment, measured in Hz) sweeps linearly over time; the in-phase (I) and quadrature (Q) channels are the real and imaginary parts of the resulting complex-valued baseband signal, forming the two-channel (2, N) tensor consumed by downstream models. This emitter is the comms-side companion to ChirpRadarEmitter; they share the word “chirp” but represent different regimes (continuous comms-symbol-rate chirp vs deterministic single-pulse radar LFM) and belong to different emitter families.
class TorchSigChirpEmitter(BaseEmitter):
family: ClassVar[EmitterFamily] = EmitterFamily.COMMS
supported_classes: ClassVar[tuple[str, ...]] = ("chirpss", "lfm_data")
def __init__(self) -> None: ...
def schema(self) -> type[BaseModel]: ...
def generate(
self,
*,
class_label: str,
sample_rate: float,
duration_s: float,
f_offset_hz: float,
rng: torch.Generator,
device_id: str | None = None,
params: BaseModel | None = None,
) -> Signal: ...
class TorchSigChirpParams(BaseModel):
bandwidth_hz: float = Field(default=125e3, gt=0)
Parameter |
Type |
Units |
Default |
Purpose |
|---|---|---|---|---|
|
|
n/a |
required |
One of |
|
|
Hz |
required |
Samples per second on the output tensor. Must be finite and positive. |
|
|
s |
required |
Output duration. Total sample count is |
|
|
Hz |
required |
Baseband frequency shift in Hz. May be negative, zero, or positive; must be finite. |
|
|
n/a |
required |
Seeds the NumPy random-number generator consumed by the TorchSig builder. State advances once per call. |
|
|
n/a |
|
Optional metadata label echoed into |
|
|
Hz |
|
Nominal 3 dB bandwidth passed to the TorchSig builder. Default matches the standard LoRa (Long Range, a low-power wide-area network protocol) channel raster per Semtech SX1276 datasheet §4.1. |
The two class labels correspond to different TorchSig builder paths:
Class label |
TorchSig builder |
Construction |
|---|---|---|
|
|
CSS (chirp-spread-spectrum, the class of chirp-based spread-spectrum modulation that LoRa belongs to) symbol stream: each symbol is one of 128 cyclic-shifted copies of an upchirp template spanning |
|
|
Binary LFM (linear frequency modulation) data chirp: each symbol independently selects an upchirp or downchirp with equal probability. |
import torch
from rfgen.emitters import TorchSigChirpEmitter
emitter = TorchSigChirpEmitter()
signal = emitter.generate(
class_label="lfm_data",
sample_rate=1_000_000.0, # 1 MHz
duration_s=0.05, # 50 ms -> 50,000 samples
f_offset_hz=0.0,
rng=torch.Generator().manual_seed(0),
)
# signal.iq has shape (2, 50_000) and dtype torch.float32:
# channel 0 is the in-phase (I) component, channel 1 is the quadrature (Q).
assert signal.iq.shape == (2, 50_000) and signal.iq.dtype == torch.float32
The class taxonomy is ("comms", "chirp", <label>) under EmitterFamily.COMMS. The emitter rejects the radar-side label "lfm_radar" (handled by ChirpRadarEmitter) with EmitterError. Channel-side impairments (noise, multipath, phase noise, IQ imbalance, power-amplifier nonlinearity) are out of scope and assigned to a separate channel layer.
2. What we validated¶
This validation establishes 8 load-bearing claims. Each is restated and supported by evidence in §3.
Shape, dtype, and metadata contract (§3.1): the output is a
(2, N)float32tensor with the correct family, taxonomy, and sample count for both class labels.DC-removal contract (§3.2): the emitter subtracts the complex mean so the DC residue is below the rfgen
|mean| / |peak| < 1e-4threshold.LFM instantaneous-frequency confinement (§3.3): for
lfm_data, the instantaneous frequency stays within the[-B/2, +B/2]band (measured: clean 99th-percentile max 62.5 kHz vs 75 kHz threshold at B = 125 kHz).LFM symbol-map symmetry (§3.4): the equal-probability binary symbol map produces near-zero mean instantaneous frequency over a long record.
Occupied-bandwidth tracking for lfm_data (§3.5): the empirical -3 dB bandwidth tracks the requested
bandwidth_hzwithin 8 % across three operating points.chirpss occupied bandwidth is approximately 2x nominal (§3.6): the LoRa-style cyclic-shift symbol map causes the -3 dB bandwidth to be 1.7x to 1.9x
bandwidth_hz; pinned by a regression test.Frequency-offset mixing (§3.7):
f_offset_hz != 0shifts the spectral centroid byf_offset_hzwithin the tolerance max(5 %, 5 kHz).Determinism and error-path contract (§3.8): same seed produces bit-identical output; the documented error paths surface the correct exception type.
Limits and scope-bounded items appear in §4; full citations are in §5.
3. Evidence per claim¶
3.1 Shape, dtype, and metadata contract¶
The emitter produces a (2, N) float32 tensor with N = round(sample_rate * duration_s). Metadata fields family, class_taxonomy, generator_name, sample_rate_hz, bandwidth_hz, realized_carrier_hz, and duration_samples are all populated correctly.
Assertion |
Test |
Result |
|---|---|---|
|
|
Pass (6 parametrized cases) |
|
same test |
Pass |
|
|
Pass |
|
same test |
Pass |
|
|
Pass (2 parametrized cases) |
|
same test |
Pass |
|
same test |
Pass |
3.2 DC-removal contract¶
The rfgen contract requires |mean(cx)| / max(|cx|) < 1e-4 where cx = I + jQ. The implementation subtracts iq.mean() after the TorchSig builder and any frequency-offset mixing. The measured residues are four to five orders of magnitude below the threshold.
Class |
Measured DC/peak ratio |
Contract threshold |
Test |
|---|---|---|---|
|
7.09e-09 |
1e-04 |
|
|
3.35e-09 |
1e-04 |
|
3.3 LFM instantaneous-frequency confinement¶
For lfm_data, each symbol is an upchirp (f0, f1) = (-B/2, +B/2) or a downchirp (+B/2, -B/2) running for samples_per_symbol baseband samples, then polyphase-resampled (a multi-rate filter operation that changes the effective sample rate by a rational ratio) to sample_rate. The LFM phase polynomial is theta(t) = 2*pi*(f0*t + 0.5*(f1-f0)/T_sym*t^2), giving instantaneous frequency f(t) = f0 + (f1-f0)*t/T_sym sweeping linearly within [-B/2, +B/2] (Richards, 2014, Ch. 4). Short transients at symbol boundaries (Fresnel overshoot and phase-derivative estimator artifacts from numpy.unwrap) affect the top 1 % of |f_inst| values; the clean 99th-percentile maximum is the load-bearing measurement.
Measurement |
Value |
Threshold |
Test |
|---|---|---|---|
Clean 99th-pct max |
62.5 kHz |
75 kHz (1.2 x B/2) |
|
Figure 1 shows the instantaneous-frequency time series for both class labels, with the ±B/2 limits marked.

3.4 LFM symbol-map symmetry¶
The TorchSig lfm_data builder selects the upchirp or downchirp symbol with equal probability (np.array([-1.0, 1.0]) symbol map). Over many symbols, upchirp and downchirp frequency excursions cancel, so the mean instantaneous frequency over a long record must be near zero. The test trims the 1st and 99th percentiles to remove numpy.unwrap artifacts before computing the mean.
Measurement |
Value |
Threshold |
Test |
|---|---|---|---|
Mean trimmed |
-61 Hz |
max |
|
3.5 Occupied-bandwidth tracking for lfm_data¶
The claim is that the TorchSig lfm_modulator builder honors its documented “3 dB bandwidth” parameter for the lfm_data path: the empirical -3 dB occupied bandwidth (the width of the contiguous spectral band whose PSD exceeds peak minus 3 dB) tracks bandwidth_hz within 15 %. Vangelista (2017) derives the ideal occupied bandwidth of a CSS symbol stream as approximately BW at the -3 dB point (reference value ≈ bandwidth_hz); these measurements are compared against that reference.
Measurement setup: sample_rate = 2 MHz, duration_s = 0.2 s, scipy.signal.welch (Welch’s method: PSD, or power spectral density, is how signal energy is distributed across frequency; it is estimated by averaging overlapping windowed Fourier transforms) with nperseg = 4096 (~488 Hz bin resolution, ~195 averaged segments), 8 seeds per cell.
Nominal |
Empirical -3 dB BW mean ± std |
Ratio to nominal |
Test |
|---|---|---|---|
50 kHz |
47.4 ± 1.0 kHz |
0.95 |
|
100 kHz |
95.0 ± 2.1 kHz |
0.95 |
|
200 kHz |
184.9 ± 19.0 kHz |
0.92 |
|
The 8 % shortfall (ratio 0.92) at 200 kHz is within the 15 % tolerance. The increased standard deviation at 200 kHz reflects the random samples_per_symbol draw inside the TorchSig builder. The proportional-scaling test test_psd_bandwidth.py::test_bandwidth_scaling_doubles confirms that doubling bandwidth_hz doubles the empirical -3 dB BW within 30 % for both class labels.
Figure 2 shows the Welch-averaged PSD for both class labels at three nominal bandwidths.

3.6 chirpss occupied bandwidth is approximately 2x nominal¶
The chirpss path uses a 128-shift cyclic-permutation symbol map, the LoRa-style SF (spreading factor, SF=7 here, meaning 2^7=128 distinct symbols) map. Each symbol shifts the frequency excursion of an upchirp template that spans [-B, +B] by k × B/128 for shift index k in {0, …, 127}. Because the template spans [-B, +B] rather than [-B/2, +B/2], the occupied bandwidth at the -3 dB point is approximately twice bandwidth_hz. Additionally, the TorchSig chirpss_modulator_baseband function applies a 50 % random LPF (low-pass filter) on each call, which is the dominant source of bandwidth variance at larger nominal bandwidths.
Nominal |
Empirical -3 dB BW mean ± std |
Ratio to nominal |
Test |
|---|---|---|---|
50 kHz |
96.2 ± 1.6 kHz |
1.92 |
|
100 kHz |
192.6 ± 3.2 kHz |
1.93 |
|
200 kHz |
345.2 ± 80.3 kHz |
1.73 |
|
The regression test pins the ratio in [1.4, 2.4] so any future upstream change to chirpss_modulator realized-occupancy semantics is detected automatically. The metadata.bandwidth_hz field records the user’s input value, not the realized occupancy; a scheduler using metadata.bandwidth_hz as a guard band will under-reserve by approximately 2x for chirpss signals.
Figure 3 shows the time-frequency structure of both class labels.

3.7 Frequency-offset mixing¶
When f_offset_hz != 0, the implementation multiplies the baseband signal by exp(j * 2*pi * f_offset_hz * t) where t = arange(N) / sample_rate. This shifts the spectral centroid of the signal by f_offset_hz. The test verifies the shift using the power-weighted spectral centroid (band-energy center) computed via scipy.signal.welch, which is more robust to per-bin noise than a single PSD-peak bin.
Parameter |
Measurement |
Expected |
Tolerance |
Test |
|---|---|---|---|---|
Spectral centroid shift at |
200 kHz |
200 kHz |
max(5 %, 5 kHz) = 10 kHz |
|
3.8 Determinism and error-path contract¶
Determinism. The implementation seeds a NumPy RNG (random-number generator) from a single torch.randint draw on the supplied torch.Generator. Same seed gives bit-identical output; different seeds give different output; consecutive calls on the same generator advance the state.
Assertion |
Tests |
Result |
|---|---|---|
Same seed → bit-identical IQ (both class labels) |
|
Pass |
Different seeds → different IQ (both class labels) |
|
Pass |
Consecutive calls on same generator differ |
|
Pass |
Error paths. Every validated error path surfaces the correct exception type.
Input condition |
Exception |
Test |
|---|---|---|
|
|
|
Extra schema field |
|
|
|
|
|
|
No exception, correct shape |
|
|
|
|
|
|
|
|
|
|
Unsupported class label (e.g. |
|
|
4. Limits and what is not validated¶
chirpss is CSS-family, not LoRa-compliant. The spreading factor SF is effectively fixed at 7 (the 128-shift symbol map is hard-coded in the upstream TorchSig builder); symbol duration is randomized per call rather than determined by T_sym = 2^SF / BW; there is no preamble, sync word, payload, CRC, or whitening; the 50 % random LPF is not part of the LoRa specification. The LoRaPHYEmitter and LoRaSdrEmitter classes in this repository are the LoRa-compliant paths. Exposing spreading factor and symbol time as user-controlled parameters would require switching to a lower-level TorchSig API or patching TorchSig; both are architectural changes deferred if downstream consumers require deterministic LoRa-style symbol timing.
chirpss realized -3 dB bandwidth is approximately 2x bandwidth_hz. The metadata.bandwidth_hz field records the user’s input, not the realized occupancy. A scheduler using metadata.bandwidth_hz as a guard band will under-reserve by approximately 2x for chirpss signals. The class docstring carries a forward reference to this report.
Per-sample envelope is not unit. The TorchSig polyphase resampler introduces amplitude ripple; the DC-subtract step further perturbs per-sample magnitude. Mean power is within 0.15 of unity (test_math_fidelity.py::test_envelope_power_close_to_unity); per-sample |cx|^2 = 1 is not satisfied. The ChirpRadarEmitter is the path for a constant-envelope LFM.
Realized symbol rate is not in metadata. The per-call random samples_per_symbol draw inside both TorchSig builders produces different effective symbol rates across calls with identical parameters and different seeds. The realized symbol rate is neither a user-facing parameter nor a metadata field; exposing it would require patching TorchSig or moving to a lower-level API.
Realized chirpss LPF is invisible. The 50 % random LPF impulse response inside chirpss_modulator_baseband is not exposed. The realized -3 dB bandwidth distribution at fixed bandwidth_hz is bimodal-ish (with vs without the LPF), which accounts for the large standard deviation (80.3 kHz) at B = 200 kHz in §3.6.
Degenerate B / fs corner. When bandwidth_hz / sample_rate is small enough that the baseband sample count rounds to zero (for example bandwidth_hz = 1e-3 at fs = 1 MHz), the TorchSig builder raises a bare ValueError rather than an EmitterError. This regime is outside any realistic operating point.
No transmitter impairments. No power-amplifier compression, no IQ imbalance, no oscillator phase noise, no carrier-frequency drift. All deferred to the channel layer per the rfgen architecture.
No multi-burst or duty-cycle structure. The entire duration_s window is filled with consecutive symbols; there is no PTT (push-to-talk) on/off envelope, no preamble, and no inter-burst silence. Out of scope for a clean-baseband emitter.
Sample-rate range tested. The validation tests cover 250 kHz to 2 MHz. Behavior at extreme sample rates (below 250 kHz or above 2 MHz) is not validated.
5. References¶
Published works¶
Reference |
Role |
|---|---|
L. Vangelista, “Frequency Shift Chirp Modulation: The LoRa Modulation,” IEEE Signal Processing Letters 24(12), Dec. 2017, pp. 1818–1821, doi:10.1109/LSP.2017.2762960 |
Closed-form CSS analysis; derives symbol-orthogonality and ideal occupied-bandwidth ≈ BW at -3 dB for a single-SF symbol stream; used as reference value in §3.5 |
Semtech AN1200.22, “LoRa Modulation Basics,” Semtech Corp., 2015 |
Canonical CSS / LoRa physical-layer reference; defines |
Semtech SX1276/77/78/79 Datasheet, rev. 7, Semtech Corp., 2020, §4.1 |
LoRa modulator block diagram; defines the supported-bandwidth set (7.8 kHz to 500 kHz); 125 kHz is the standard channel and the |
M. A. Richards, Fundamentals of Radar Signal Processing, 2nd ed., McGraw-Hill, 2014, Ch. 4, ISBN 978-0071798327 |
LFM phase polynomial and instantaneous-frequency derivation; contrasted with the comms-regime LFM in §3.3 |
Libraries¶
PyPI distribution |
Installed version |
Documentation |
Role |
|---|---|---|---|
|
2.1.1 |
Source of |
|
|
1.18.0 |
|
|
|
2.4.6 |
|
|
|
2.12.1 |
|
|
|
2.13.4 |
|