Getting Started with CommPy¶
A beginner's guide to using the CommPy communication engineering library.
Installation¶
From PyPI¶
pip install commpy
From Source¶
git clone <repository-url>
cd CommPy
pip install -e .
Verify Installation¶
import commpy
print(commpy.__version__)
# Test a simple modulation
from commpy import BPSK_Modulator
bits = [0, 1, 0, 1]
symbols = BPSK_Modulator.modulate(bits)
print(symbols)
Basic Concepts¶
What is Digital Modulation?¶
Digital modulation converts binary data into analog signals suitable for transmission over wireless or wired channels.
CommPy provides several modulation schemes:
| Scheme | Bits/Symbol | Constellation | Use Case |
|---|---|---|---|
| BPSK | 1 | 2 points | Simple, robust |
| QPSK | 2 | 4 points | Bandwidth efficient |
| ASK-2 | 1 | 2 amplitudes | Amplitude-based |
| ASK-4 | 2 | 4 amplitudes | Bandwidth efficient |
| PSK-8 | 3 | 8 phases | High spectral efficiency |
| OOK | 1 | On/Off | Simple, optical |
What are Channels?¶
Channels model real-world transmission impairments:
BSC (Binary Symmetric Channel)
- Random bit flips with probability p
- Useful for error-correcting code testing
BEC (Binary Erasure Channel)
- Symbols are erased (lost) with probability p
- Models packet loss scenarios
AWGN (Additive White Gaussian Noise) - Gaussian noise added to signal - Most common wireless channel model
Tutorial 1: Simple Modulation & Demodulation¶
Let's modulate some bits, simulate transmission, and recover them.
import numpy as np
from commpy import BPSK_Modulator, Channels
# Step 1: Create test data
bits = np.array([1, 0, 1, 1, 0, 1, 0, 0])
print(f"Original bits: {bits}")
# Step 2: Modulate using BPSK
symbols = BPSK_Modulator.modulate(bits)
print(f"Modulated symbols: {symbols}")
# Step 3: Transmit through AWGN channel
received = Channels.awgn(symbols, snr_db=5.0)
print(f"Received (noisy): {received}")
# Step 4: Demodulate
recovered = BPSK_Modulator.demodulate(received)
print(f"Recovered bits: {recovered}")
# Step 5: Check errors
errors = np.sum(recovered != bits)
print(f"Bit errors: {errors}/{len(bits)}")
Output:
Original bits: [1 0 1 1 0 1 0 0]
Modulated symbols: [ 1.+0.j -1.+0.j 1.+0.j 1.+0.j -1.+0.j 1.+0.j -1.+0.j -1.+0.j]
Received (noisy): [ 0.85+0.2j -1.1-0.15j ...]
Recovered bits: [1 0 1 1 0 1 0 0]
Bit errors: 0/8
Tutorial 2: Bit Error Rate (BER) Simulation¶
Create a BER curve showing performance vs SNR.
import numpy as np
import matplotlib.pyplot as plt
from commpy import BPSK_Modulator, Channels
# Parameters
snr_values = np.arange(0, 11, 2) # 0, 2, 4, ..., 10 dB
num_bits = 10000
ber_values = []
# Loop over SNR values
for snr in snr_values:
# Generate random bits
bits = np.random.randint(0, 2, num_bits)
# Modulate
symbols = BPSK_Modulator.modulate(bits)
# Add noise
received = Channels.awgn(symbols, snr_db=snr)
# Demodulate
recovered = BPSK_Modulator.demodulate(received)
# Calculate BER
errors = np.sum(recovered != bits)
ber = errors / num_bits
ber_values.append(ber)
print(f"SNR={snr:2d} dB: BER={ber:.4f}")
# Plot results
plt.figure(figsize=(8, 5))
plt.semilogy(snr_values, ber_values, 'bo-', label='BPSK')
plt.xlabel('SNR (dB)')
plt.ylabel('Bit Error Rate')
plt.title('BER vs SNR for BPSK')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
Tutorial 3: Channel Comparison¶
Compare different channel models.
import numpy as np
from commpy import Channels
bits = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 1])
print("Original bits:", bits)
print()
# BSC with 20% error rate
bsc_out = Channels.bsc(bits, p=0.2)
print("BSC (20% error):", bsc_out)
print()
# BEC with 20% erasure rate
bec_out = Channels.bec(bits, p=0.2, erasure_value=-1)
print("BEC (20% erasure):", bec_out)
print()
# Count effects
bsc_errors = np.sum(bsc_out != bits)
bec_erasures = np.sum(bec_out == -1)
print(f"BSC: {bsc_errors} bit flips")
print(f"BEC: {bec_erasures} erasures")
Tutorial 4: Different Modulation Schemes¶
Compare BPSK, QPSK, and ASK.
import numpy as np
from commpy import BPSK_Modulator, QPSK_Modulator, ASK_4_Modulator
# For BPSK: 1 bit per symbol
bits_bpsk = [0, 1, 0, 1, 1, 0]
symbols_bpsk = BPSK_Modulator.modulate(bits_bpsk)
print("BPSK symbols (1 bit each):")
print(f" {bits_bpsk} → {symbols_bpsk}")
print()
# For QPSK: 2 bits per symbol (grouped as 2-bit values)
bits_qpsk = [0, 1, 2, 3] # 00, 01, 10, 11 in binary
symbols_qpsk = QPSK_Modulator.modulate(bits_qpsk)
print("QPSK symbols (2 bits each):")
print(f" {bits_qpsk} → {symbols_qpsk}")
print()
# For ASK-4: 2 bits per symbol (4 amplitude levels)
bits_ask = [0, 1, 2, 3]
symbols_ask = ASK_4_Modulator.modulate(bits_ask)
print("ASK-4 symbols (2 bits each):")
print(f" {bits_ask} → {symbols_ask}")
Tutorial 5: IQ Waveform Generation¶
Generate an IQ modulated RF signal.
import numpy as np
import matplotlib.pyplot as plt
from commpy import IQWaveform, BPSK_Modulator
# Step 1: Create bit sequence and modulate to BPSK
bits = [0, 1, 0, 1, 1, 0]
bpsk_symbols = BPSK_Modulator.modulate(bits)
# Step 2: Extract I and Q components
I = bpsk_symbols.real
Q = bpsk_symbols.imag
print(f"I symbols: {I}")
print(f"Q symbols: {Q}")
# Step 3: Create IQ waveform
waveform = IQWaveform(
I=I,
Q=Q,
T=1e-4, # 100 µs symbol period
fs=1e6, # 1 MHz sampling rate
f0=100e3, # 100 kHz carrier frequency
span=4
)
# Step 4: Analyze the waveform
print(f"\nWaveform Statistics:")
print(f" Duration: {waveform.t[-1]*1e6:.1f} µs")
print(f" Samples: {len(waveform.t)}")
print(f" Signal RMS: {np.sqrt(np.mean(waveform.s**2)):.3f}")
# Step 5: Plot
waveform.plot_waveform()
plt.show()
# Optional: Save signal data
np.savetxt('waveform_signal.txt', waveform.s)
np.savetxt('waveform_time.txt', waveform.t)
Tutorial 6: using Reproducible Results with RNG¶
Use seeded random number generators for reproducible simulations.
import numpy as np
from commpy import BPSK_Modulator, Channels
# Create a seeded RNG
rng = np.random.default_rng(seed=42)
# Use the same seed for consistent results
bits = np.array([0, 1, 0, 1, 1, 0])
symbols = BPSK_Modulator.modulate(bits)
# First transmission with seed 42
received_1 = Channels.awgn(symbols, snr_db=5, rng=np.random.default_rng(42))
# Second transmission with same seed
received_2 = Channels.awgn(symbols, snr_db=5, rng=np.random.default_rng(42))
# They should be identical
print(np.allclose(received_1, received_2)) # True
Tutorial 7: Error Correction with Hamming Codes¶
Protect data against bit errors with HammingCode.
import numpy as np
from commpy import HammingCode
code = HammingCode(m=3) # Hamming(7, 4): 4 data bits, 3 parity bits
message = np.array([1, 0, 1, 1])
codeword = code.encode(message)
# Simulate a single-bit error during transmission.
corrupted = codeword.copy()
corrupted[4] ^= 1
decoded, corrected_codeword, error_position = code.decode(corrupted)
print(f'Error detected at 1-indexed position {error_position}')
print(f'Recovered message: {decoded}') # matches the original exactly
Hamming codes correct any single bit error per codeword. For multiple errors per block, see
CyclicCode/BCHCode/ReedSolomonCode next.
Tutorial 8: Convolutional Coding + Viterbi Decoding¶
Convolutional codes protect a continuous bitstream (rather than fixed-size blocks) and are decoded with the Viterbi algorithm — maximum-likelihood sequence estimation over a trellis.
import numpy as np
from commpy import Trellis, ConvolutionalEncoder, viterbi_decode, MPSKModulator, Channels
trellis = Trellis(constraint_length=7, generators=(0o171, 0o133)) # the Voyager/NASA code
encoder = ConvolutionalEncoder(trellis)
message = np.random.randint(0, 2, 200)
codeword, _ = encoder.encode(message, terminate=True) # zero-tail terminated
# Transmit over a noisy channel and decode with soft-decision (LLR) information --
# this is more accurate than hard-decision decoding at the same SNR.
mod = MPSKModulator(2) # BPSK
symbols = mod.modulate(codeword)
received = Channels.awgn(symbols, snr_db=3.0)
llrs = mod.soft_demodulate(received, noise_var=1.0)
decoded = viterbi_decode(trellis, llrs, mode='soft', terminated=True)
errors = np.sum(decoded != message)
print(f'Bit errors after soft-decision Viterbi decoding: {errors}/{len(message)}')
Tutorial 9: Generic M-QAM/M-PSK with Soft-Decision Demodulation¶
The generic Modulator engine (MPSKModulator/MQAMModulator/MPAMModulator) replaces the
legacy per-scheme classes with one Gray-coded, unit-energy implementation, and adds soft-decision
(LLR) output for feeding into a Viterbi or other soft decoder.
import numpy as np
from commpy import MQAMModulator, Channels
mod = MQAMModulator(64) # 64-QAM: 6 bits/symbol
print(f'{mod.bits_per_symbol} bits/symbol, {mod.M} constellation points')
bits = np.random.randint(0, 2, mod.bits_per_symbol * 10_000)
symbols = mod.modulate(bits)
received = Channels.awgn(symbols, snr_db=20)
hard_bits = mod.demodulate(received)
ber = np.mean(hard_bits != bits)
print(f'Hard-decision BER at 20 dB: {ber:.4f}')
# Soft output: positive LLR favors bit 0, negative favors bit 1.
llrs = mod.soft_demodulate(received, noise_var=10**(-20 / 10))
Tutorial 10: OFDM¶
OFDM splits a wideband channel into many narrowband subcarriers, turning a frequency-selective (multipath) channel into a set of flat-fading channels — one simple equalizer tap per subcarrier instead of a complex wideband equalizer.
import numpy as np
from commpy import OFDMModulator, OFDMDemodulator, MQAMModulator, papr_db
n_fft, cp_len = 64, 16
active = range(4, 60) # null the DC bin and edge guard bands
mod = OFDMModulator(n_fft, cp_len, active_subcarriers=active)
demod = OFDMDemodulator(n_fft, cp_len, active_subcarriers=active)
qam = MQAMModulator(16)
bits = np.random.randint(0, 2, len(list(active)) * qam.bits_per_symbol * 5)
symbols = qam.modulate(bits)
tx_samples = mod.modulate(symbols) # (n_fft + cp_len) samples per OFDM symbol
print(f'PAPR of the first OFDM symbol: {papr_db(tx_samples[:n_fft]):.1f} dB')
rx_symbols = demod.demodulate(tx_samples) # exact inverse (noiseless round trip)
assert np.allclose(rx_symbols, symbols)
Tutorial 11: Source Coding with Huffman Codes¶
Compress data before transmission by exploiting known symbol statistics.
from collections import Counter
from commpy import huffman_codes, huffman_encode, huffman_decode, shannon_entropy
text = 'the quick brown fox jumps over the lazy dog'
counts = Counter(text)
probabilities = {ch: n / len(text) for ch, n in counts.items()}
codes = huffman_codes(probabilities)
encoded = huffman_encode(list(text), codes)
decoded = huffman_decode(encoded, codes)
assert ''.join(decoded) == text
print(f'Original: {len(text) * 8} bits, Huffman: {len(encoded)} bits')
print(f'Entropy bound: {shannon_entropy(list(probabilities.values())):.2f} bits/char')
Tutorial 12: Queuing Theory¶
Model waiting-line performance for a service system (e.g. a packet buffer, a call center) with closed-form M/M/1-family formulas.
from commpy import MM1Queue
queue = MM1Queue(arrival_rate=8.0, service_rate=10.0) # customers/hour
print(f'Utilization: {queue.utilization:.2f}')
print(f'Average number waiting: {queue.mean_number_in_queue:.2f}')
print(f'Average wait time: {queue.mean_wait_in_queue * 60:.1f} minutes')
For finite-capacity systems (MM1KQueue) or multiple servers (MMcQueue), see docs/API.md.
Tutorial 13: Monte-Carlo BER Simulation & SDR File I/O¶
simulate_ber formalizes the "sweep SNR, count bit errors" pattern from Tutorial 2: it runs
each SNR point until a target number of errors is observed (or a trial cap is hit), and reports
a confidence interval alongside the point estimate.
from commpy import Channels, MQAMModulator, plot_waterfall, simulate_ber
mod = MQAMModulator(16)
result = simulate_ber(
mod, Channels.awgn, snr_db_range=[6, 8, 10, 12, 14, 16],
target_errors=200, # stop early once 200 bit errors are observed
)
for snr, ber, lo, hi in zip(result.snr_db, result.error_rate, result.ci_lower, result.ci_upper):
print(f'{snr:.0f} dB: BER={ber:.5f} 95% CI=[{lo:.5f}, {hi:.5f}]')
plot_waterfall(result) # BER-vs-SNR curve with error bars; call plt.show() to display it
write_iq/read_iq and write_sigmf/read_sigmf connect CommPy to real IQ recordings: the
former is a headerless raw binary stream compatible with GNU Radio's blocks.file_source, the
latter adds a SigMF JSON metadata sidecar (sample rate, center
frequency, description).
from commpy import write_sigmf, read_sigmf
symbols = mod.modulate(bits)
write_sigmf('capture', symbols, sample_rate=1e6, center_freq=915e6, description='16-QAM test')
recovered, meta = read_sigmf('capture') # -> (ndarray, dict); meta['global']['core:sample_rate']
See examples/ber_waterfall_simulation.py and
examples/sigmf_iq_io.py for full runnable versions.
Common Patterns¶
Pattern 1: Monte Carlo Simulation¶
def monte_carlo_ber(snr_db, num_trials=1000):
"""Estimate BER for given SNR."""
ber_list = []
for _ in range(num_trials):
bits = np.random.randint(0, 2, 1000)
symbols = BPSK_Modulator.modulate(bits)
received = Channels.awgn(symbols, snr_db=snr_db)
recovered = BPSK_Modulator.demodulate(received)
ber = np.sum(recovered != bits) / len(bits)
ber_list.append(ber)
return np.mean(ber_list)
ber = monte_carlo_ber(snr_db=10)
Pattern 2: Batch Processing¶
def batch_modulate(bitstream, modulator_class):
"""Modulate multiple frames."""
frames = [bitstream[i:i+100] for i in range(0, len(bitstream), 100)]
return [modulator_class.modulate(frame) for frame in frames]
bits = np.random.randint(0, 2, 1000)
symbols = batch_modulate(bits, BPSK_Modulator)
Pattern 3: Channel Cascade¶
def apply_channels(signal):
"""Apply multiple channel effects."""
# Add noise
noisy = Channels.awgn(signal, snr_db=10)
# Convert symbols to bits, apply BSC, convert back
# ... (depends on application)
return noisy
Troubleshooting¶
Issue: ImportError when importing commpy¶
Solution: Ensure package is installed:
pip install -e . # From repo root
Issue: Shape mismatch errors¶
Solution: Check input shapes match expectations:
bits = np.array([0, 1]) # Must be 1D
symbols = BPSK_Modulator.modulate(bits)
Issue: Unexpected BER values¶
Solution: Verify SNR calculation and ensure: - Units are in dB - SNR is reasonable (negative = very noisy, >20 = very clean) - Signal and noise powers are correct
Issue: Plotting not showing¶
Solution: Add plt.show() or enable interactive mode:
%matplotlib inline # In Jupyter
plt.show() # In scripts
Next Steps¶
- Explore Examples: Check the
examples/directory - Read API Docs: See API Reference
- Run Tests:
pytest tests/ - Experiment: Modify examples for your use case
- Contribute: Submit issues or pull requests
Resources¶
- Documentation: API Reference
- GitHub: Repository
- Issues: Report bugs or request features
- Discussions: Ask questions or share ideas
Quick Reference Cheat Sheet¶
from commpy import *
# Modulation (generic engine -- preferred)
mod = MQAMModulator(16)
symbols = mod.modulate(bits)
bits = mod.demodulate(symbols)
llrs = mod.soft_demodulate(symbols, noise_var=0.1)
# Modulation (legacy, kept for backward compatibility)
symbols = BPSK_Modulator.modulate(bits)
bits = BPSK_Modulator.demodulate(symbols)
# Channel coding
codeword = HammingCode(m=3).encode(message) # or CyclicCode/BCHCode/ReedSolomonCode
codeword, _ = ConvolutionalEncoder(trellis).encode(message)
decoded = viterbi_decode(trellis, received, mode='soft')
# Channels
noisy = Channels.awgn(signal, snr_db=10)
degraded = Channels.bsc(bits, p=0.1)
erased = Channels.bec(bits, p=0.1)
# OFDM
tx = OFDMModulator(n_fft=64, cp_len=16).modulate(symbols)
# Information Theory
H = shannon_entropy([0.5, 0.25, 0.25])
capacity = channel_capacity_awgn(snr_linear=10)
codes = huffman_codes(probabilities)
# Queuing
q = MM1Queue(arrival_rate=3.0, service_rate=5.0)
# Waveforms
wf = IQWaveform(I=I, Q=Q, T=1e-3, fs=1e6, f0=0)
# Link-level simulation
result = simulate_ber(mod, Channels.awgn, snr_db_range=[6, 10, 14], target_errors=200)
plot_waterfall(result)
# SDR file I/O
write_iq('recording.cf32', symbols)
write_sigmf('capture', symbols, sample_rate=1e6, center_freq=915e6)
# Utilities
is_prime(7)
modinv(3, 7)