Scientific validation: emitters/torchsig_fm

Verdict. Validated with documented limitations.


1. The component

TorchSigFMEmitter generates a clean complex-baseband waveform for analog frequency modulation (FM: a modulation scheme where information is encoded in the instantaneous frequency of a carrier, not its amplitude). The instantaneous frequency wanders around zero according to a band-limited Gaussian noise message; the output is exp(j·φ(t)), a unit-envelope complex tone (IQ signal: in-phase and quadrature components stacked as a (2, N) tensor).

Two class labels are supported: nbfm (NBFM: narrowband FM, used in channelised voice radio) and wbfm (WBFM: wideband FM, used in commercial broadcast). The labels are distinguished by label-specific rfgen/TorchSig-wrapper candidate defaults and ceilings on the Carson 98%-containment bandwidth (Carson bandwidth: the spectral window used here to test whether at least 98% of the signal’s measured average power is contained). The cited standards provide channel-separation, deviation, and emission-mask context; they do not define these wrapper defaults.

Class signature

class TorchSigFMEmitter(BaseEmitter):
    family: ClassVar[EmitterFamily]         # EmitterFamily.COMMS
    supported_classes: ClassVar[tuple[str, ...]]  # ("nbfm", "wbfm")

    def __init__(self) -> None: ...

    def generate(
        self,
        *,
        class_label: str,          # "nbfm" or "wbfm"
        sample_rate: float,        # Hz; Nyquist rate for the complex-baseband record
        duration_s: float,         # seconds; record length
        f_offset_hz: float,        # Hz; digital baseband frequency shift
        rng: torch.Generator,      # seeded generator for reproducibility
        device_id: str | None = None,
        params: BaseModel | None = None,  # TorchSigFMParams
    ) -> Signal: ...

Parameters

Name

Type

Units

Default

Purpose

class_label

str

n/a

required

"nbfm" or "wbfm"; selects per-label bandwidth defaults and ceilings

sample_rate

float

Hz

required

Complex-baseband Nyquist rate; must satisfy bandwidth_hz <= sample_rate/2

duration_s

float

s

required

Record length; round(sample_rate * duration_s) samples are produced

f_offset_hz

float

Hz

required

Baseband frequency shift applied as iq * exp(j·2π·f·t)

rng

torch.Generator

n/a

required

Seeded random generator; same seed produces identical IQ

device_id

str &#124; None

n/a

None

Propagated to SignalMetadata.device_id

params

TorchSigFMParams &#124; None

n/a

None

Pydantic model with optional bandwidth_hz field

params.bandwidth_hz

float &#124; None

Hz

label default

Carson 98%-containment bandwidth; None uses the label-specific rfgen wrapper default

Per-label defaults and ceilings:

Label

rfgen wrapper default bandwidth_hz

rfgen ceiling

Standards context

nbfm

12,500 Hz

25,000 Hz

ETSI EN 300 086 V2.1.2 (2016-08), clause 1 (12.5 / 20 / 25 kHz channel separations)

wbfm

150,000 Hz

200,000 Hz

47 CFR §§73.201 and 73.317 (200 kHz channels and the FM emission mask)

The 12.5 kHz and 150 kHz values are candidate defaults selected by the rfgen wrapper. ETSI EN 300 086 and 47 CFR §§73.201 and 73.317 provide operating context and ceiling rationale, not default values for this API.

Worked example

import torch
from rfgen.integrations.torchsig.emitters import TorchSigFMEmitter, TorchSigFMParams

em = TorchSigFMEmitter()
g = torch.Generator()
g.manual_seed(42)

sig = em.generate(
    class_label="wbfm",
    sample_rate=2_000_000.0,   # 2 MHz sample rate
    duration_s=0.1,             # 100 ms = 200,000 samples
    f_offset_hz=0.0,
    rng=g,
    params=TorchSigFMParams(bandwidth_hz=200_000.0),  # 200 kHz Carson bandwidth
)

print(sig.iq.shape)   # torch.Size([2, 200000])
print(sig.iq.dtype)   # torch.float32
print(sig.metadata.bandwidth_hz)    # 200000.0
print(sig.metadata.class_taxonomy)  # ('comms', 'fm', 'wbfm')

The emitter wraps torchsig.signals.builders.fm.fm_modulator at v2.1.1. TorchSig internally draws a modulation index m ~ Uniform(1, 10), where m = fdev / fmax, fdev is the peak frequency deviation in Hz, and fmax is the highest message-frequency cutoff in Hz. It computes fdev = (bandwidth_hz/2) / (1 + 1/m) and fmax = fdev/m, then builds a Gaussian-noise message filtered to fmax and integrates the phase. The wrapper adds the Nyquist guard, which rejects requested spectral extents that could alias across the sampled complex-baseband frequency interval, plus per-label ceiling enforcement and the f_offset_hz baseband shift.


2. What we validated

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

  1. Output shape and metadata contract (§3.1): the emitter produces a (2, N) float32 IQ tensor and correct SignalMetadata.

  2. Carson 98%-containment bandwidth (§3.2): the bandwidth-parameter identity holds algebraically, and the selected candidates pass an empirical integrated-power containment threshold of 0.98 across the tested operating points.

  3. Constant-envelope invariant (§3.3): FM is phase-only modulation; |I + jQ| stays at unity for both WBFM and NBFM.

  4. WBFM stays inside a local 200 kHz containment window (§3.4): at bandwidth_hz = 200 kHz, the measured power fits inside ±100 kHz; this is not a pointwise FCC-mask compliance claim.

  5. NBFM and WBFM per-label defaults produce distinguishable IQ (§3.5): per-label bandwidth defaults route the two labels through different spectral regimes.

  6. Baseband frequency shift is exact (§3.6): f_offset_hz migrates the spectral peak by the requested offset.

  7. Boundary and error enforcement (§3.7): per-label ceilings, Nyquist guards, and non-finite input rejection all raise the correct error types.

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


3. Evidence per claim

3.1 Output shape and metadata contract

The emitter returns a Signal whose iq field is a (2, N) torch.float32 tensor with N = round(sample_rate * duration_s). SignalMetadata carries the realised bandwidth, sample rate, carrier offset, class taxonomy, and generator name.

sig.iq.shape  == (2, N)
sig.iq.dtype  == torch.float32
sig.metadata.bandwidth_hz         == bandwidth_hz (as resolved from params or label default)
sig.metadata.realized_carrier_hz  == f_offset_hz
sig.metadata.class_taxonomy       == ("comms", "fm", class_label)
sig.metadata.snr_db               == float("inf")   # clean baseband

Tests: test_iq_shape_dtype (3 (fs, dur_s, expected_n) parameter combinations), test_metadata_carries_request, test_family_and_supported_classes in tests/validation/emitters/torchsig_fm/test_experiment_contract.py. All pass.

3.2 Carson 98%-containment bandwidth

The claim. The wrapper interprets bandwidth_hz as a candidate Carson window [-bandwidth_hz/2, +bandwidth_hz/2] and empirically requires that window to contain at least 98% of the generated signal’s average power. Haykin and Moher, Communication Systems, 5th ed., Ch. 3, gives Carson’s rule as B_carson = 2·(fdev + fmax); the 0.98 threshold here is verified by measurement rather than inferred from the parameter identity alone.

Mathematical basis. TorchSig picks m ~ Uniform(1, 10) and sets fdev = (bandwidth_hz/2) / (1 + 1/m) and fmax = fdev/m. Then:

2·(fdev + fmax) = 2·fdev·(1 + 1/m) = 2·(bandwidth_hz/2 / (1 + 1/m))·(1 + 1/m) = bandwidth_hz

This parameter identity holds for every draw of m. It does not prove strict spectral confinement: frequency modulation can produce sidebands beyond the Carson window. The containment claim therefore rests on the integrated scipy.signal.welch measurements below.

Measured result. Containment fraction is at least 0.98 across five (label, bandwidth_hz) pairs, including the selected candidate: (wbfm, 150 kHz), (wbfm, 180 kHz), (wbfm, 200 kHz), (nbfm, 12.5 kHz), and (nbfm, 25 kHz). In the exact 150 kHz WBFM cohort, all ten fixed-seed cases pass the threshold.

Figure 1 shows the measured integrated containment fraction across the plotted bandwidth sweep for both labels. It is empirical component evidence, not evidence of target parameters or regulatory mask compliance.

Figure 1: Carson 98%-containment fraction across the valid bandwidth sweep. Circles are nbfm operating points (5, 12.5, 25 kHz); squares are wbfm operating points (50, 100, 150, 200 kHz). Red dashed line marks the 0.98 lower bound from Carson's rule. All 12 seeds per point show containment ≥ 0.9999, well above the bound. Supports the Carson bandwidth claim.

Figure 2 shows the average Welch PSD (power spectral density: signal energy plotted against frequency) for WBFM and NBFM at representative bandwidths, confirming the two labels can produce distinct emission widths.

Figure 2: Average Welch PSD over 20 seeds for WBFM (left, bandwidth_hz = 200 kHz) and NBFM (right, bandwidth_hz = 12.5 kHz). Red dashed lines mark the candidate Carson half-bandwidth. The measured WBFM spectrum is wider than the measured NBFM spectrum. Supports the distinguishable-IQ and empirical-containment claims.

Tests: test_carson_98_percent_containment (5 parametrized cases, including the exact 150 kHz WBFM candidate), test_carson_holds_across_bandwidth_sweep (6 cases across both labels), and test_carson_holds_across_sample_rate_sweep (4 sample rates) in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py and test_robustness_envelope.py.

Note on -3 dB bandwidth. A reader expecting bandwidth_hz to equal the -3 dB bandwidth will be surprised. For m ~ Uniform(1, 10), the message cutoff is fmax = bandwidth_hz / (2·(m+1)). The wrapper treats bandwidth_hz as a candidate Carson window and tests its realized integrated containment; it does not claim that the FM spectrum is strictly confined to that window.

3.3 Constant-envelope invariant

The claim. FM is phase-only modulation: s(t) = exp(j·φ(t)), which has |s(t)| = 1 at every sample by construction. The wrapper deliberately does NOT subtract the empirical complex-baseband mean (unlike the digital-comms wrapper). For narrow bandwidths the phase trajectory walks slowly enough that the empirical mean magnitude can reach ~0.4 over short records; subtracting it would collapse the unit envelope into a ~30%-RMS (root-mean-square: the quadratic average of a signal’s amplitude) amplitude variation, converting a textbook constant-envelope FM signal into an AM-contaminated artefact.

Measured result. Envelope mean = 1.000, std < 0.005 for WBFM at 200 kHz bandwidth; envelope mean = 1.000, std < 0.05 for NBFM at 12.5 kHz bandwidth (float32 cast introduces small rounding ripple at narrow bandwidths).

Figure 3 shows the IQ envelope over time for both WBFM and NBFM realisations, confirming the unit-envelope property holds for both labels.

Figure 3: FM envelope |I + jQ| vs. time for WBFM (top, bandwidth_hz = 200 kHz) and NBFM (bottom, bandwidth_hz = 12.5 kHz), seed = 7. Both panels show mean ≈ 1.000 with small ripple. Dotted line marks unit envelope. Supports the constant-envelope claim.

Figure 4 shows the instantaneous frequency trajectory (how fast the signal’s frequency is changing at each moment), computed from phase unwrapped with numpy.unwrap, for both labels. The plotted realization stays within the Carson half-bandwidth.

Figure 4: Instantaneous frequency (time-derivative of the unwrapped phase, in Hz) vs. time for WBFM (top) and NBFM (bottom), seed = 11. Red dashed lines mark the candidate Carson half-bandwidth (fdev + fmax = bandwidth_hz/2). The plotted realization remains within those bounds; the integrated spectral-containment claim is measured separately. Supports the constant-envelope claim.

Tests: test_envelope_approximately_unit_wbfm, test_envelope_approximately_unit_nbfm, test_constant_envelope_holds in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py.

3.4 WBFM containment inside a nominal 200 kHz channel

The claim. At bandwidth_hz = 200 kHz, the emitter produces a WBFM signal whose measured power lies inside ±100 kHz around the carrier. Section 73.201 divides the FM broadcast band into 200 kHz channels. This integrated-power check is a deliberately stricter local containment probe; it is not compliance evidence for the pointwise attenuation mask in §73.317(b)-(d), whose first limits apply from 120 kHz offset.

Measured result. Power fraction inside ±100 kHz = 1.0000 (measured at 2 MHz sample rate, 0.1 s capture, Welch nperseg = 16384). TorchSig’s iterative-design LPF leaves negligible measured spectral leakage beyond the Carson boundary in this test configuration.

Tests: test_wbfm_200khz_local_containment_probe in tests/validation/emitters/torchsig_fm/test_empirical_known_results.py.

Out-of-band floor. Beyond twice the Carson half-bandwidth, the average PSD is below -50 dB relative to the in-band peak. Test: test_oob_floor_below_minus_50_db.

3.5 NBFM and WBFM per-label defaults produce distinguishable IQ

The claim. Calling the emitter with class_label = "nbfm" (no explicit bandwidth_hz) selects the rfgen wrapper’s 12.5 kHz candidate default; calling it with class_label = "wbfm" selects the wrapper’s 150 kHz candidate default. ETSI channel separations and the FCC channel/mask rules provide context for the label-specific regimes and ceilings, but do not define these API defaults. The two labels cannot silently produce the same IQ under any seed.

Figure 5 shows the realised bandwidth_hz from SignalMetadata across 10 seeds for both labels, confirming the defaults are constant and differ by a factor of 12.

Figure 5: Realised bandwidth_hz from metadata across 10 seeds for nbfm (circles, 12.5 kHz) and wbfm (squares, 150 kHz). Each seed shows the correct per-label default, confirming the two labels route through distinct spectral regimes. Supports the distinguishable-IQ claim.

Tested. test_nbfm_wbfm_default_bandwidth_differs in tests/validation/emitters/torchsig_fm/test_experiment_contract.py confirms both the bit-difference and the metadata bandwidth values.

3.6 Baseband frequency shift is exact

The claim. Setting f_offset_hz = f applies the complex multiply iq * exp(j·2π·f·t), which shifts the baseband spectrum by exactly f Hz without distorting the waveform.

Measured result. At f_offset_hz = 200 kHz, the spectral peak migrates from 0 to 200 kHz within ±30 kHz tolerance (approximately half the empirical -3 dB bandwidth of the FM waveform at bandwidth_hz = 100 kHz).

Figure 6 shows the Welch PSD before and after the frequency shift for three offset values, confirming the spectral envelope translates cleanly.

Figure 6: Welch PSD for f_offset_hz in {-300, 0, +300} kHz (blue, green, orange). The spectral envelope translates by the requested offset without shape distortion. bandwidth_hz = 100 kHz, sample_rate = 2 MHz, seed = 2. Supports the baseband-shift claim.

Tests: test_f_offset_shifts_spectrum in tests/validation/emitters/torchsig_fm/test_experiment_contract.py.

Determinism. Same seed produces bit-identical IQ; different seeds produce different IQ. Tests: test_same_seed_same_iq, test_different_seed_different_iq.

3.7 Boundary and error enforcement

The emitter enforces all boundary conditions before calling TorchSig, so every error surfaces as a typed rfgen exception rather than a raw backend error.

Boundary

Expected error

Test

bandwidth_hz > per-label ceiling

EmitterError naming the standard

test_nbfm_bandwidth_above_ceiling_raises, test_wbfm_bandwidth_above_ceiling_raises

bandwidth_hz > sample_rate/2

EmitterError with Nyquist context

test_bandwidth_above_fs_over_two_raises_emitter_error

bandwidth_hz + 2·&#124;f_offset_hz&#124;sample_rate

EmitterError with shifted-extent context

test_offset_plus_bw_exceeds_budget_raises, test_offset_exactly_at_budget_raises

bandwidth_hz < 1 Hz

pydantic.ValidationError from ge=1.0

test_tiny_bandwidth_rejected_by_schema

Non-finite f_offset_hz, sample_rate, duration_s

EmitterError with field name

test_nan_offset_rejected_with_emitter_error, test_nan_sample_rate_rejected_with_emitter_error, test_nan_duration_rejected_with_emitter_error, test_inf_offset_rejected_by_budget

round(sample_rate * duration_s) == 0

EmitterError

test_zero_samples_raises

TorchSig backend absent

BackendUnavailableError

test_backend_missing_surfaces_actionable_error

Unsupported class_label

EmitterError listing supported labels

test_unsupported_class_raises

All tests in tests/validation/emitters/torchsig_fm/test_robustness_envelope.py and test_experiment_contract.py pass.


4. Limits and what’s not validated

No audio source. The message is white Gaussian noise band-limited to fmax. Real FM transmissions carry voice, music, or signalling tones with distinctive spectral and temporal structure absent here.

No pre-emphasis. Real FM broadcast applies 75 µs (US) or 50 µs (Europe) pre-emphasis (a high-frequency boost applied to the audio before modulation) before modulation. The emitter omits this because pre-emphasis is part of an audio pipeline outside the framework’s scope.

No stereo pilot or sub-carriers. Commercial FM stereo (19 kHz pilot, 38 kHz L-R subcarrier) and RDS (57 kHz subcarrier) are not present. These belong to a higher-layer broadcast emitter.

No CTCSS sub-audible tones. Narrowband FM voice channels use CTCSS (Continuous Tone-Coded Squelch System: a sub-audible tone that activates a receiver’s squelch circuit) for squelch control. Out of scope at the modulation-class abstraction level.

No transmitter impairments. No PA (power amplifier) nonlinearity, IQ imbalance, oscillator phase noise, or carrier-frequency drift. The rfgen channel pipeline owns those effects; this emitter produces clean baseband IQ.

No frame structure. Continuous modulation only; no PTT (push-to-talk) on/off envelope, no preamble, no inter-burst silence.

Modulation index not user-controllable. m ~ Uniform(1, 10) is drawn per call inside TorchSig and cannot be fixed. The retained evidence does not establish that this distribution represents a particular analog-FM service or a target dataset. Exposing m would require patching TorchSig or switching to its lower-level signal-generator API.

No upper bound on n_samples. Very large sample_rate * duration_s products can exhaust memory. Resource caps belong in the pipeline-configuration layer.


5. References

5.1 Published works

Citation

Role

J. R. Carson, “Notes on the Theory of Modulation,” Proceedings of the IRE, vol. 10, no. 1, pp. 57–64, Feb. 1922, DOI 10.1109/JRPROC.1922.219793

Original analysis from which Carson’s bandwidth rule is named; the bandwidth_hz parameter is interpreted as a 98%-containment target

S. Haykin and M. Moher, Communication Systems, 5th ed., Wiley, 2009, Ch. 3, print ISBN 978-0-471-69790-9

Canonical continuous-wave modulation treatment covering FM and Carson’s rule

L. W. Couch II, Digital & Analog Communication Systems, 8th ed., Pearson, 2013, eq. 5-94, ISBN 978-0-13-291538-0

Carson’s rule restatement and modulation-index definition

47 CFR §73.201, §73.317(b)-(e), and §73.1570(b)(2), eCFR (accessed 2026-07-25; Title 47 current through 2026-07-23)

§73.201 specifies 200 kHz FM channels; §73.317, sourced at 51 FR 17028 (1986), specifies attenuation beyond 120 kHz and 75 μs pre-emphasis; §73.1570(b)(2), last amended by 89 FR 7255 (2024), references 75 kHz peak deviation

ETSI EN 300 086 V2.1.2 (2016-08), Land Mobile Service; Radio equipment … intended primarily for analogue speech

Clause 1 covers 12.5, 20, and 25 kHz channel separations; §§7.4.3.1 and 7.5 specify deviation and adjacent/alternate-channel limits

Recommendation ITU-R BS.450-3 (11/2001), Transmission standards for FM sound broadcasting at VHF, §§1.1-1.2

Specifies ±75 kHz or ±50 kHz maximum deviation and 50 μs or 75 μs pre-emphasis; it was editorially amended in 2018 and is now superseded

L. Boegner et al., “Large Scale Radio Frequency Signal Classification,” 2022, arXiv:2207.09918, DOI 10.48550/arXiv.2207.09918

TorchSig library and dataset citation

5.2 Libraries

PyPI distribution

Installed version

Documentation

Role in this validation

torchsig

2.1.1

fm_modulator source at v2.1.1

Provides fm_modulator, the iterative-design LPF, and phase integration; the primary backend under test

torch

2.12.1

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

torch.Generator for reproducible seeding; torch.from_numpy for IQ tensor construction

scipy

1.18.0

scipy.signal.welch

Welch PSD estimator used for all spectral containment and OOB (out-of-band: signal energy falling outside the intended frequency channel) floor measurements

numpy

2.4.6

numpy.unwrap and numpy.random.default_rng

Complex arithmetic, instantaneous frequency, and per-call NumPy seeding

matplotlib

3.10.8

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

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