Scientific validation: TorchSig AM emitter

Validated with documented limitations.

1. The component

TorchSigAMEmitter is a clean IQ (in-phase / quadrature: the two orthogonal components of a complex-valued baseband signal) generator for four standard amplitude-modulation (AM: a family of analog modulations where a carrier wave’s amplitude is varied proportionally to a message signal) modes. It wraps TorchSig’s am_modulator builder and produces deterministic, noise-free waveforms for training and evaluation of RF (radio-frequency: the electromagnetic spectrum used for wireless communication) foundation models.

Class signature.

class TorchSigAMEmitter(BaseEmitter):
    family: ClassVar[EmitterFamily] = EmitterFamily.COMMS
    supported_classes: ClassVar[tuple[str, ...]] = (
        "am-dsb-fc", "am-dsb-sc", "am-ssb-usb", "am-ssb-lsb"
    )

    def __init__(self) -> None: ...     # raises BackendUnavailableError if torchsig not installed

    def generate(
        self,
        *,
        class_label: str,          # one of supported_classes
        sample_rate: float,        # Hz
        duration_s: float,         # seconds
        f_offset_hz: float,        # baseband frequency shift, Hz
        rng: torch.Generator,
        device_id: str | None = None,
        params: BaseModel | None = None,  # TorchSigAMParams or None
    ) -> Signal: ...

Parameter table.

Name

Type

Units

Default

Purpose

class_label

str

-

required

One of the four AM class labels (see below)

sample_rate

float

Hz

required

Complex-baseband sample rate; Nyquist guard enforced

duration_s

float

s

required

Output duration; N = round(sample_rate * duration_s)

f_offset_hz

float

Hz

required

Baseband frequency shift applied after modulation; must be finite

rng

torch.Generator

-

required

Seeds the numpy RNG forwarded to TorchSig’s builder

bandwidth_hz

float

Hz

10 000

3 dB occupied bandwidth; must satisfy bandwidth_hz + 2*|f_offset_hz| < sample_rate

Worked example.

import torch
from rfgen.emitters.torchsig_am import TorchSigAMEmitter, TorchSigAMParams

em = TorchSigAMEmitter()
rng = torch.Generator().manual_seed(42)
sig = em.generate(
    class_label="am-dsb-fc",
    sample_rate=200_000,   # 200 kHz
    duration_s=0.01,       # 10 ms -> 2 000 samples
    f_offset_hz=0.0,
    rng=rng,
    params=TorchSigAMParams(bandwidth_hz=10_000),  # 10 kHz
)
print(sig.iq.shape)   # torch.Size([2, 2000])
print(sig.iq.dtype)   # torch.float32
print(sig.metadata.class_taxonomy)  # ('comms', 'am', 'am-dsb-fc')

Class-label to TorchSig mode mapping.

Class label

TorchSig am_mode

Common name

am-dsb-fc

dsb

Double-sideband (DSB: two symmetric spectral copies of the message around a carrier), full carrier (broadcast AM)

am-dsb-sc

dsb-sc

Double-sideband, suppressed carrier

am-ssb-usb

usb

Single-sideband (SSB: only one of the two DSB spectral copies), upper sideband

am-ssb-lsb

lsb

Single-sideband, lower sideband

Taxonomy and scope. The emitter occupies the EmitterFamily.COMMS sub-family ("comms", "am", <label>) in the rfgen taxonomy. Excluded from scope: vestigial-sideband (VSB), independent-sideband (ISB), compatible-quadrature AM (C-QUAM), real audio sources, pre-emphasis, envelope companding, and caller control over the DSB-FC modulation index. All channel impairments (phase noise, IQ imbalance, PA (power amplifier: the transmitter stage that amplifies signal power before the antenna) distortion, propagation) are introduced downstream in the rfgen pipeline.


2. What we validated

This validation establishes 8 load-bearing claims. Each is restated and supported by evidence in §3.

  1. Output shape and dtype (§3.1): all four class labels produce a (2, N) float32 IQ tensor with the correct sample count.

  2. Zero-mean IQ after DC-subtract (§3.2): the wrapper’s DC-subtraction makes the output satisfy the rfgen zero-mean contract for all four modes, including DSB-FC where the raw TorchSig output carries a substantial carrier offset.

  3. Determinism (§3.3): the same generator seed yields bit-identical output; a different seed yields different output.

  4. Class-label to TorchSig mode mapping (§3.4): rfgen labels route to the correct TorchSig am_mode argument.

  5. DSB real-valued baseband; SSB complex with correct spectral peak side (§3.5): structural property confirming the modulation geometry for all four modes.

  6. Occupied bandwidth tracks bandwidth_hz (§3.6): the OBW (occupied bandwidth: the narrowest frequency interval containing most of the signal power) stays within verified tolerances of the requested value across DSB and SSB modes.

  7. Error paths surface EmitterError with self-diagnostic messages (§3.7): unsupported labels, Nyquist violations, and pathological inputs all raise typed errors.

  8. Safe operating envelope across a parameter grid (§3.8): no NaN, Inf, or zero-power output inside the documented envelope.

Limits and scope-bounded items appear in §4; full citations are in §5.


3. Evidence per claim

3.1 Output shape and dtype

Claim. For every class label and any valid (sample_rate, duration_s) pair, generate returns a Signal whose iq field is a (2, N) float32 tensor with N = round(sample_rate * duration_s).

Evidence. test_claim_A_shape_and_dtype in tests/validation/emitters/torchsig_am/test_experiment_contract.py calls generate for each of the 4 class labels at sample_rate = 200 kHz, duration_s = 0.01 s (N = 2 000) and asserts sig.iq.dtype == torch.float32 and sig.iq.shape == (2, 2000). All 4 assertions pass.

The dtype contract comes from the astype(np.float32) cast on both I and Q channels before torch.from_numpy. The sample-count contract follows from n_samples = int(round(sample_rate * duration_s)) in the implementation, which is evaluated before am_modulator is called.

3.2 Zero-mean IQ after DC-subtract

Claim. After the wrapper’s step iq_complex = iq_complex - iq_complex.mean(), the output satisfies |mean(I)| / peak(|I|) < 1e-4 and |mean(Q)| / peak(|Q|) < 1e-4 (when Q is non-trivial). This holds even for DSB-FC (double-sideband full-carrier), where TorchSig’s builder produces s(t) = A_c + m * x(t) with a non-zero carrier offset A_c = max(|x|) / mod_index.

Evidence. Two tests anchor this claim.

test_dsb_fc_carrier_is_real_and_dominant in tests/validation/emitters/torchsig_am/test_experiment_psd_bandwidth.py calls am_modulator directly (bypassing the wrapper) across 16 seeds and asserts the maximum |mean|/std ratio exceeds 0.5, confirming the pre-subtract DSB-FC output carries a substantial DC offset. Measurement: maximum ratio above 0.5 on at least one of 16 seeds.

test_claim_B_zero_mean_post_dc_subtract in tests/validation/emitters/torchsig_am/test_experiment_contract.py calls the wrapped generate for all 4 labels and asserts |mean(I)| <= 1e-4 * peak(|I|) (and similarly for Q when Q is non-trivial). All 4 per-label assertions pass.

The subtraction is therefore load-bearing: without it, DSB-FC would fail the rfgen zero-mean contract. The cost is that the carrier component A_c is removed, which affects the envelope statistics discussed in §4.

3.3 Determinism

Claim. Two calls to generate with the same torch.Generator seed (same class_label, sample_rate, duration_s, params) produce bit-identical IQ tensors. The same call with a different seed produces a different IQ tensor.

Evidence. test_claim_C_determinism in tests/validation/emitters/torchsig_am/test_experiment_contract.py, executed for all 4 class labels. Three generators seeded at 42, 42, and 43 are created. Seeds 42 and 42 produce identical arrays (verified with np.testing.assert_array_equal); seed 43 produces a different array. 8 total assertions (4 labels × 2 comparisons), all pass.

The determinism chain is: torch.Generator seeds torch.randint which draws one 64-bit integer; that integer seeds numpy.random.default_rng; TorchSig’s builder uses this RNG for its LPF (low-pass filter: a circuit or algorithm that passes frequencies below a cutoff and attenuates those above) design and message realization. Equal seeds produce bit-identical LPF coefficients and message sequences.

3.4 Class-label to TorchSig mode mapping

Claim. The mapping {am-dsb-fc dsb, am-dsb-sc dsb-sc, am-ssb-usb usb, am-ssb-lsb lsb} is implemented correctly; metadata.extras["am_mode"] reports the TorchSig-internal mode string used for each call.

Evidence. test_claim_D_label_to_mode_mapping in tests/validation/emitters/torchsig_am/test_experiment_contract.py. For each of the 4 label/mode pairs, generate is called and sig.metadata.extras["am_mode"] is compared to the expected TorchSig mode string. 4 assertions, all pass.

The mapping is defined in _AM_MODE_BY_LABEL at the module level of src/rfgen/emitters/torchsig_am.py. test_claim_I_metadata_fields additionally verifies the remaining metadata fields: family == "comms", class_taxonomy == ("comms", "am", <label>), bandwidth_hz, sample_rate_hz, and snr_db == inf.

3.5 DSB real-valued baseband; SSB complex with correct spectral peak side

Claim (a). At f_offset_hz = 0, the DSB modes produce real-valued baseband IQ: the Q channel satisfies max(|Q|) < 1e-6. The TorchSig builder returns a 1D real array for DSB modes before the wrapper promotes it to complex IQ.

Claim (b). The SSB modes produce complex baseband where both I and Q are non-trivial, and the spectral peak falls on the expected side: USB (upper sideband) peak at positive frequency, LSB (lower sideband) peak at negative frequency.

Evidence (a). test_claim_E_dsb_is_real_valued in tests/validation/emitters/torchsig_am/test_experiment_contract.py. For both DSB labels, max(abs(Q)) is verified below 1e-6. Both assertions pass.

Evidence (b). test_claim_F_ssb_peak_side_and_complex in tests/validation/emitters/torchsig_am/test_experiment_contract.py. For both SSB labels, the test verifies max(|Q|) > 1e-3 (non-trivial quadrature channel) and that the FFT (fast Fourier transform: the standard algorithm for computing the discrete frequency spectrum of a sampled signal) argmax frequency is positive for USB and negative for LSB. 2 assertions pass.

Figure 1 and Figure 2 show the Welch PSD (power spectral density: how signal energy is distributed across frequency) for DSB-FC and DSB-SC respectively. Figure 3 and Figure 4 show the SSB modes.

Figure 1: Welch PSD for am-dsb-fc at 10 kHz bandwidth, seed 7, 200 kHz sample rate. Shows the symmetric two-sideband spectrum about DC, confirming the DSB-FC spectral signature. Supports claim §3.5 (DSB real-valued baseband).

Figure 1 shows the symmetric two-lobed DSB-FC spectrum with the expected shape described in Haykin (2001, §3.3).

Figure 2: Welch PSD for am-dsb-sc at 10 kHz bandwidth, seed 7, 200 kHz sample rate. Shows the suppressed-carrier spectral shape contrasted with DSB-FC; symmetric occupied bandwidth confirms §3.6 bandwidth tracking.

Figure 2 shows the DSB-SC spectrum where the carrier at DC is suppressed, leaving only the two message sidebands.

Figure 3: Welch PSD for am-ssb-lsb at 10 kHz bandwidth, seed 7, 200 kHz sample rate. Energy concentrated in the negative-frequency half-band confirms the LSB label assignment. Supports claims §3.5 and §3.6.

Figure 3 shows the LSB spectrum with the dominant energy lobe in the negative-frequency half-band. TorchSig’s real-IF construction leaves residual energy on the positive side (visible but smaller), which is the weak-SSB limitation discussed in §4.

Figure 4: Welch PSD for am-ssb-usb at 10 kHz bandwidth, seed 7, 200 kHz sample rate. Energy concentrated in the positive-frequency half-band confirms the USB label assignment. Supports claims §3.5 and §3.6.

Figure 4 shows the USB spectrum with the dominant energy lobe in the positive-frequency half-band, mirroring the LSB shape in Figure 3.

3.6 Occupied bandwidth tracks bandwidth_hz

Claim. The 99% OBW (occupied bandwidth: the smallest frequency interval containing 99% of the signal’s total power, defined by FCC 47 CFR §73.1570 and ITU-R BS.706-2) of the output stays within [0.9×, 1.5×] of bandwidth_hz for the DSB modes and within [0.5×, 2.0×] for the SSB modes. These tolerances are verified across 8 seeds at bandwidths of 5 kHz, 10 kHz, and 20 kHz for DSB, and 10 kHz for SSB, all at sample_rate = 200 kHz.

The wider SSB tolerance reflects TorchSig’s real-IF construction: the output spectrum retains power on both sidebands, so the 99% OBW window encompasses a larger fraction of the requested value.

Evidence. test_dsb_occupied_bandwidth and test_ssb_occupied_bandwidth in tests/validation/emitters/torchsig_am/test_experiment_psd_bandwidth.py. For each of the 6 DSB cells (3 bandwidths × 2 labels) and 2 SSB cells (1 bandwidth × 2 labels), 8 seeds are drawn and the mean OBW across seeds is asserted within tolerance. All 8 cells pass.

The OBW estimator uses scipy.signal.welch with nperseg=8192 (Hann window) and integrates outward from the spectral peak until 99% of total power is enclosed. The tolerance multipliers are set above the observed seed-to-seed jitter (~5%) to catch real regressions without false positives from TorchSig’s internal LPF parameter randomization (rng.uniform(0.05, 0.25) for the LPF transition bandwidth at each call).

The DSB-FC envelope distinguishability from test_dsb_fc_envelope_distinct_from_dsb_sc in tests/validation/emitters/torchsig_am/test_empirical_known_results.py provides an additional realism anchor: the raw TorchSig DSB-FC output has envelope mean/std ratio above 0.8 and strictly exceeds the DSB-SC ratio across 20 seeds, consistent with the Haykin (2001, §3.3) prediction that DSB-FC envelope |A_c + m·x(t)| is less variable than DSB-SC envelope |m·x(t)|.

test_dsb_fc_modulation_index_distribution_exceeds_broadcast_cap in tests/validation/emitters/torchsig_am/test_empirical_known_results.py measures the realized modulation depth (max(env) - min(env)) / (max(env) + min(env)) across 40 seeds and confirms that more than 50% of seeds produce over-modulated output (depth > 99%), consistent with TorchSig’s Uniform(0.8, 4.0) modulation-index distribution. This is a scope-bounded limitation discussed in §4.

Figure 5 shows the time-domain envelope of DSB-FC (pre-subtract) and DSB-SC, and Figure 6 shows the empirical 99% OBW versus requested bandwidth across all four labels.

Figure 5: Time-domain envelope of DSB-FC (pre-DC-subtract, upper panel) and DSB-SC (lower panel) across 10 ms at 200 kHz / 10 kHz bandwidth, seed 7. The high carrier-level bias of the DSB-FC envelope confirms DC subtraction is load-bearing. Supports §3.2 and §4 (envelope alteration).

Figure 5 shows the DSB-FC carrier bias (upper panel: envelope rides well above zero) versus DSB-SC (lower panel: envelope crosses zero). Both the in-phase signal (I) and the envelope magnitude are plotted in each panel.

Figure 6: Empirical 99% occupied bandwidth versus requested bandwidthhz, 5-seed mean per label, 200 kHz sample rate. Dashed line is y = x (ideal). DSB modes stay within 1.5× and SSB modes within 2.0×. Supports claim §3.6.

Figure 6 shows all four labels tracking the identity line within the stated tolerances across the 2 kHz to 40 kHz bandwidth range.

3.7 Error paths surface EmitterError with self-diagnostic messages

Claim. Every documented error condition raises rfgen.errors.EmitterError. Unsupported class labels include at least one supported label in the error message so the caller can self-diagnose. No error path produces a bare Python exception or a silent wrong result.

Evidence. Eight test functions cover the error paths:

  • Unsupported label with hint: test_claim_G_unsupported_label in test_experiment_contract.py: asserts the error message contains at least one of the 4 supported labels when "am-vsb" is passed. Pass.

  • Unknown class label: test_unknown_class_label_raises in test_robustness_pathological.py. Pass.

  • Empty class label: test_empty_string_label_raises in test_robustness_pathological.py. Pass.

  • Nyquist guard (bandwidth_hz + 2*|f_offset_hz| >= sample_rate): test_claim_H_nyquist_guard in test_experiment_contract.py and test_bandwidth_exceeds_nyquist_raises in test_robustness_envelope.py. The guard is stricter than TorchSig’s internal check (bandwidth <= sample_rate/2) because the wrapper adds a baseband frequency shift after the builder runs.

  • Nyquist-boundary equality: test_offset_at_nyquist_boundary_raises in test_robustness_pathological.py: the guard uses strict >=, so exact equality is also rejected.

  • Zero duration: test_zero_duration_raises in test_robustness_envelope.py. Pass.

  • Negative duration: test_negative_duration_raises in test_robustness_pathological.py. Pass.

  • NaN f_offset_hz: test_nan_offset_propagates_as_emitter_error in test_robustness_pathological.py: the math.isfinite guard fires before the bandwidth check because abs(NaN) >= sample_rate evaluates to False under IEEE 754, which would let a NaN offset pass the budget check silently. Match on "finite" in the error message. Pass.

  • Inf f_offset_hz: test_inf_offset_raises_or_caught in test_robustness_pathological.py. Pass.

  • bandwidth_hz <= 0: test_negative_bandwidth_pydantic_rejects in test_robustness_envelope.py: Pydantic Field(gt=0) raises ValidationError at parameter construction before generate is called. Pass.

3.8 Safe operating envelope across a parameter grid

Claim. Inside the documented envelope (sample_rate from 10 kHz to 10 MHz, bandwidth_hz from 1 kHz to 1 MHz with the Nyquist constraint, duration_s from 1/sample_rate to 5 s, f_offset_hz from 0 to (sample_rate - bandwidth_hz) / 2), all four class labels produce finite, non-zero-power IQ tensors with the correct sample count.

Evidence. Four test functions:

  • test_envelope_succeeds in test_robustness_envelope.py: parametric grid of 5 (sample_rate, bandwidth_hz, duration_s) tuples × 4 labels = 20 combinations. Each call asserts np.isfinite(iq).all() and np.max(np.abs(iq)) > 0. All 20 pass.

  • test_no_nan_inf_or_zero_power_across_grid in test_robustness_envelope.py: 4 bandwidths × 3 durations × 4 labels = 48 cells at sample_rate = 200 kHz. All pass.

  • test_one_sample_succeeds in test_robustness_pathological.py: duration_s = 1/sample_rate produces shape (2, 1) with finite values. Pass.

  • test_very_long_duration in test_robustness_pathological.py: 5 s at 200 kHz = 1 000 000 samples, shape (2, 1000000), finite values. All 4 labels pass.

  • test_frequency_offset_inside_budget in test_robustness_envelope.py: f_offset_hz = 20 kHz with bandwidth_hz = 10 kHz and sample_rate = 200 kHz (budget: 10 kHz + 2×20 kHz = 50 kHz < 200 kHz). Spectral centroid verified positive for all 4 labels. Pass.


4. Limits and what’s not validated

TorchSig SSB is not Hilbert-pair SSB. The usb and lsb modes use a real-IF translate-filter-translate-decimate construction rather than the canonical Hilbert-transform analytic-signal method. The Hilbert method produces s(t) = x(t) ± j·hilbert(x)(t) with ≥ 35 dB suppression of the unwanted sideband (ITU-R F.349-5, §3.2). TorchSig’s construction retains approximately 50% of power on the unwanted side: roughly 3 dB suppression. Only a small spectral first-moment (the signal’s average frequency weighted by PSD) asymmetry distinguishes USB from LSB, verified by test_ssb_spectral_first_moment_on_expected_side in test_empirical_known_results.py. The emitter is usable for AMC (automated modulation classification: machine-learning identification of modulation type from IQ observations) training where the classifier learns the weak spectral asymmetry, but not for SSB-demodulator validation or fixed-service HF (high-frequency: the 3–30 MHz band used for long-range SSB voice links) communications emulation.

DSB-FC modulation depth exceeds the FCC broadcast cap on the majority of calls. TorchSig draws the modulation index uniformly from [0.8, 4.0]; more than 50% of the 40-seed cohort produces over-modulation (envelope modulation depth above 99%), while FCC 47 CFR §73.1570(b)(1) caps US AM broadcast at 100%. For foundation-model training corpus generation, the wider distribution is a defensible label-noise expansion; for broadcast-compliance simulation it is not. The modulation index is not user-controllable through the wrapper API. Exposing modulation_index as a caller-controlled parameter would require patching TorchSig or bypassing am_modulator in favour of the lower-level AMSignalGenerator class; both options are architectural changes deferred to a future iteration.

DSB-FC envelope statistics are altered by DC subtraction. The textbook DSB-FC envelope |A_c + m·x(t)| is what an envelope-detection receiver exploits to demodulate the message. The wrapper’s iq_complex - iq_complex.mean() step removes A_c, shifting the post-subtract envelope statistics toward those of DSB-SC. The envelope distinguishability claim in §3.6 is therefore asserted on raw TorchSig output, not on the wrapped output. A downstream consumer building an envelope-detection AM demodulator should call torchsig.signals.builders.am.am_modulator directly.

The message is Gaussian noise, not real audio. The baseband message x(t) is band-limited Gaussian noise shaped by TorchSig’s iterative-design LPF. This matches the RadioML 2018.01a convention for analog AM label generation. A classifier trained on this emitter alone does not learn audio-cadence features (pauses, formants, tonal energy).

No broadcast-grade or protocol impairments. Pre-emphasis, envelope companding, AM-stereo / C-QUAM stereo subcarrier, mains hum, AGC (automatic gain control: a circuit that adjusts receiver gain to keep output amplitude stable) overshoot, PA non-linearity, IQ imbalance, and oscillator phase noise are absent. These are introduced at the channel and receiver-frontend layers downstream in the rfgen pipeline.

VSB, ISB, and C-QUAM are not supported. None of these AM variants are available in TorchSig’s am_modulator.


5. References

Published works

Reference

Role

S. Haykin, Communication Systems, 4th ed., Wiley, 2001, §3.3, ISBN 978-0-471-17869-9

Canonical DSB-FC envelope s(t) = A_c[1 + k_a m(t)] cos(2π f_c t), DSB-SC, and SSB derivations; envelope distinguishability prediction

J. G. Proakis and M. Salehi, Digital Communications, 5th ed., McGraw-Hill, 2008, §3.2, ISBN 978-0-07-295718-2

Hilbert-transform SSB and analytic-signal representation s(t) = x(t) ± j·hilbert(x)(t)

ITU-R Recommendation BS.706-2 (2002)

Broadcast AM envelope reference and 99% occupied-bandwidth convention

ITU-R Recommendation F.349-5 (2010), §3.2

Fixed-service HF SSB unwanted-sideband suppression target (≥ 35 dB); benchmark for the weak-SSB limitation documented in §4

FCC 47 CFR §73.1570, “Modulation levels: AM, FM, TV, and Class A TV aural”

AM-broadcast modulation-level rule; §73.1570(b)(1) caps negative-direction modulation at 100%; benchmark for the over-modulation limitation in §4

T. J. O’Shea and N. West, “Radio machine learning dataset generation with GNU Radio,” Proceedings of the GNU Radio Conference, vol. 1, no. 1, 2016

RadioML 2018.01a convention for Gaussian-noise AM message; basis for the non-audio message design choice

Libraries

PyPI distribution

Installed version

Documentation URL

Role in validation

torchsig

2.1.1

https://torchsig.readthedocs.io

Provides am_modulator; the IQ synthesis backend under test

torch

2.12.1

https://pytorch.org/docs/stable/index.html

torch.Generator for seeded RNG; torch.from_numpy for tensor output

scipy

1.18.0

https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.welch.html

scipy.signal.welch Welch PSD estimator used in all bandwidth validation tests

numpy

2.4.6

https://numpy.org/doc/stable/

numpy.random.default_rng for the per-call RNG; numpy.exp for the baseband frequency shift; envelope arithmetic

pydantic

2.13.4

https://docs.pydantic.dev/latest/

TorchSigAMParams Pydantic v2 model; Field(gt=0) constraint on bandwidth_hz

matplotlib

-

https://matplotlib.org/stable/api/index.html

Figure generation in tests/validation/emitters/torchsig_am/generate_figures.py