diff --git a/README.md b/README.md
index e6df7eed8..afba40198 100644
--- a/README.md
+++ b/README.md
@@ -6,15 +6,13 @@
-
-
The `sdr` library is a Python 3 package for software-defined radio (SDR).
diff --git a/docs/_static/extra.css b/docs/_static/extra.css
index bafa1d3fd..1025140d3 100644
--- a/docs/_static/extra.css
+++ b/docs/_static/extra.css
@@ -49,7 +49,7 @@
/* Override announcement banner colors */
.md-banner {
- background-color: var(--md-typeset-mark-color);
+ background-color: var(--md-default-bg-color--lighter);
color: var(--md-default-fg-color);
}
@@ -72,3 +72,34 @@ div.cell_output.docutils.container>div {
border: transparent;
background: transparent;
}
+
+/* Change the object info colors. */
+[data-md-color-scheme="default"] {
+ /* --objinfo-icon-fg-default: var(--md-code-hl-keyword-color); */
+ /* Properties */
+ --objinfo-icon-fg-alias: var(--md-code-hl-constant-color);
+ /* Classes */
+ --objinfo-icon-fg-data: var(--md-code-hl-string-color);
+ /* Functions, methods */
+ --objinfo-icon-fg-procedure: var(--md-code-hl-keyword-color);
+ /* Parameters (function/method arguments) */
+ --objinfo-icon-fg-sub-data: var(--md-code-hl-function-color);
+}
+
+[data-md-color-scheme="slate"] {
+ /* --objinfo-icon-fg-default: var(--md-code-hl-keyword-color); */
+ /* Properties */
+ --objinfo-icon-fg-alias: var(--md-code-hl-constant-color);
+ /* Classes */
+ --objinfo-icon-fg-data: var(--md-code-hl-string-color);
+ /* Functions, methods */
+ --objinfo-icon-fg-procedure: var(--md-code-hl-keyword-color);
+ /* Parameters (function/method arguments) */
+ --objinfo-icon-fg-sub-data: var(--md-code-hl-function-color);
+}
+
+/* Change the "class" and "property" label colors. */
+.md-typeset dl.objdesc>dt .property {
+ color: var(--md-code-hl-constant-color);
+ font-style: italic;
+}
diff --git a/docs/_templates/base.html b/docs/_templates/base.html
index 9e086e9bb..13fcd9312 100644
--- a/docs/_templates/base.html
+++ b/docs/_templates/base.html
@@ -3,7 +3,7 @@
{% block announce %}
Enjoying the library? Give us a
-
+
on
GitHub.
diff --git a/docs/conf.py b/docs/conf.py
index 0d3aa8fb6..f8205067b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -305,19 +305,81 @@
]
sphinx_immaterial_custom_admonitions = [
+ {
+ "name": "note",
+ "title": "Note",
+ "classes": ["collapsible"],
+ "icon": "fontawesome/solid/pencil",
+ "override": True,
+ },
+ {
+ "name": "warning",
+ "title": "Warning",
+ "classes": ["collapsible"],
+ "icon": "fontawesome/solid/triangle-exclamation",
+ "override": True,
+ },
+ {
+ "name": "info",
+ "icon": "fontawesome/solid/circle-info",
+ "override": True,
+ },
+ {
+ "name": "tip",
+ "icon": "fontawesome/regular/lightbulb",
+ "override": True,
+ },
+ {
+ "name": "abstract",
+ "icon": "fontawesome/regular/file-lines",
+ "override": True,
+ },
+ {
+ "name": "important",
+ "icon": "fontawesome/solid/bolt",
+ "override": True,
+ },
+ {
+ "name": "example",
+ "icon": "fontawesome/solid/terminal",
+ "override": True,
+ },
+ {
+ "name": "quote",
+ "icon": "fontawesome/solid/quote-left",
+ "override": True,
+ },
{
"name": "seealso",
"title": "See also",
"classes": ["collapsible"],
- "icon": "fontawesome/regular/eye",
+ "icon": "fontawesome/solid/magnifying-glass",
"color": (108, 117, 125), # --sd-color-secondary
"override": True,
},
{
+ "name": "versionadded",
+ "icon": "fontawesome/solid/code-commit",
+ "override": True,
+ },
+ {
+ "name": "versionchanged",
+ "icon": "fontawesome/solid/code-branch",
+ "override": True,
+ },
+ {
+ # This needs to be defined here so the icon is available when referenced in _templates/base.html
"name": "star",
- "icon": "octicons/star-16",
+ "icon": "fontawesome/regular/star",
"color": (255, 233, 3), # Gold
},
+ {
+ "name": "nomenclature",
+ "title": "Variable nomenclature",
+ "classes": ["collapsible"],
+ "icon": "fontawesome/solid/arrow-down-a-z",
+ "color": (108, 117, 125), # --sd-color-secondary
+ },
# {
# "name": "fast-performance",
# "title": "Faster performance",
diff --git a/docs/examples/peak-to-average-power.ipynb b/docs/examples/peak-to-average-power.ipynb
index 933912e56..9246c7f58 100644
--- a/docs/examples/peak-to-average-power.ipynb
+++ b/docs/examples/peak-to-average-power.ipynb
@@ -53,7 +53,7 @@
"outputs": [],
"source": [
"span = 10 # Length of the pulse shape in symbols\n",
- "sps = 200 # Samples per symbol"
+ "samples_per_symbol = 200 # Samples per symbol"
]
},
{
@@ -64,11 +64,14 @@
"source": [
"def pulse_shape(alpha):\n",
" if alpha is None:\n",
- " h = np.zeros(span * sps + 1)\n",
- " h[span * sps // 2 - sps // 2 : span * sps // 2 + sps // 2] = 1 / np.sqrt(sps)\n",
+ " h = np.zeros(span * samples_per_symbol + 1)\n",
+ " h[\n",
+ " span * samples_per_symbol // 2 - samples_per_symbol // 2 : span * samples_per_symbol // 2\n",
+ " + samples_per_symbol // 2\n",
+ " ] = 1 / np.sqrt(samples_per_symbol)\n",
" else:\n",
- " h = sdr.root_raised_cosine(alpha, span, sps)\n",
- " fir = sdr.Interpolator(sps, h)\n",
+ " h = sdr.root_raised_cosine(alpha, span, samples_per_symbol)\n",
+ " fir = sdr.Interpolator(samples_per_symbol, h)\n",
"\n",
" bb = fir(x)\n",
" pb = sdr.mix(bb, 0.02)\n",
@@ -118,7 +121,7 @@
"sdr.plot.time_domain(x_bb_0p9, label=rf\"$\\alpha = 0.9$, PAPR = {papr_bb_0p9:.2f} dB\", diff=\"line\")\n",
"sdr.plot.time_domain(x_bb_0p5, label=rf\"$\\alpha = 0.5$, PAPR = {papr_bb_0p5:.2f} dB\", diff=\"line\")\n",
"sdr.plot.time_domain(x_bb_0p1, label=rf\"$\\alpha = 0.1$, PAPR = {papr_bb_0p1:.2f} dB\", diff=\"line\")\n",
- "plt.xlim(25 * sps, 50 * sps)\n",
+ "plt.xlim(25 * samples_per_symbol, 50 * samples_per_symbol)\n",
"plt.title(\"Baseband QPSK with SRRC pulse shaping\")\n",
"plt.show()"
]
@@ -145,7 +148,7 @@
"sdr.plot.time_domain(x_pb_0p9, label=rf\"$\\alpha = 0.9$, PAPR = {papr_pb_0p9:.2f} dB\")\n",
"sdr.plot.time_domain(x_pb_0p5, label=rf\"$\\alpha = 0.5$, PAPR = {papr_pb_0p5:.2f} dB\")\n",
"sdr.plot.time_domain(x_pb_0p1, label=rf\"$\\alpha = 0.1$, PAPR = {papr_pb_0p1:.2f} dB\")\n",
- "plt.xlim(25 * sps, 50 * sps)\n",
+ "plt.xlim(25 * samples_per_symbol, 50 * samples_per_symbol)\n",
"plt.title(\"Passband QPSK with SRRC pulse shaping\")\n",
"plt.show()"
]
@@ -257,8 +260,8 @@
" pb_papr = []\n",
"\n",
" for alpha in alphas:\n",
- " h = sdr.root_raised_cosine(alpha, span, sps)\n",
- " fir = sdr.Interpolator(sps, h)\n",
+ " h = sdr.root_raised_cosine(alpha, span, samples_per_symbol)\n",
+ " fir = sdr.Interpolator(samples_per_symbol, h)\n",
"\n",
" bb = fir(x)\n",
" pb = sdr.mix(bb, 0.02)\n",
diff --git a/docs/examples/psk.ipynb b/docs/examples/psk.ipynb
index 543eacdce..4c1a66735 100644
--- a/docs/examples/psk.ipynb
+++ b/docs/examples/psk.ipynb
@@ -48,7 +48,7 @@
" x = psk.map_symbols(s)\n",
"\n",
" # Add AWGN to complex symbols to achieve desired Es/N0\n",
- " snr = sdr.esn0_to_snr(esn0, sps=1)\n",
+ " snr = sdr.esn0_to_snr(esn0, samples_per_symbol=1)\n",
" x_hat = sdr.awgn(x, snr)\n",
"\n",
" plt.figure()\n",
@@ -61,8 +61,8 @@
" plt.show()\n",
"\n",
" y = psk.modulate(s)\n",
- " # h_srrc = sdr.root_raised_cosine(0.1, 6, sps)\n",
- " # tx_mf = sdr.Interpolator(sps, h_srrc)\n",
+ " # h_srrc = sdr.root_raised_cosine(0.1, 6, samples_per_symbol)\n",
+ " # tx_mf = sdr.Interpolator(samples_per_symbol, h_srrc)\n",
" # y = tx_mf(x)\n",
"\n",
" plt.figure()\n",
@@ -111,7 +111,7 @@
}
],
"source": [
- "bpsk = sdr.PSK(2, sps=10, pulse_shape=\"srrc\")\n",
+ "bpsk = sdr.PSK(2, samples_per_symbol=10, pulse_shape=\"srrc\")\n",
"analyze_psk(bpsk, 6)"
]
},
@@ -149,7 +149,7 @@
}
],
"source": [
- "qpsk = sdr.PSK(4, phase_offset=45, sps=10, pulse_shape=\"srrc\")\n",
+ "qpsk = sdr.PSK(4, phase_offset=45, samples_per_symbol=10, pulse_shape=\"srrc\")\n",
"analyze_psk(qpsk, 9)"
]
},
@@ -187,7 +187,7 @@
}
],
"source": [
- "psk8 = sdr.PSK(8, sps=10, pulse_shape=\"srrc\")\n",
+ "psk8 = sdr.PSK(8, samples_per_symbol=10, pulse_shape=\"srrc\")\n",
"analyze_psk(psk8, 12)"
]
},
@@ -225,7 +225,7 @@
}
],
"source": [
- "psk16 = sdr.PSK(16, sps=10, pulse_shape=\"srrc\")\n",
+ "psk16 = sdr.PSK(16, samples_per_symbol=10, pulse_shape=\"srrc\")\n",
"analyze_psk(psk16, 18)"
]
},
@@ -243,7 +243,7 @@
"outputs": [],
"source": [
"def error_rates(psk, ebn0):\n",
- " esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)\n",
+ " esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)\n",
" snr = sdr.esn0_to_snr(esn0)\n",
"\n",
" ber = sdr.ErrorRate()\n",
@@ -255,7 +255,7 @@
" a_tilde = sdr.awgn(a, snr[i])\n",
" s_hat, a_hat = psk.decide_symbols(a_tilde)\n",
"\n",
- " ber.add(ebn0[i], sdr.unpack(s, psk.bps), sdr.unpack(s_hat, psk.bps))\n",
+ " ber.add(ebn0[i], sdr.unpack(s, psk.bits_per_symbol), sdr.unpack(s_hat, psk.bits_per_symbol))\n",
" ser.add(esn0[i], s, s_hat)\n",
"\n",
" return ber, ser"
@@ -457,7 +457,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.6"
+ "version": "3.11.-1"
},
"orig_nbformat": 4
},
diff --git a/docs/examples/pulse-shapes.ipynb b/docs/examples/pulse-shapes.ipynb
index 4524467ad..d982a02ce 100644
--- a/docs/examples/pulse-shapes.ipynb
+++ b/docs/examples/pulse-shapes.ipynb
@@ -35,7 +35,7 @@
"outputs": [],
"source": [
"span = 8 # Length of the pulse shape in symbols\n",
- "sps = 10 # Samples per symbol"
+ "samples_per_symbol = 10 # Samples per symbol"
]
},
{
@@ -51,8 +51,10 @@
"metadata": {},
"outputs": [],
"source": [
- "rect = np.zeros(sps * span + 1)\n",
- "rect[rect.size // 2 - sps // 2 : rect.size // 2 + sps // 2] = 1 / np.sqrt(sps)"
+ "rect = np.zeros(samples_per_symbol * span + 1)\n",
+ "rect[rect.size // 2 - samples_per_symbol // 2 : rect.size // 2 + samples_per_symbol // 2] = 1 / np.sqrt(\n",
+ " samples_per_symbol\n",
+ ")"
]
},
{
@@ -76,9 +78,9 @@
"metadata": {},
"outputs": [],
"source": [
- "rc_0p1 = sdr.raised_cosine(0.1, span, sps)\n",
- "rc_0p5 = sdr.raised_cosine(0.5, span, sps)\n",
- "rc_0p9 = sdr.raised_cosine(0.9, span, sps)"
+ "rc_0p1 = sdr.raised_cosine(0.1, span, samples_per_symbol)\n",
+ "rc_0p5 = sdr.raised_cosine(0.5, span, samples_per_symbol)\n",
+ "rc_0p9 = sdr.raised_cosine(0.9, span, samples_per_symbol)"
]
},
{
@@ -132,11 +134,11 @@
],
"source": [
"plt.figure()\n",
- "sdr.plot.time_domain(np.roll(rc_0p1, -3 * sps))\n",
- "sdr.plot.time_domain(np.roll(rc_0p1, -2 * sps))\n",
- "sdr.plot.time_domain(np.roll(rc_0p1, -1 * sps))\n",
- "sdr.plot.time_domain(np.roll(rc_0p1, 0 * sps))\n",
- "sdr.plot.time_domain(np.roll(rc_0p1, 1 * sps))\n",
+ "sdr.plot.time_domain(np.roll(rc_0p1, -3 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(rc_0p1, -2 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(rc_0p1, -1 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(rc_0p1, 0 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(rc_0p1, 1 * samples_per_symbol))\n",
"plt.xlim(0, 60)\n",
"plt.title(\"Raised cosine pulses for adjacent symbols\")\n",
"plt.show()"
@@ -160,10 +162,10 @@
],
"source": [
"plt.figure()\n",
- "sdr.plot.magnitude_response(rect, sample_rate=sps, color=\"k\", label=\"Rectangular\")\n",
- "sdr.plot.magnitude_response(rc_0p1, sample_rate=sps, label=r\"$\\alpha = 0.1$\")\n",
- "sdr.plot.magnitude_response(rc_0p5, sample_rate=sps, label=r\"$\\alpha = 0.5$\")\n",
- "sdr.plot.magnitude_response(rc_0p9, sample_rate=sps, label=r\"$\\alpha = 0.9$\")\n",
+ "sdr.plot.magnitude_response(rect, sample_rate=samples_per_symbol, color=\"k\", label=\"Rectangular\")\n",
+ "sdr.plot.magnitude_response(rc_0p1, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.1$\")\n",
+ "sdr.plot.magnitude_response(rc_0p5, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.5$\")\n",
+ "sdr.plot.magnitude_response(rc_0p9, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.9$\")\n",
"plt.xlabel(\"Normalized frequency, $f/f_{sym}$\")\n",
"plt.show()"
]
@@ -196,10 +198,10 @@
],
"source": [
"# Compute the one-sided power spectral density of the pulses\n",
- "w, H_rect = scipy.signal.freqz(rect, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_rc_0p1 = scipy.signal.freqz(rc_0p1, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_rc_0p5 = scipy.signal.freqz(rc_0p5, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_rc_0p9 = scipy.signal.freqz(rc_0p9, 1, worN=1024, whole=False, fs=sps)\n",
+ "w, H_rect = scipy.signal.freqz(rect, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_rc_0p1 = scipy.signal.freqz(rc_0p1, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_rc_0p5 = scipy.signal.freqz(rc_0p5, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_rc_0p9 = scipy.signal.freqz(rc_0p9, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
"\n",
"# Compute the relative power in the main lobe of the pulses\n",
"P_rect = sdr.db(np.cumsum(np.abs(H_rect) ** 2) / np.sum(np.abs(H_rect) ** 2))\n",
@@ -242,9 +244,9 @@
"metadata": {},
"outputs": [],
"source": [
- "srrc_0p1 = sdr.root_raised_cosine(0.1, span, sps)\n",
- "srrc_0p5 = sdr.root_raised_cosine(0.5, span, sps)\n",
- "srrc_0p9 = sdr.root_raised_cosine(0.9, span, sps)"
+ "srrc_0p1 = sdr.root_raised_cosine(0.1, span, samples_per_symbol)\n",
+ "srrc_0p5 = sdr.root_raised_cosine(0.5, span, samples_per_symbol)\n",
+ "srrc_0p9 = sdr.root_raised_cosine(0.9, span, samples_per_symbol)"
]
},
{
@@ -298,11 +300,11 @@
],
"source": [
"plt.figure()\n",
- "sdr.plot.time_domain(np.roll(srrc_0p1, -3 * sps))\n",
- "sdr.plot.time_domain(np.roll(srrc_0p1, -2 * sps))\n",
- "sdr.plot.time_domain(np.roll(srrc_0p1, -1 * sps))\n",
- "sdr.plot.time_domain(np.roll(srrc_0p1, 0 * sps))\n",
- "sdr.plot.time_domain(np.roll(srrc_0p1, 1 * sps))\n",
+ "sdr.plot.time_domain(np.roll(srrc_0p1, -3 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(srrc_0p1, -2 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(srrc_0p1, -1 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(srrc_0p1, 0 * samples_per_symbol))\n",
+ "sdr.plot.time_domain(np.roll(srrc_0p1, 1 * samples_per_symbol))\n",
"plt.xlim(0, 60)\n",
"plt.title(\"Square-root raised cosine pulses for adjacent symbols\")\n",
"plt.show()"
@@ -326,10 +328,10 @@
],
"source": [
"plt.figure()\n",
- "sdr.plot.magnitude_response(rect, sample_rate=sps, color=\"k\", label=\"Rectangular\")\n",
- "sdr.plot.magnitude_response(srrc_0p1, sample_rate=sps, label=r\"$\\alpha = 0.1$\")\n",
- "sdr.plot.magnitude_response(srrc_0p5, sample_rate=sps, label=r\"$\\alpha = 0.5$\")\n",
- "sdr.plot.magnitude_response(srrc_0p9, sample_rate=sps, label=r\"$\\alpha = 0.9$\")\n",
+ "sdr.plot.magnitude_response(rect, sample_rate=samples_per_symbol, color=\"k\", label=\"Rectangular\")\n",
+ "sdr.plot.magnitude_response(srrc_0p1, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.1$\")\n",
+ "sdr.plot.magnitude_response(srrc_0p5, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.5$\")\n",
+ "sdr.plot.magnitude_response(srrc_0p9, sample_rate=samples_per_symbol, label=r\"$\\alpha = 0.9$\")\n",
"plt.xlabel(\"Normalized frequency, $f/f_{sym}$\")\n",
"plt.show()"
]
@@ -361,10 +363,10 @@
],
"source": [
"# Compute the one-sided power spectral density of the pulses\n",
- "w, H_rect = scipy.signal.freqz(rect, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_srrc_0p1 = scipy.signal.freqz(srrc_0p1, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_srrc_0p5 = scipy.signal.freqz(srrc_0p5, 1, worN=1024, whole=False, fs=sps)\n",
- "w, H_srrc_0p9 = scipy.signal.freqz(srrc_0p9, 1, worN=1024, whole=False, fs=sps)\n",
+ "w, H_rect = scipy.signal.freqz(rect, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_srrc_0p1 = scipy.signal.freqz(srrc_0p1, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_srrc_0p5 = scipy.signal.freqz(srrc_0p5, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
+ "w, H_srrc_0p9 = scipy.signal.freqz(srrc_0p9, 1, worN=1024, whole=False, fs=samples_per_symbol)\n",
"\n",
"# Compute the relative power in the main lobe of the pulses\n",
"P_rect = sdr.db(np.cumsum(np.abs(H_rect) ** 2) / np.sum(np.abs(H_rect) ** 2))\n",
@@ -407,9 +409,9 @@
"metadata": {},
"outputs": [],
"source": [
- "gauss_0p1 = sdr.gaussian(0.1, span, sps)\n",
- "gauss_0p2 = sdr.gaussian(0.2, span, sps)\n",
- "gauss_0p3 = sdr.gaussian(0.3, span, sps)"
+ "gauss_0p1 = sdr.gaussian(0.1, span, samples_per_symbol)\n",
+ "gauss_0p2 = sdr.gaussian(0.2, span, samples_per_symbol)\n",
+ "gauss_0p3 = sdr.gaussian(0.3, span, samples_per_symbol)"
]
},
{
@@ -462,9 +464,9 @@
],
"source": [
"plt.figure()\n",
- "sdr.plot.magnitude_response(gauss_0p1, sample_rate=sps, label=r\"$B T_{sym} = 0.1$\")\n",
- "sdr.plot.magnitude_response(gauss_0p2, sample_rate=sps, label=r\"$B T_{sym} = 0.2$\")\n",
- "sdr.plot.magnitude_response(gauss_0p3, sample_rate=sps, label=r\"$B T_{sym} = 0.3$\")\n",
+ "sdr.plot.magnitude_response(gauss_0p1, sample_rate=samples_per_symbol, label=r\"$B T_{sym} = 0.1$\")\n",
+ "sdr.plot.magnitude_response(gauss_0p2, sample_rate=samples_per_symbol, label=r\"$B T_{sym} = 0.2$\")\n",
+ "sdr.plot.magnitude_response(gauss_0p3, sample_rate=samples_per_symbol, label=r\"$B T_{sym} = 0.3$\")\n",
"plt.xlabel(\"Normalized frequency, $f/f_{sym}$\")\n",
"plt.show()"
]
@@ -486,7 +488,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.6"
+ "version": "3.11.-1"
},
"orig_nbformat": 4
},
diff --git a/docs/index.rst b/docs/index.rst
index a36a8b930..b0e8b7a8f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -9,12 +9,13 @@ sdr
-
-
-
-
-
+
+
The :obj:`sdr` library is a Python 3 package for software-defined radio (SDR).
diff --git a/src/sdr/_conversion.py b/src/sdr/_conversion.py
index 158a828ad..e35ec096b 100644
--- a/src/sdr/_conversion.py
+++ b/src/sdr/_conversion.py
@@ -143,7 +143,7 @@ def linear(
@export
-def ebn0_to_esn0(ebn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np.float64]:
+def ebn0_to_esn0(ebn0: npt.ArrayLike, bits_per_symbol: int, code_rate: int = 1) -> npt.NDArray[np.float64]:
r"""
Converts from $E_b/N_0$ to $E_s/N_0$.
@@ -153,8 +153,8 @@ def ebn0_to_esn0(ebn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np
Arguments:
ebn0: Bit energy $E_b$ to noise PSD $N_0$ ratio in dB.
- bps: Coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
- rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
+ bits_per_symbol: The number of coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
+ code_rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
number of coded bits.
Returns:
@@ -165,25 +165,27 @@ def ebn0_to_esn0(ebn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np
.. ipython:: python
- sdr.ebn0_to_esn0(5, 2, rate=2/3)
+ sdr.ebn0_to_esn0(5, 2, code_rate=2/3)
Convert from $E_b/N_0 = 10$ dB to $E_s/N_0$ for a 16-QAM signal with $r = 1$.
.. ipython:: python
- sdr.ebn0_to_esn0(10, 4, rate=1)
+ sdr.ebn0_to_esn0(10, 4, code_rate=1)
Group:
conversions-snrs
"""
ebn0 = np.asarray(ebn0) # Energy per information bit
- ecn0 = ebn0 + db(rate) # Energy per coded bit
- esn0 = ecn0 + db(bps) # Energy per symbol
+ ecn0 = ebn0 + db(code_rate) # Energy per coded bit
+ esn0 = ecn0 + db(bits_per_symbol) # Energy per symbol
return esn0
@export
-def ebn0_to_snr(ebn0: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> npt.NDArray[np.float64]:
+def ebn0_to_snr(
+ ebn0: npt.ArrayLike, bits_per_symbol: int, code_rate: int = 1, samples_per_symbol: int = 1
+) -> npt.NDArray[np.float64]:
r"""
Converts from $E_b/N_0$ to $S/N$.
@@ -193,10 +195,10 @@ def ebn0_to_snr(ebn0: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> n
Arguments:
ebn0: Bit energy $E_b$ to noise PSD $N_0$ ratio in dB.
- bps: Coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
- rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
+ bits_per_symbol: The number of coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
+ code_rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
number of coded bits.
- sps: Samples per symbol $f_s / f_{sym}$.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
Returns:
The signal-to-noise ratio $S/N$ in dB.
@@ -206,19 +208,19 @@ def ebn0_to_snr(ebn0: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> n
.. ipython:: python
- sdr.ebn0_to_snr(5, 2, rate=2/3, sps=1)
+ sdr.ebn0_to_snr(5, 2, code_rate=2/3, samples_per_symbol=1)
Convert from $E_b/N_0 = 10$ dB to $S/N$ for a 16-QAM signal with $r = 1$ and 4 samples per symbol.
.. ipython:: python
- sdr.ebn0_to_snr(10, 4, rate=1, sps=4)
+ sdr.ebn0_to_snr(10, 4, code_rate=1, samples_per_symbol=4)
Group:
conversions-snrs
"""
- esn0 = ebn0_to_esn0(ebn0, bps, rate=rate) # SNR per symbol
- snr = esn0 - db(sps) # SNR per sample
+ esn0 = ebn0_to_esn0(ebn0, bits_per_symbol, code_rate=code_rate) # SNR per symbol
+ snr = esn0 - db(samples_per_symbol) # SNR per sample
return snr
@@ -228,7 +230,7 @@ def ebn0_to_snr(ebn0: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> n
@export
-def esn0_to_ebn0(esn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np.float64]:
+def esn0_to_ebn0(esn0: npt.ArrayLike, bits_per_symbol: int, code_rate: int = 1) -> npt.NDArray[np.float64]:
r"""
Converts from $E_s/N_0$ to $E_b/N_0$.
@@ -238,8 +240,8 @@ def esn0_to_ebn0(esn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np
Arguments:
esn0: Symbol energy $E_s$ to noise PSD $N_0$ ratio in dB.
- bps: Coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
- rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
+ bits_per_symbol: The number of coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
+ code_rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
number of coded bits.
Returns:
@@ -250,25 +252,25 @@ def esn0_to_ebn0(esn0: npt.ArrayLike, bps: int, rate: int = 1) -> npt.NDArray[np
.. ipython:: python
- sdr.esn0_to_ebn0(5, 2, rate=2/3)
+ sdr.esn0_to_ebn0(5, 2, code_rate=2/3)
Convert from $E_s/N_0 = 10$ dB to $E_b/N_0$ for a 16-QAM signal with $r = 1$.
.. ipython:: python
- sdr.esn0_to_ebn0(10, 4, rate=1)
+ sdr.esn0_to_ebn0(10, 4, code_rate=1)
Group:
conversions-snrs
"""
esn0 = np.asarray(esn0)
- ecn0 = esn0 - db(bps) # Energy per coded bit
- ebn0 = ecn0 - db(rate) # Energy per information bit
+ ecn0 = esn0 - db(bits_per_symbol) # Energy per coded bit
+ ebn0 = ecn0 - db(code_rate) # Energy per information bit
return ebn0
@export
-def esn0_to_snr(esn0: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
+def esn0_to_snr(esn0: npt.ArrayLike, samples_per_symbol: int = 1) -> npt.NDArray[np.float64]:
r"""
Converts from $E_s/N_0$ to $S/N$.
@@ -278,7 +280,7 @@ def esn0_to_snr(esn0: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
Arguments:
esn0: Symbol energy $E_s$ to noise PSD $N_0$ ratio in dB.
- sps: Samples per symbol $f_s / f_{sym}$.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
Returns:
The signal-to-noise ratio $S/N$ in dB.
@@ -289,19 +291,19 @@ def esn0_to_snr(esn0: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
.. ipython:: python
- sdr.esn0_to_snr(5, sps=1)
+ sdr.esn0_to_snr(5, samples_per_symbol=1)
Convert from $E_s/N_0 = 10$ dB to $S/N$ with 4 samples per symbol.
.. ipython:: python
- sdr.esn0_to_snr(10, sps=4)
+ sdr.esn0_to_snr(10, samples_per_symbol=4)
Group:
conversions-snrs
"""
esn0 = np.asarray(esn0) # SNR per symbol
- snr = esn0 - db(sps) # SNR per sample
+ snr = esn0 - db(samples_per_symbol) # SNR per sample
return snr
@@ -311,7 +313,9 @@ def esn0_to_snr(esn0: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
@export
-def snr_to_ebn0(snr: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> npt.NDArray[np.float64]:
+def snr_to_ebn0(
+ snr: npt.ArrayLike, bits_per_symbol: int, code_rate: int = 1, samples_per_symbol: int = 1
+) -> npt.NDArray[np.float64]:
r"""
Converts from $S/N$ to $E_b/N_0$.
@@ -321,10 +325,10 @@ def snr_to_ebn0(snr: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> np
Arguments:
snr: Signal-to-noise ratio $S/N$ in dB.
- bps: Coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
- rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
+ bits_per_symbol: The number of coded bits per symbol $\log_2 M$, where $M$ is the modulation order.
+ code_rate: Code rate $r = k/n$, where $k$ is the number of information bits and $n$ is the
number of coded bits.
- sps: Samples per symbol $f_s / f_{sym}$.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
Returns:
The bit energy $E_b$ to noise PSD $N_0$ ratio in dB.
@@ -334,25 +338,25 @@ def snr_to_ebn0(snr: npt.ArrayLike, bps: int, rate: int = 1, sps: int = 1) -> np
.. ipython:: python
- sdr.snr_to_ebn0(5, 2, rate=2/3, sps=1)
+ sdr.snr_to_ebn0(5, 2, code_rate=2/3, samples_per_symbol=1)
Convert from $S/N = 10$ dB to $E_b/N_0$ for a 16-QAM signal with $r = 1$ and 4 samples per symbol.
.. ipython:: python
- sdr.snr_to_ebn0(10, 4, rate=1, sps=4)
+ sdr.snr_to_ebn0(10, 4, code_rate=1, samples_per_symbol=4)
Group:
conversions-snrs
"""
snr = np.asarray(snr) # SNR per sample
- esn0 = snr_to_esn0(snr, sps=sps) # Energy per symbol
- ebn0 = esn0_to_ebn0(esn0, bps, rate=rate) # Energy per information bit
+ esn0 = snr_to_esn0(snr, samples_per_symbol=samples_per_symbol) # Energy per symbol
+ ebn0 = esn0_to_ebn0(esn0, bits_per_symbol, code_rate=code_rate) # Energy per information bit
return ebn0
@export
-def snr_to_esn0(snr: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
+def snr_to_esn0(snr: npt.ArrayLike, samples_per_symbol: int = 1) -> npt.NDArray[np.float64]:
r"""
Converts from $S/N$ to $E_s/N_0$.
@@ -362,7 +366,7 @@ def snr_to_esn0(snr: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
Arguments:
snr: Signal-to-noise ratio $S/N$ in dB.
- sps: Samples per symbol $f_s / f_{sym}$.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
Returns:
The symbol energy $E_s$ to noise PSD $N_0$ ratio in dB.
@@ -373,17 +377,17 @@ def snr_to_esn0(snr: npt.ArrayLike, sps: int = 1) -> npt.NDArray[np.float64]:
.. ipython:: python
- sdr.snr_to_esn0(5, sps=1)
+ sdr.snr_to_esn0(5, samples_per_symbol=1)
Convert from $S/N = 10$ dB to $E_s/N_0$ with 4 samples per symbol.
.. ipython:: python
- sdr.snr_to_esn0(10, sps=4)
+ sdr.snr_to_esn0(10, samples_per_symbol=4)
Group:
conversions-snrs
"""
snr = np.asarray(snr)
- esn0 = snr + db(sps)
+ esn0 = snr + db(samples_per_symbol)
return esn0
diff --git a/src/sdr/_measurement/_modulation.py b/src/sdr/_measurement/_modulation.py
index 34958b4f8..f5788eadf 100644
--- a/src/sdr/_measurement/_modulation.py
+++ b/src/sdr/_measurement/_modulation.py
@@ -215,7 +215,7 @@ def rms_bandwidth(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
psk = sdr.PSK(2, pulse_shape="rect")
symbols = np.random.randint(0, psk.order, 10_000)
x_rect = psk.modulate(symbols)
- sdr.rms_bandwidth(x_rect, sample_rate=symbol_rate * psk.sps)
+ sdr.rms_bandwidth(x_rect, sample_rate=symbol_rate * psk.samples_per_symbol)
Make the same measurements with square-root raised cosine (SRRC) pulse shaping. The SRRC spectrum is narrower
and, therefore, closer to the rectangular spectrum.
@@ -225,7 +225,7 @@ def rms_bandwidth(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
psk = sdr.PSK(2, pulse_shape="srrc")
symbols = np.random.randint(0, psk.order, 10_000)
x_srrc = psk.modulate(symbols)
- sdr.rms_bandwidth(x_srrc, sample_rate=symbol_rate * psk.sps)
+ sdr.rms_bandwidth(x_srrc, sample_rate=symbol_rate * psk.samples_per_symbol)
Plot the power spectral density (PSD) of the rectangular and SRRC pulse-shaped signals.
@@ -233,8 +233,8 @@ def rms_bandwidth(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
@savefig sdr_rms_bandwidth_1.png
plt.figure(); \
- sdr.plot.periodogram(x_rect, sample_rate=symbol_rate * psk.sps, label="Rectangular"); \
- sdr.plot.periodogram(x_srrc, sample_rate=symbol_rate * psk.sps, label="SRRC");
+ sdr.plot.periodogram(x_rect, sample_rate=symbol_rate * psk.samples_per_symbol, label="Rectangular"); \
+ sdr.plot.periodogram(x_srrc, sample_rate=symbol_rate * psk.samples_per_symbol, label="SRRC");
Group:
measurement-modulation
@@ -250,11 +250,13 @@ def rms_bandwidth(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
psd = np.fft.fftshift(psd)
# Calculate the centroid of the PSD
- f_mean = scipy.integrate.simpson(f * psd, f) / scipy.integrate.simpson(psd, f)
+ f_mean = scipy.integrate.simpson(f * psd, x=f)
+ f_mean /= scipy.integrate.simpson(psd, x=f)
f -= f_mean
# Calculate the RMS bandwidth
- ms_bandwidth = scipy.integrate.simpson(f**2 * psd, f) / scipy.integrate.simpson(psd, f)
+ ms_bandwidth = scipy.integrate.simpson(f**2 * psd, x=f)
+ ms_bandwidth /= scipy.integrate.simpson(psd, x=f)
rms_bandwidth = np.sqrt(float(ms_bandwidth))
return rms_bandwidth
@@ -308,8 +310,8 @@ def rms_integration_time(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
.. ipython:: python
symbol_rate = 100 # symbols/s
- sps = 100 # samples/symbol
- sample_rate = symbol_rate * sps # samples/s
+ samples_per_symbol = 100 # samples/symbol
+ sample_rate = symbol_rate * samples_per_symbol # samples/s
n_symbols = symbol_rate # Make a 1-second long signal
t_s = n_symbols / symbol_rate # Integration time (s)
t_s / np.sqrt(12)
@@ -319,7 +321,7 @@ def rms_integration_time(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
.. ipython:: python
- psk = sdr.PSK(2, pulse_shape="rect", sps=sps)
+ psk = sdr.PSK(2, pulse_shape="rect", samples_per_symbol=samples_per_symbol)
symbols = np.random.randint(0, psk.order, n_symbols)
x_rect = psk.modulate(symbols).real
sdr.rms_integration_time(x_rect, sample_rate=sample_rate)
@@ -332,7 +334,7 @@ def rms_integration_time(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
.. ipython:: python
- psk = sdr.PSK(2, pulse_shape="srrc", sps=sps)
+ psk = sdr.PSK(2, pulse_shape="srrc", samples_per_symbol=samples_per_symbol)
symbols = np.random.randint(0, psk.order, n_symbols)
x_srrc = psk.modulate(symbols).real
sdr.rms_integration_time(x_srrc, sample_rate=sample_rate)
@@ -363,11 +365,13 @@ def rms_integration_time(x: npt.ArrayLike, sample_rate: float = 1.0) -> float:
t = np.arange(x.size) / sample_rate
# Calculate the centroid of the signal
- t_mean = scipy.integrate.simpson(t * np.abs(x) ** 2, t) / scipy.integrate.simpson(np.abs(x) ** 2, t)
+ t_mean = scipy.integrate.simpson(t * np.abs(x) ** 2, x=t)
+ t_mean /= scipy.integrate.simpson(np.abs(x) ** 2, x=t)
t -= t_mean
# Calculate the RMS integration time
- ms_integration_time = scipy.integrate.simpson(t**2 * np.abs(x) ** 2, t) / scipy.integrate.simpson(np.abs(x) ** 2, t)
+ ms_integration_time = scipy.integrate.simpson(t**2 * np.abs(x) ** 2, x=t)
+ ms_integration_time /= scipy.integrate.simpson(np.abs(x) ** 2, x=t)
rms_integration_time = np.sqrt(float(ms_integration_time))
return rms_integration_time
diff --git a/src/sdr/_modulation/_cpm.py b/src/sdr/_modulation/_cpm.py
index ce9092f20..0478bae16 100644
--- a/src/sdr/_modulation/_cpm.py
+++ b/src/sdr/_modulation/_cpm.py
@@ -22,12 +22,18 @@ class CPM:
r"""
Implements continuous-phase modulation (CPM).
- Note:
- The nomenclature for variable names in continuous-phase modulation is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Group:
modulation-continuous-phase
@@ -39,7 +45,8 @@ def __init__(
index: float = 0.5,
symbol_labels: Literal["bin", "gray"] | npt.ArrayLike = "bin",
phase_offset: float = 0.0,
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
# pulse_shape: npt.ArrayLike | Literal["rect", "rc", "srrc", "gaussian"] = "rect",
pulse_shape: npt.ArrayLike | Literal["rect"] = "rect",
span: int = 1,
@@ -61,11 +68,12 @@ def __init__(
the new symbol labels.
phase_offset: A phase offset $\phi$ in degrees.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
pulse_shape: The pulse shape $h[n]$ of the instantaneous frequency of the signal. If a string is passed,
the pulse shape is normalized such that the maximum value is 1.
- - `npt.ArrayLike`: A custom pulse shape. It is important that `sps` matches the design
+ - `npt.ArrayLike`: A custom pulse shape. It is important that `samples_per_symbol` matches the design
of the pulse shape. See :ref:`pulse-shaping-functions`.
- `"rect"`: Rectangular pulse shape.
@@ -81,7 +89,7 @@ def __init__(
if not np.log2(order).is_integer():
raise ValueError(f"Argument 'order' must be a power of 2, not {order}.")
self._order = order # Modulation order
- self._bps = int(np.log2(self._order)) # Coded bits per symbol
+ self._bits_per_symbol = int(np.log2(self._order)) # Coded bits per symbol
if not isinstance(index, (int, float)):
raise TypeError(f"Argument 'index' must be a number, not {type(index)}.")
@@ -90,10 +98,10 @@ def __init__(
self._index = index # Modulation index
if symbol_labels == "bin":
- self._symbol_labels = binary_code(self.bps)
+ self._symbol_labels = binary_code(self.bits_per_symbol)
self._symbol_labels_str = "bin"
elif symbol_labels == "gray":
- self._symbol_labels = gray_code(self.bps)
+ self._symbol_labels = gray_code(self.bits_per_symbol)
self._symbol_labels_str = "gray"
else:
if not np.array_equal(np.sort(symbol_labels), np.arange(self.order)):
@@ -105,11 +113,17 @@ def __init__(
raise TypeError(f"Argument 'phase_offset' must be a number, not {type(phase_offset)}.")
self._phase_offset = phase_offset # Phase offset in degrees
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps > 1:
- raise ValueError(f"Argument 'sps' must be greater than 1, not {sps}.")
- self._sps = sps # Samples per symbol
+ if not isinstance(symbol_rate, (int, float)):
+ raise TypeError(f"Argument 'symbol_rate' must be a number, not {type(symbol_rate)}.")
+ if not symbol_rate > 0:
+ raise ValueError(f"Argument 'symbol_rate' must be positive, not {symbol_rate}.")
+ self._symbol_rate = symbol_rate # symbols/s
+
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol > 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be greater than 1, not {samples_per_symbol}.")
+ self._samples_per_symbol = samples_per_symbol # Samples per symbol
if not isinstance(span, int):
raise TypeError(f"Argument 'span' must be an integer, not {type(span)}.")
@@ -118,22 +132,22 @@ def __init__(
if isinstance(pulse_shape, str):
if pulse_shape == "rect":
- self._pulse_shape = rectangular(self.sps, span=span, norm="passband") / 2
- # self._pulse_shape = np.ones(self.sps * span) / (self.sps * span) / 2
+ self._pulse_shape = rectangular(self.samples_per_symbol, span=span, norm="passband") / 2
+ # self._pulse_shape = np.ones(self.samples_per_symbol * span) / (self.samples_per_symbol * span) / 2
else:
raise ValueError(f"Argument 'pulse_shape' must be 'rect', not {pulse_shape!r}.")
# elif pulse_shape == "sine":
- # # self._pulse_shape = half_sine(self.sps, norm="passband") / 2
- # self._pulse_shape = 1 - np.cos(2 * np.pi * np.arange(0.5, self.sps + 0.5, 1) / self.sps)
+ # # self._pulse_shape = half_sine(self.samples_per_symbol, norm="passband") / 2
+ # self._pulse_shape = 1 - np.cos(2 * np.pi * np.arange(0.5, self.samples_per_symbol + 0.5, 1) / self.samples_per_symbol)
# self._pulse_shape = _normalize(self._pulse_shape, norm="passband") / 2
# elif pulse_shape == "rc":
# if alpha is None:
# alpha = 0.2
- # self._pulse_shape = raised_cosine(alpha, span, self.sps, norm="passband") / 2
+ # self._pulse_shape = raised_cosine(alpha, span, self.samples_per_symbol, norm="passband") / 2
# elif pulse_shape == "gaussian":
# if time_bandwidth is None:
# time_bandwidth = 0.3
- # self._pulse_shape = gaussian(time_bandwidth, span, self.sps, norm="passband") / 2
+ # self._pulse_shape = gaussian(time_bandwidth, span, self.samples_per_symbol, norm="passband") / 2
# else:
# raise ValueError(f"Argument 'pulse_shape' must be 'rect', 'rc', or 'srrc', not {pulse_shape!r}.")
else:
@@ -147,8 +161,8 @@ def __init__(
# if time_bandwidth is not None and pulse_shape not in ["gaussian"]:
# raise ValueError("Argument 'time_bandwidth' is only valid for 'gaussian' pulse shape, not {pulse_shape!r}.")
- self._tx_filter = Interpolator(self.sps, self.pulse_shape) # Transmit pulse shaping filter
- self._rx_filter = Decimator(self.sps, self.pulse_shape[::-1].conj()) # Receive matched filter
+ self._tx_filter = Interpolator(self.samples_per_symbol, self.pulse_shape) # Transmit pulse shaping filter
+ self._rx_filter = Decimator(self.samples_per_symbol, self.pulse_shape[::-1].conj()) # Receive matched filter
self._nco = NCO()
@@ -170,8 +184,8 @@ def modulate(self, s: npt.ArrayLike) -> npt.NDArray[np.complex128]:
s: The decimal symbols $s[k]$ to modulate, $0$ to $M-1$.
Returns:
- The pulse-shaped complex samples $x[n]$ with :obj:`sps` samples per symbol
- and length `sps * s.size + pulse_shape.size - 1`.
+ The pulse-shaped complex samples $x[n]$ with :obj:`samples_per_symbol` samples per symbol
+ and length `samples_per_symbol * s.size + pulse_shape.size - 1`.
"""
s = np.asarray(s) # Decimal symbols
return self._modulate(s)
@@ -187,8 +201,8 @@ def _modulate(self, s: npt.NDArray[np.int_]) -> npt.NDArray[np.complex128]:
# phase_ps = np.insert(phase_ps, 0, 0) # Start with phase 0
# phase_ps = phase_ps[:-1] # Trim last phase
# # return phase_ps
- # x = np.exp(1j * np.pi / self.sps * phase_ps) # Complex samples
- # # x = np.exp(1j * (2 * np.pi / self.sps * self.index * freq_ps + self._phase_offset)) # Complex samples
+ # x = np.exp(1j * np.pi / self.samples_per_symbol * phase_ps) # Complex samples
+ # # x = np.exp(1j * (2 * np.pi / self.samples_per_symbol * self.index * freq_ps + self._phase_offset)) # Complex samples
x = self._nco(2 * np.pi * freq_ps, output="complex-exp") # Complex samples
return x
@@ -204,8 +218,8 @@ def demodulate(self, x_tilde: npt.ArrayLike) -> npt.NDArray[np.int_]:
This method applies matched filtering and maximum-likelihood estimation.
Arguments:
- x_tilde: The received pulse-shaped complex samples $\tilde{x}[n]$ to demodulate, with :obj:`sps`
- samples per symbol and length `sps * s_hat.size + pulse_shape.size - 1`.
+ x_tilde: The received pulse-shaped complex samples $\tilde{x}[n]$ to demodulate, with :obj:`samples_per_symbol`
+ samples per symbol and length `samples_per_symbol * s_hat.size + pulse_shape.size - 1`.
Returns:
- The decimal symbol decisions $\hat{s}[k]$, $0$ to $M-1$.
@@ -256,11 +270,39 @@ def order(self) -> int:
return self._order
@property
- def bps(self) -> int:
+ def symbol_rate(self) -> float:
+ r"""
+ The symbol rate $f_{sym}$ in symbols/s.
+ """
+ return self._symbol_rate
+
+ @property
+ def bits_per_symbol(self) -> int:
r"""
The number of coded bits per symbol $k = \log_2 M$.
"""
- return self._bps
+ return self._bits_per_symbol
+
+ @property
+ def bit_rate(self) -> float:
+ r"""
+ The bit rate $f_{b}$ in bits/s.
+ """
+ return self.symbol_rate * self.bits_per_symbol
+
+ @property
+ def samples_per_symbol(self) -> int:
+ r"""
+ The number of samples per symbol $f_s / f_{sym}$.
+ """
+ return self._samples_per_symbol
+
+ @property
+ def sample_rate(self) -> float:
+ r"""
+ The sample rate $f_s$ in samples/s.
+ """
+ return self.symbol_rate * self.samples_per_symbol
@property
def index(self) -> float:
@@ -279,13 +321,6 @@ def phase_offset(self) -> float:
"""
return self._phase_offset
- @property
- def sps(self) -> int:
- r"""
- The number of samples per symbol $f_s / f_{sym}$.
- """
- return self._sps
-
@property
def pulse_shape(self) -> np.ndarray:
r"""
diff --git a/src/sdr/_modulation/_linear.py b/src/sdr/_modulation/_linear.py
index 910f84c53..ba19c6e75 100644
--- a/src/sdr/_modulation/_linear.py
+++ b/src/sdr/_modulation/_linear.py
@@ -20,12 +20,18 @@ class LinearModulation:
r"""
Implements linear phase/amplitude modulation with arbitrary symbol mapping.
- Note:
- The nomenclature for variable names in linear modulators is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Group:
modulation-linear
@@ -35,7 +41,8 @@ def __init__(
self,
symbol_map: npt.ArrayLike,
phase_offset: float = 0.0,
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
pulse_shape: npt.ArrayLike | Literal["rect", "rc", "srrc"] = "rect",
span: int | None = None,
alpha: float | None = None,
@@ -48,10 +55,11 @@ def __init__(
are decimal symbols $s[k]$ and whose values are complex symbols $a[k]$, where $M$ is the
modulation order.
phase_offset: A phase offset $\phi$ in degrees to apply to `symbol_map`.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
pulse_shape: The pulse shape $h[n]$ of the modulated signal.
- - `npt.ArrayLike`: A custom pulse shape. It is important that `sps` matches the design
+ - `npt.ArrayLike`: A custom pulse shape. It is important that `samples_per_symbol` matches the design
of the pulse shape. See :ref:`pulse-shaping-functions`.
- `"rect"`: Rectangular pulse shape.
- `"rc"`: Raised cosine pulse shape.
@@ -71,35 +79,41 @@ def __init__(
raise ValueError(f"Argument 'symbol_map' must have a size that is a power of 2, not {symbol_map.size}.")
self._symbol_map = symbol_map # Decimal-to-complex symbol map
self._order = symbol_map.size # Modulation order
- self._bps = int(np.log2(self._order)) # Coded bits per symbol
+ self._bits_per_symbol = int(np.log2(self._order)) # Coded bits per symbol
if not isinstance(phase_offset, (int, float)):
raise TypeError(f"Argument 'phase_offset' must be a number, not {type(phase_offset)}.")
self._phase_offset = phase_offset # Phase offset in degrees
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps > 1:
- raise ValueError(f"Argument 'sps' must be greater than 1, not {sps}.")
- self._sps = sps # Samples per symbol
+ if not isinstance(symbol_rate, (int, float)):
+ raise TypeError(f"Argument 'symbol_rate' must be a number, not {type(symbol_rate)}.")
+ if not symbol_rate > 0:
+ raise ValueError(f"Argument 'symbol_rate' must be positive, not {symbol_rate}.")
+ self._symbol_rate = symbol_rate # symbols/s
+
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol > 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be greater than 1, not {samples_per_symbol}.")
+ self._samples_per_symbol = samples_per_symbol # Samples per symbol
if isinstance(pulse_shape, str):
if pulse_shape == "rect":
if span is None:
span = 1
- self._pulse_shape = rectangular(self.sps, span=span)
+ self._pulse_shape = rectangular(self.samples_per_symbol, span=span)
elif pulse_shape == "rc":
if span is None:
span = 10
if alpha is None:
alpha = 0.2
- self._pulse_shape = raised_cosine(alpha, span, self.sps)
+ self._pulse_shape = raised_cosine(alpha, span, self.samples_per_symbol)
elif pulse_shape == "srrc":
if span is None:
span = 10
if alpha is None:
alpha = 0.2
- self._pulse_shape = root_raised_cosine(alpha, span, self.sps)
+ self._pulse_shape = root_raised_cosine(alpha, span, self.samples_per_symbol)
else:
raise ValueError(f"Argument 'pulse_shape' must be 'rect', 'rc', or 'srrc', not {pulse_shape!r}.")
else:
@@ -108,8 +122,8 @@ def __init__(
raise ValueError(f"Argument 'pulse_shape' must be 1-D, not {pulse_shape.ndim}-D.")
self._pulse_shape = pulse_shape # Pulse shape
- self._tx_filter = Interpolator(self.sps, self.pulse_shape) # Transmit pulse shaping filter
- self._rx_filter = Decimator(self.sps, self.pulse_shape[::-1].conj()) # Receive matched filter
+ self._tx_filter = Interpolator(self.samples_per_symbol, self.pulse_shape) # Transmit pulse shaping filter
+ self._rx_filter = Decimator(self.samples_per_symbol, self.pulse_shape[::-1].conj()) # Receive matched filter
def __repr__(self) -> str:
return f"sdr.{type(self).__name__}({self.symbol_map.tolist()}, phase_offset={self.phase_offset})"
@@ -171,8 +185,8 @@ def modulate(self, s: npt.ArrayLike) -> npt.NDArray[np.complex128]:
s: The decimal symbols $s[k]$ to modulate, $0$ to $M-1$.
Returns:
- The pulse-shaped complex samples $x[n]$ with :obj:`sps` samples per symbol
- and length `sps * s.size + pulse_shape.size - 1`.
+ The pulse-shaped complex samples $x[n]$ with :obj:`samples_per_symbol` samples per symbol
+ and length `samples_per_symbol * s.size + pulse_shape.size - 1`.
"""
s = np.asarray(s) # Decimal symbols
return self._modulate(s)
@@ -195,8 +209,8 @@ def demodulate(
This method uses matched filtering and maximum-likelihood estimation.
Arguments:
- x_tilde: The received pulse-shaped complex samples $\tilde{x}[n]$ to demodulate, with :obj:`sps`
- samples per symbol and length `sps * s_hat.size + pulse_shape.size - 1`.
+ x_tilde: The received pulse-shaped complex samples $\tilde{x}[n]$ to demodulate, with :obj:`samples_per_symbol`
+ samples per symbol and length `samples_per_symbol * s_hat.size + pulse_shape.size - 1`.
Returns:
- The decimal symbol decisions $\hat{s}[k]$, $0$ to $M-1$.
@@ -214,17 +228,17 @@ def _demodulate(
return s_hat, a_tilde, a_hat
def _rx_matched_filter(self, x_tilde: npt.NDArray[np.complex128]) -> npt.NDArray[np.complex128]:
- if self.pulse_shape.size % self.sps == 0:
+ if self.pulse_shape.size % self.samples_per_symbol == 0:
x_tilde = np.insert(x_tilde, 0, 0)
a_tilde = self._rx_filter(x_tilde, mode="full") # Complex symbols
- span = self.pulse_shape.size // self.sps
+ span = self.pulse_shape.size // self.samples_per_symbol
if span == 1:
- N_symbols = x_tilde.size // self.sps
+ N_symbols = x_tilde.size // self.samples_per_symbol
offset = span
else:
- N_symbols = x_tilde.size // self.sps - span
+ N_symbols = x_tilde.size // self.samples_per_symbol - span
offset = span
# Select the symbol decisions from the output of the decimating filter
@@ -272,11 +286,39 @@ def order(self) -> int:
return self._order
@property
- def bps(self) -> int:
+ def symbol_rate(self) -> float:
+ r"""
+ The symbol rate $f_{sym}$ in symbols/s.
+ """
+ return self._symbol_rate
+
+ @property
+ def bits_per_symbol(self) -> int:
r"""
The number of coded bits per symbol $k = \log_2 M$.
"""
- return self._bps
+ return self._bits_per_symbol
+
+ @property
+ def bit_rate(self) -> float:
+ r"""
+ The bit rate $f_{b}$ in bits/s.
+ """
+ return self.symbol_rate * self.bits_per_symbol
+
+ @property
+ def samples_per_symbol(self) -> int:
+ r"""
+ The number of samples per symbol $f_s / f_{sym}$.
+ """
+ return self._samples_per_symbol
+
+ @property
+ def sample_rate(self) -> float:
+ r"""
+ The sample rate $f_s$ in samples/s.
+ """
+ return self.symbol_rate * self.samples_per_symbol
@property
def phase_offset(self) -> float:
@@ -294,13 +336,6 @@ def symbol_map(self) -> npt.NDArray[np.complex128]:
"""
return self._symbol_map
- @property
- def sps(self) -> int:
- r"""
- The number of samples per symbol $f_s / f_{sym}$.
- """
- return self._sps
-
@property
def pulse_shape(self) -> npt.NDArray[np.float64]:
r"""
diff --git a/src/sdr/_modulation/_msk.py b/src/sdr/_modulation/_msk.py
index 097c37ee5..e5167e704 100644
--- a/src/sdr/_modulation/_msk.py
+++ b/src/sdr/_modulation/_msk.py
@@ -25,12 +25,18 @@ class MSK(OQPSK):
MSK can also be consider as continuous-phase frequency-shift keying (CPFSK) with the frequency separation
equaling half the bit period.
- Note:
- The nomenclature for variable names in linear modulators is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Examples:
Create a MSK modem.
@@ -48,7 +54,7 @@ class MSK(OQPSK):
.. ipython:: python
bits = np.random.randint(0, 2, 1000); bits[0:8]
- symbols = sdr.pack(bits, msk.bps); symbols[0:4]
+ symbols = sdr.pack(bits, msk.bits_per_symbol); symbols[0:4]
complex_symbols = msk.map_symbols(symbols); complex_symbols[0:4]
@savefig sdr_MSK_2.png
@@ -63,7 +69,7 @@ class MSK(OQPSK):
@savefig sdr_MSK_3.png
plt.figure(); \
- sdr.plot.time_domain(tx_samples[0:50*msk.sps]);
+ sdr.plot.time_domain(tx_samples[0:50*msk.samples_per_symbol]);
MSK, like OQPSK, has I and Q channels that are offset by half a symbol period.
@@ -71,7 +77,7 @@ class MSK(OQPSK):
@savefig sdr_MSK_4.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[5*msk.sps : -5*msk.sps], msk.sps); \
+ sdr.plot.eye(tx_samples[5*msk.samples_per_symbol : -5*msk.samples_per_symbol], msk.samples_per_symbol); \
plt.suptitle("Noiseless transmitted signal");
The phase trajectory of MSK is linear and continuous. Although, it should be noted that the phase is not
@@ -82,19 +88,19 @@ class MSK(OQPSK):
@savefig sdr_MSK_5.png
plt.figure(); \
- sdr.plot.phase_tree(tx_samples[msk.sps:], msk.sps);
+ sdr.plot.phase_tree(tx_samples[msk.samples_per_symbol:], msk.samples_per_symbol);
Add AWGN noise such that $E_b/N_0 = 30$ dB.
.. ipython:: python
ebn0 = 30; \
- snr = sdr.ebn0_to_snr(ebn0, bps=msk.bps, sps=msk.sps); \
+ snr = sdr.ebn0_to_snr(ebn0, bits_per_symbol=msk.bits_per_symbol, samples_per_symbol=msk.samples_per_symbol); \
rx_samples = sdr.awgn(tx_samples, snr=snr)
@savefig sdr_MSK_6.png
plt.figure(); \
- sdr.plot.time_domain(rx_samples[0:50*msk.sps]);
+ sdr.plot.time_domain(rx_samples[0:50*msk.samples_per_symbol]);
Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal.
@@ -105,7 +111,7 @@ class MSK(OQPSK):
@savefig sdr_MSK_7.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(mf_samples[10*msk.sps : -10*msk.sps], msk.sps); \
+ sdr.plot.eye(mf_samples[10*msk.samples_per_symbol : -10*msk.samples_per_symbol], msk.samples_per_symbol); \
plt.suptitle("Noisy received and matched filtered signal");
Matched filter and demodulate. Note, the first symbol has $Q = 0$ and the last symbol has $I = 0$.
@@ -131,7 +137,8 @@ def __init__(
self,
phase_offset: float = 45,
symbol_labels: Literal["bin", "gray"] | npt.ArrayLike = "gray",
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
):
r"""
Creates a new MSK object.
@@ -146,19 +153,21 @@ def __init__(
the new symbol labels. The default symbol labels are $0$ to $4-1$ for phases starting at $1 + 0j$
and going counter-clockwise around the unit circle.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
See Also:
sdr.half_sine
"""
- pulse_shape = half_sine(sps)
+ pulse_shape = half_sine(samples_per_symbol)
super().__init__(
phase_offset=phase_offset,
symbol_labels=symbol_labels,
- sps=sps,
+ symbol_rate=symbol_rate,
+ samples_per_symbol=samples_per_symbol,
pulse_shape=pulse_shape,
)
- if sps > 1 and sps % 2 != 0:
- raise ValueError(f"Argument 'sps' must be even, not {sps}.")
+ if samples_per_symbol > 1 and samples_per_symbol % 2 != 0:
+ raise ValueError(f"Argument 'samples_per_symbol' must be even, not {samples_per_symbol}.")
diff --git a/src/sdr/_modulation/_psk.py b/src/sdr/_modulation/_psk.py
index 3977b5bdb..bdab258b3 100644
--- a/src/sdr/_modulation/_psk.py
+++ b/src/sdr/_modulation/_psk.py
@@ -32,12 +32,18 @@ class PSK(LinearModulation):
$$a[k] = \exp \left[ j\left(\frac{2\pi}{M}s[k] + \phi\right) \right] .$$
- Note:
- The nomenclature for variable names in linear modulators is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Examples:
Create a QPSK modem whose constellation has a 45° phase offset.
@@ -55,7 +61,7 @@ class PSK(LinearModulation):
.. ipython:: python
bits = np.random.randint(0, 2, 1000); bits[0:8]
- symbols = sdr.pack(bits, qpsk.bps); symbols[0:4]
+ symbols = sdr.pack(bits, qpsk.bits_per_symbol); symbols[0:4]
complex_symbols = qpsk.map_symbols(symbols); complex_symbols[0:4]
@savefig sdr_PSK_2.png
@@ -70,7 +76,7 @@ class PSK(LinearModulation):
@savefig sdr_PSK_3.png
plt.figure(); \
- sdr.plot.time_domain(tx_samples[0:50*qpsk.sps]);
+ sdr.plot.time_domain(tx_samples[0:50*qpsk.samples_per_symbol]);
Examine the eye diagram of the pulse-shaped transmitted signal. The SRRC pulse shape is not a Nyquist filter,
so ISI is present.
@@ -79,7 +85,7 @@ class PSK(LinearModulation):
@savefig sdr_PSK_4.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[5*qpsk.sps : -5*qpsk.sps], qpsk.sps, persistence=True); \
+ sdr.plot.eye(tx_samples[5*qpsk.samples_per_symbol : -5*qpsk.samples_per_symbol], qpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noiseless transmitted signal with ISI");
Add AWGN noise such that $E_b/N_0 = 30$ dB.
@@ -87,12 +93,12 @@ class PSK(LinearModulation):
.. ipython:: python
ebn0 = 30; \
- snr = sdr.ebn0_to_snr(ebn0, bps=qpsk.bps, sps=qpsk.sps); \
+ snr = sdr.ebn0_to_snr(ebn0, bits_per_symbol=qpsk.bits_per_symbol, samples_per_symbol=qpsk.samples_per_symbol); \
rx_samples = sdr.awgn(tx_samples, snr=snr)
@savefig sdr_PSK_5.png
plt.figure(); \
- sdr.plot.time_domain(rx_samples[0:50*qpsk.sps]);
+ sdr.plot.time_domain(rx_samples[0:50*qpsk.samples_per_symbol]);
Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal. The
two cascaded SRRC filters create a Nyquist RC filter. Therefore, the ISI is removed.
@@ -104,7 +110,7 @@ class PSK(LinearModulation):
@savefig sdr_PSK_6.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(mf_samples[10*qpsk.sps : -10*qpsk.sps], qpsk.sps, persistence=True); \
+ sdr.plot.eye(mf_samples[10*qpsk.samples_per_symbol : -10*qpsk.samples_per_symbol], qpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noisy received and matched filtered signal without ISI");
Matched filter and demodulate.
@@ -131,7 +137,8 @@ def __init__(
order: int,
phase_offset: float = 0.0,
symbol_labels: Literal["bin", "gray"] | npt.ArrayLike = "gray",
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
pulse_shape: npt.ArrayLike | Literal["rect", "rc", "srrc"] = "rect",
span: int | None = None,
alpha: float | None = None,
@@ -150,10 +157,11 @@ def __init__(
the new symbol labels. The default symbol labels are $0$ to $M-1$ for phases starting at $1 + 0j$
and going counter-clockwise around the unit circle.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
pulse_shape: The pulse shape $h[n]$ of the modulated signal.
- - `npt.ArrayLike`: A custom pulse shape. It is important that `sps` matches the design
+ - `npt.ArrayLike`: A custom pulse shape. It is important that `samples_per_symbol` matches the design
of the pulse shape. See :ref:`pulse-shaping-functions`.
- `"rect"`: Rectangular pulse shape.
- `"rc"`: Raised cosine pulse shape.
@@ -172,17 +180,18 @@ def __init__(
super().__init__(
base_symbol_map,
phase_offset=phase_offset,
- sps=sps,
+ symbol_rate=symbol_rate,
+ samples_per_symbol=samples_per_symbol,
pulse_shape=pulse_shape,
span=span,
alpha=alpha,
)
if symbol_labels == "bin":
- self._symbol_labels = binary_code(self.bps)
+ self._symbol_labels = binary_code(self.bits_per_symbol)
self._symbol_labels_str = "bin"
elif symbol_labels == "gray":
- self._symbol_labels = gray_code(self.bps)
+ self._symbol_labels = gray_code(self.bits_per_symbol)
self._symbol_labels_str = "gray"
else:
if not np.array_equal(np.sort(symbol_labels), np.arange(self.order)):
@@ -259,7 +268,7 @@ def ber(self, ebn0: npt.ArrayLike, diff_encoded: bool = False) -> npt.NDArray[np
plt.title("BER curves for PSK and DE-PSK modulation in an AWGN channel");
"""
M = self.order
- k = self.bps
+ k = self.bits_per_symbol
ebn0 = np.asarray(ebn0)
ebn0_linear = linear(ebn0)
esn0 = ebn0_to_esn0(ebn0, k)
@@ -346,7 +355,7 @@ def ser(self, esn0: npt.ArrayLike, diff_encoded: bool = False) -> npt.NDArray[np
plt.title("SER curves for PSK and DE-PSK modulation in an AWGN channel");
"""
M = self.order
- k = self.bps
+ k = self.bits_per_symbol
esn0 = np.asarray(esn0)
esn0_linear = linear(esn0)
ebn0 = esn0_to_ebn0(esn0, k)
@@ -480,12 +489,18 @@ class PiMPSK(PSK):
\end{cases}
$$
- Note:
- The nomenclature for variable names in linear modulators is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Examples:
Create a $\pi/4$ QPSK modem.
@@ -503,7 +518,7 @@ class PiMPSK(PSK):
.. ipython:: python
bits = np.random.randint(0, 2, 1000); bits[0:8]
- symbols = sdr.pack(bits, pi4_qpsk.bps); symbols[0:4]
+ symbols = sdr.pack(bits, pi4_qpsk.bits_per_symbol); symbols[0:4]
complex_symbols = pi4_qpsk.map_symbols(symbols); complex_symbols[0:4]
@savefig sdr_PiMPSK_2.png
@@ -518,7 +533,7 @@ class PiMPSK(PSK):
@savefig sdr_PiMPSK_3.png
plt.figure(); \
- sdr.plot.time_domain(tx_samples[0:50*pi4_qpsk.sps]);
+ sdr.plot.time_domain(tx_samples[0:50*pi4_qpsk.samples_per_symbol]);
Examine the eye diagram of the pulse-shaped transmitted signal. The SRRC pulse shape is not a Nyquist filter,
so ISI is present.
@@ -527,7 +542,7 @@ class PiMPSK(PSK):
@savefig sdr_PiMPSK_4.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[5*pi4_qpsk.sps : -5*pi4_qpsk.sps], pi4_qpsk.sps, persistence=True); \
+ sdr.plot.eye(tx_samples[5*pi4_qpsk.samples_per_symbol : -5*pi4_qpsk.samples_per_symbol], pi4_qpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noiseless transmitted signal with ISI");
Add AWGN noise such that $E_b/N_0 = 30$ dB.
@@ -535,12 +550,12 @@ class PiMPSK(PSK):
.. ipython:: python
ebn0 = 30; \
- snr = sdr.ebn0_to_snr(ebn0, bps=pi4_qpsk.bps, sps=pi4_qpsk.sps); \
+ snr = sdr.ebn0_to_snr(ebn0, bits_per_symbol=pi4_qpsk.bits_per_symbol, samples_per_symbol=pi4_qpsk.samples_per_symbol); \
rx_samples = sdr.awgn(tx_samples, snr=snr)
@savefig sdr_PiMPSK_5.png
plt.figure(); \
- sdr.plot.time_domain(rx_samples[0:50*pi4_qpsk.sps]);
+ sdr.plot.time_domain(rx_samples[0:50*pi4_qpsk.samples_per_symbol]);
Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal. The
two cascaded SRRC filters create a Nyquist RC filter. Therefore, the ISI is removed.
@@ -552,7 +567,7 @@ class PiMPSK(PSK):
@savefig sdr_PiMPSK_6.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(mf_samples[10*pi4_qpsk.sps : -10*pi4_qpsk.sps], pi4_qpsk.sps, persistence=True); \
+ sdr.plot.eye(mf_samples[10*pi4_qpsk.samples_per_symbol : -10*pi4_qpsk.samples_per_symbol], pi4_qpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noisy received and matched filtered signal without ISI");
Matched filter and demodulate.
@@ -579,7 +594,8 @@ def __init__(
order: int,
phase_offset: float = 0.0,
symbol_labels: Literal["bin", "gray"] | npt.ArrayLike = "gray",
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
pulse_shape: npt.ArrayLike | Literal["rect", "rc", "srrc"] = "rect",
span: int | None = None,
alpha: float | None = None,
@@ -598,10 +614,11 @@ def __init__(
the new symbol labels. The default symbol labels are $0$ to $M-1$ for phases starting at $1 + 0j$
and going counter-clockwise around the unit circle.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
pulse_shape: The pulse shape $h[n]$ of the modulated signal.
- - `npt.ArrayLike`: A custom pulse shape. It is important that `sps` matches the design
+ - `npt.ArrayLike`: A custom pulse shape. It is important that `samples_per_symbol` matches the design
of the pulse shape. See :ref:`pulse-shaping-functions`.
- `"rect"`: Rectangular pulse shape.
- `"rc"`: Raised cosine pulse shape.
@@ -618,7 +635,8 @@ def __init__(
order,
phase_offset=phase_offset,
symbol_labels=symbol_labels,
- sps=sps,
+ symbol_rate=symbol_rate,
+ samples_per_symbol=samples_per_symbol,
pulse_shape=pulse_shape,
)
@@ -667,12 +685,18 @@ class OQPSK(PSK):
\end{align}
$$
- Note:
- The nomenclature for variable names in linear modulators is as follows: $s[k]$ are decimal symbols,
- $\hat{s}[k]$ are decimal symbol decisions, $a[k]$ are complex symbols, $\tilde{a}[k]$ are received complex
- symbols, $\hat{a}[k]$ are complex symbol decisions, $x[n]$ are pulse-shaped complex samples, and
- $\tilde{x}[n]$ are received pulse-shaped complex samples. $k$ indicates a symbol index and $n$ indicates a
- sample index.
+ .. nomenclature::
+ :collapsible:
+
+ - $k$: Symbol index
+ - $n$: Sample index
+ - $s[k]$: Decimal symbols
+ - $a[k]$ Complex symbols
+ - $x[n]$: Pulse-shaped complex samples
+ - $\tilde{x}[n]$: Received (noisy) pulse-shaped complex samples
+ - $\tilde{a}[k]$: Received (noisy) complex symbols
+ - $\hat{a}[k]$: Complex symbol decisions
+ - $\hat{s}[k]$: Decimal symbol decisions
Examples:
Create a OQPSK modem.
@@ -690,7 +714,7 @@ class OQPSK(PSK):
.. ipython:: python
bits = np.random.randint(0, 2, 1000); bits[0:8]
- symbols = sdr.pack(bits, oqpsk.bps); symbols[0:4]
+ symbols = sdr.pack(bits, oqpsk.bits_per_symbol); symbols[0:4]
complex_symbols = oqpsk.map_symbols(symbols); complex_symbols[0:4]
@savefig sdr_OQPSK_2.png
@@ -705,7 +729,7 @@ class OQPSK(PSK):
@savefig sdr_OQPSK_3.png
plt.figure(); \
- sdr.plot.time_domain(tx_samples[0:50*oqpsk.sps]);
+ sdr.plot.time_domain(tx_samples[0:50*oqpsk.samples_per_symbol]);
Examine the eye diagram of the pulse-shaped transmitted signal. The SRRC pulse shape is not a Nyquist filter,
so ISI is present.
@@ -714,7 +738,7 @@ class OQPSK(PSK):
@savefig sdr_OQPSK_4.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[5*oqpsk.sps : -5*oqpsk.sps], oqpsk.sps, persistence=True); \
+ sdr.plot.eye(tx_samples[5*oqpsk.samples_per_symbol : -5*oqpsk.samples_per_symbol], oqpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noiseless transmitted signal with ISI");
Add AWGN noise such that $E_b/N_0 = 30$ dB.
@@ -722,12 +746,12 @@ class OQPSK(PSK):
.. ipython:: python
ebn0 = 30; \
- snr = sdr.ebn0_to_snr(ebn0, bps=oqpsk.bps, sps=oqpsk.sps); \
+ snr = sdr.ebn0_to_snr(ebn0, bits_per_symbol=oqpsk.bits_per_symbol, samples_per_symbol=oqpsk.samples_per_symbol); \
rx_samples = sdr.awgn(tx_samples, snr=snr)
@savefig sdr_OQPSK_5.png
plt.figure(); \
- sdr.plot.time_domain(rx_samples[0:50*oqpsk.sps]);
+ sdr.plot.time_domain(rx_samples[0:50*oqpsk.samples_per_symbol]);
Manually apply a matched filter. Examine the eye diagram of the matched filtered received signal. The
two cascaded SRRC filters create a Nyquist RC filter. Therefore, the ISI is removed.
@@ -739,7 +763,7 @@ class OQPSK(PSK):
@savefig sdr_OQPSK_6.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(mf_samples[10*oqpsk.sps : -10*oqpsk.sps], oqpsk.sps, persistence=True); \
+ sdr.plot.eye(mf_samples[10*oqpsk.samples_per_symbol : -10*oqpsk.samples_per_symbol], oqpsk.samples_per_symbol, persistence=True); \
plt.suptitle("Noisy received and matched filtered signal without ISI");
Matched filter and demodulate. Note, the first symbol has $Q = 0$ and the last symbol has $I = 0$.
@@ -765,7 +789,8 @@ def __init__(
self,
phase_offset: float = 45,
symbol_labels: Literal["bin", "gray"] | npt.ArrayLike = "gray",
- sps: int = 8,
+ symbol_rate: float = 1.0,
+ samples_per_symbol: int = 8,
pulse_shape: npt.ArrayLike | Literal["rect", "rc", "srrc"] = "rect",
span: int | None = None,
alpha: float | None = None,
@@ -783,10 +808,11 @@ def __init__(
the new symbol labels. The default symbol labels are $0$ to $4-1$ for phases starting at $1 + 0j$
and going counter-clockwise around the unit circle.
- sps: The number of samples per symbol $f_s / f_{sym}$.
+ symbol_rate: The symbol rate $f_{sym}$ in symbols/s.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
pulse_shape: The pulse shape $h[n]$ of the modulated signal.
- - `npt.ArrayLike`: A custom pulse shape. It is important that `sps` matches the design
+ - `npt.ArrayLike`: A custom pulse shape. It is important that `samples_per_symbol` matches the design
of the pulse shape. See :ref:`pulse-shaping-functions`.
- `"rect"`: Rectangular pulse shape.
- `"rc"`: Raised cosine pulse shape.
@@ -803,14 +829,15 @@ def __init__(
4,
phase_offset=phase_offset,
symbol_labels=symbol_labels,
- sps=sps,
+ symbol_rate=symbol_rate,
+ samples_per_symbol=samples_per_symbol,
pulse_shape=pulse_shape,
span=span,
alpha=alpha,
)
- if sps > 1 and sps % 2 != 0:
- raise ValueError(f"Argument 'sps' must be even, not {sps}.")
+ if samples_per_symbol > 1 and samples_per_symbol % 2 != 0:
+ raise ValueError(f"Argument 'samples_per_symbol' must be even, not {samples_per_symbol}.")
def __repr__(self) -> str:
return f"sdr.{type(self).__name__}(phase_offset={self.phase_offset}, symbol_labels={self._symbol_labels_str!r})"
@@ -848,8 +875,8 @@ def _tx_pulse_shape(self, a: npt.NDArray[np.complex128]) -> npt.NDArray[np.compl
x_Q = self._tx_filter(a_Q, mode="full") # Complex samples
# Shift Q symbols by 1/2 symbol
- x_I = np.append(x_I, np.zeros(self.sps // 2))
- x_Q = np.insert(x_Q, 0, np.zeros(self.sps // 2))
+ x_I = np.append(x_I, np.zeros(self.samples_per_symbol // 2))
+ x_Q = np.insert(x_Q, 0, np.zeros(self.samples_per_symbol // 2))
x = x_I + 1j * x_Q
@@ -873,8 +900,8 @@ def _rx_matched_filter(self, x_tilde: npt.NDArray[np.complex128]) -> npt.NDArray
x_tilde_I, x_tilde_Q = x_tilde.real, x_tilde.imag
# Shift Q samples by -1/2 symbol
- x_tilde_I = x_tilde_I[: -self.sps // 2]
- x_tilde_Q = x_tilde_Q[self.sps // 2 :]
+ x_tilde_I = x_tilde_I[: -self.samples_per_symbol // 2]
+ x_tilde_Q = x_tilde_Q[self.samples_per_symbol // 2 :]
a_tilde_I = super()._rx_matched_filter(x_tilde_I) # Complex samples
a_tilde_Q = super()._rx_matched_filter(x_tilde_Q) # Complex samples
diff --git a/src/sdr/_modulation/_pulse_shapes.py b/src/sdr/_modulation/_pulse_shapes.py
index f6bbd4ebd..d06722bc7 100644
--- a/src/sdr/_modulation/_pulse_shapes.py
+++ b/src/sdr/_modulation/_pulse_shapes.py
@@ -13,7 +13,7 @@
@export
def rectangular(
- sps: int,
+ samples_per_symbol: int,
span: int = 1,
norm: Literal["power", "energy", "passband"] = "energy",
) -> npt.NDArray[np.float64]:
@@ -21,9 +21,9 @@ def rectangular(
Returns a rectangular pulse shape.
Arguments:
- sps: The number of samples per symbol.
- span: The length of the filter in symbols. The length of the filter is `span * sps` samples,
- but only the center `sps` samples are non-zero. The only reason for `span` to be larger than 1 is to
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
+ span: The length of the filter in symbols. The length of the filter is `span * samples_per_symbol` samples,
+ but only the center `samples_per_symbol` samples are non-zero. The only reason for `span` to be larger than 1 is to
add delay to the filter.
norm: Indicates how to normalize the pulse shape.
@@ -52,20 +52,20 @@ def rectangular(
Group:
modulation-pulse-shaping
"""
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps >= 1:
- raise ValueError(f"Argument 'sps' must be at least 1, not {sps}.")
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol >= 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be at least 1, not {samples_per_symbol}.")
if not isinstance(span, int):
raise TypeError(f"Argument 'span' must be an integer, not {type(span)}.")
if not span >= 1:
raise ValueError(f"Argument 'span' must be at least 1, not {span}.")
- length = span * sps
+ length = span * samples_per_symbol
h = np.zeros(length, dtype=float)
- idx = (length - sps) // 2
- h[idx : idx + sps] = 1
+ idx = (length - samples_per_symbol) // 2
+ h[idx : idx + samples_per_symbol] = 1
h = _normalize(h, norm)
@@ -74,7 +74,7 @@ def rectangular(
@export
def half_sine(
- sps: int,
+ samples_per_symbol: int,
span: int = 1,
norm: Literal["power", "energy", "passband"] = "energy",
) -> npt.NDArray[np.float64]:
@@ -82,9 +82,9 @@ def half_sine(
Returns a half-sine pulse shape.
Arguments:
- sps: The number of samples per symbol.
- span: The length of the filter in symbols. The length of the filter is `span * sps` samples,
- but only the center `sps` samples are non-zero. The only reason for `span` to be larger than 1 is to
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
+ span: The length of the filter in symbols. The length of the filter is `span * samples_per_symbol` samples,
+ but only the center `samples_per_symbol` samples are non-zero. The only reason for `span` to be larger than 1 is to
add delay to the filter.
norm: Indicates how to normalize the pulse shape.
@@ -113,20 +113,20 @@ def half_sine(
Group:
modulation-pulse-shaping
"""
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps >= 1:
- raise ValueError(f"Argument 'sps' must be at least 1, not {sps}.")
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol >= 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be at least 1, not {samples_per_symbol}.")
if not isinstance(span, int):
raise TypeError(f"Argument 'span' must be an integer, not {type(span)}.")
if not span >= 1:
raise ValueError(f"Argument 'span' must be at least 1, not {span}.")
- length = span * sps
+ length = span * samples_per_symbol
h = np.zeros(length, dtype=float)
- idx = (length - sps) // 2
- h[idx : idx + sps] = np.sin(np.pi * np.arange(sps) / sps)
+ idx = (length - samples_per_symbol) // 2
+ h[idx : idx + samples_per_symbol] = np.sin(np.pi * np.arange(samples_per_symbol) / samples_per_symbol)
h = _normalize(h, norm)
@@ -137,7 +137,7 @@ def half_sine(
def gaussian(
time_bandwidth: float,
span: int,
- sps: int,
+ samples_per_symbol: int,
norm: Literal["power", "energy", "passband"] = "passband",
) -> npt.NDArray[np.float64]:
r"""
@@ -148,9 +148,9 @@ def gaussian(
3-dB bandwidth in Hz and $T_{sym}$ is the symbol time in seconds. The time-bandwidth product
can also be thought of as the fractional bandwidth $B / f_{sym}$. Smaller values produce
wider pulses.
- span: The length of the filter in symbols. The length of the filter is `span * sps + 1` samples.
- The filter order `span * sps` must be even.
- sps: The number of samples per symbol.
+ span: The length of the filter in symbols. The length of the filter is `span * samples_per_symbol + 1` samples.
+ The filter order `span * samples_per_symbol` must be even.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
norm: Indicates how to normalize the pulse shape.
- `"power"`: The pulse shape is normalized so that the maximum power is 1.
@@ -212,16 +212,16 @@ def gaussian(
if not span > 0:
raise ValueError(f"Argument 'span' must be positive, not {span}.")
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps > 1:
- raise ValueError(f"Argument 'sps' must be greater than 1, not {sps}.")
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol > 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be greater than 1, not {samples_per_symbol}.")
- if not span * sps % 2 == 0:
- raise ValueError("The order of the filter (span * sps) must be even.")
+ if not span * samples_per_symbol % 2 == 0:
+ raise ValueError("The order of the filter (span * samples_per_symbol) must be even.")
- t = np.arange(-(span * sps) // 2, (span * sps) // 2 + 1, dtype=float)
- t /= sps
+ t = np.arange(-(span * samples_per_symbol) // 2, (span * samples_per_symbol) // 2 + 1, dtype=float)
+ t /= samples_per_symbol
# Equation B.2
Ts = 1
@@ -239,7 +239,7 @@ def gaussian(
def raised_cosine(
alpha: float,
span: int,
- sps: int,
+ samples_per_symbol: int,
norm: Literal["power", "energy", "passband"] = "energy",
) -> npt.NDArray[np.float64]:
r"""
@@ -247,9 +247,9 @@ def raised_cosine(
Arguments:
alpha: The excess bandwidth $0 \le \alpha \le 1$ of the filter.
- span: The length of the filter in symbols. The length of the filter is `span * sps + 1` samples.
- The filter order `span * sps` must be even.
- sps: The number of samples per symbol.
+ span: The length of the filter in symbols. The length of the filter is `span * samples_per_symbol + 1` samples.
+ The filter order `span * samples_per_symbol` must be even.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
norm: Indicates how to normalize the pulse shape.
- `"power"`: The pulse shape is normalized so that the maximum power is 1.
@@ -344,16 +344,16 @@ def raised_cosine(
if not span > 0:
raise ValueError(f"Argument 'span' must be positive, not {span}.")
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps > 1:
- raise ValueError(f"Argument 'sps' must be greater than 1, not {sps}.")
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol > 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be greater than 1, not {samples_per_symbol}.")
- if not span * sps % 2 == 0:
- raise ValueError("The order of the filter (span * sps) must be even.")
+ if not span * samples_per_symbol % 2 == 0:
+ raise ValueError("The order of the filter (span * samples_per_symbol) must be even.")
- t = np.arange(-(span * sps) // 2, (span * sps) // 2 + 1, dtype=float)
- Ts = sps
+ t = np.arange(-(span * samples_per_symbol) // 2, (span * samples_per_symbol) // 2 + 1, dtype=float)
+ Ts = samples_per_symbol
# Handle special cases where the denominator is zero
t[t == 0] += 1e-16
@@ -376,7 +376,7 @@ def raised_cosine(
def root_raised_cosine(
alpha: float,
span: int,
- sps: int,
+ samples_per_symbol: int,
norm: Literal["power", "energy", "passband"] = "energy",
) -> npt.NDArray[np.float64]:
r"""
@@ -384,9 +384,9 @@ def root_raised_cosine(
Arguments:
alpha: The excess bandwidth $0 \le \alpha \le 1$ of the filter.
- span: The length of the filter in symbols. The length of the filter is `span * sps + 1` samples.
- The filter order `span * sps` must be even.
- sps: The number of samples per symbol.
+ span: The length of the filter in symbols. The length of the filter is `span * samples_per_symbol + 1` samples.
+ The filter order `span * samples_per_symbol` must be even.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
norm: Indicates how to normalize the pulse shape.
- `"power"`: The pulse shape is normalized so that the maximum power is 1.
@@ -482,16 +482,16 @@ def root_raised_cosine(
if not span > 0:
raise ValueError(f"Argument 'span' must be positive, not {span}.")
- if not isinstance(sps, int):
- raise TypeError(f"Argument 'sps' must be an integer, not {type(sps)}.")
- if not sps > 1:
- raise ValueError(f"Argument 'sps' must be greater than 1, not {sps}.")
+ if not isinstance(samples_per_symbol, int):
+ raise TypeError(f"Argument 'samples_per_symbol' must be an integer, not {type(samples_per_symbol)}.")
+ if not samples_per_symbol > 1:
+ raise ValueError(f"Argument 'samples_per_symbol' must be greater than 1, not {samples_per_symbol}.")
- if not span * sps % 2 == 0:
- raise ValueError("The order of the filter (span * sps) must be even.")
+ if not span * samples_per_symbol % 2 == 0:
+ raise ValueError("The order of the filter (span * samples_per_symbol) must be even.")
- t = np.arange(-(span * sps) // 2, (span * sps) // 2 + 1, dtype=float)
- Ts = sps # Symbol duration (in samples)
+ t = np.arange(-(span * samples_per_symbol) // 2, (span * samples_per_symbol) // 2 + 1, dtype=float)
+ Ts = samples_per_symbol # Symbol duration (in samples)
# Handle special cases where the denominator is zero
t[t == 0] += 1e-16
diff --git a/src/sdr/plot/_modulation.py b/src/sdr/plot/_modulation.py
index cfb93d62c..9a79f7c7d 100644
--- a/src/sdr/plot/_modulation.py
+++ b/src/sdr/plot/_modulation.py
@@ -235,7 +235,7 @@ def symbol_map(
@export
def eye(
x: npt.NDArray,
- sps: int,
+ samples_per_symbol: int,
span: int = 2,
sample_rate: float | None = None,
color: Literal["index"] | str = "index",
@@ -250,7 +250,7 @@ def eye(
Arguments:
x: The baseband modulated signal $x[n]$. If `x` is complex, in-phase and quadrature eye diagrams are plotted
in separate subplots.
- sps: The number of samples per symbol.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
span: The number of symbols per raster.
sample_rate: The sample rate $f_s$ of the signal in samples/s. If `None`, the x-axis will
be labeled as "Symbol".
@@ -272,13 +272,13 @@ def eye(
.. ipython:: python
psk = sdr.PSK(4, phase_offset=45, pulse_shape="srrc"); \
- sps = psk.sps; \
+ samples_per_symbol = psk.samples_per_symbol; \
s = np.random.randint(0, psk.order, 1_000); \
tx_samples = psk.modulate(s)
@savefig sdr_plot_eye_1.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[4*sps : -4*sps], sps); \
+ sdr.plot.eye(tx_samples[4*samples_per_symbol : -4*samples_per_symbol], samples_per_symbol); \
plt.suptitle("Transmitted QPSK symbols with SRRC pulse shape");
Plot the eye diagram using a persistence plot. This provides insight into the probability density
@@ -288,7 +288,7 @@ def eye(
@savefig sdr_plot_eye_2.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(tx_samples[4*sps : -4*sps], sps, persistence=True); \
+ sdr.plot.eye(tx_samples[4*samples_per_symbol : -4*samples_per_symbol], samples_per_symbol, persistence=True); \
plt.suptitle("Transmitted QPSK symbols with SRRC pulse shape");
Apply a SRRC matched filter at the receiver. The cascaded transmit and receive SRRC filters are equivalent
@@ -302,7 +302,7 @@ def eye(
@savefig sdr_plot_eye_3.png
plt.figure(figsize=(8, 6)); \
- sdr.plot.eye(rx_samples[4*sps : -4*sps], sps, persistence=True); \
+ sdr.plot.eye(rx_samples[4*samples_per_symbol : -4*samples_per_symbol], samples_per_symbol, persistence=True); \
plt.suptitle("Received and matched filtered QPSK symbols");
Group:
@@ -313,9 +313,9 @@ def eye(
def _eye(ax, xx):
raster(
xx,
- length=span * sps + 1,
- stride=sps,
- sample_rate=sample_rate if sample_rate is not None else sps,
+ length=span * samples_per_symbol + 1,
+ stride=samples_per_symbol,
+ sample_rate=sample_rate if sample_rate is not None else samples_per_symbol,
color=color,
persistence=persistence,
colorbar=colorbar,
@@ -347,7 +347,7 @@ def _eye(ax, xx):
@export
def phase_tree(
x: npt.NDArray,
- sps: int,
+ samples_per_symbol: int,
span: int = 2,
sample_rate: float | None = None,
color: Literal["index"] | str = "index",
@@ -359,7 +359,7 @@ def phase_tree(
Arguments:
x: The baseband CPM signal $x[n]$.
- sps: The number of samples per symbol.
+ samples_per_symbol: The number of samples per symbol $f_s / f_{sym}$.
span: The number of symbols per raster.
sample_rate: The sample rate $f_s$ of the signal in samples/s. If `None`, the x-axis will
be labeled as "Symbol".
@@ -381,7 +381,7 @@ def phase_tree(
@savefig sdr_plot_phase_tree_1.png
plt.figure(); \
- sdr.plot.phase_tree(x, msk.sps)
+ sdr.plot.phase_tree(x, msk.samples_per_symbol)
Group:
plot-modulation
@@ -393,8 +393,8 @@ def phase_tree(
phase = np.angle(x)
# Create a strided array of phase values
- length = sps * span + 1
- stride = sps
+ length = samples_per_symbol * span + 1
+ stride = samples_per_symbol
N_rasters = (phase.size - length) // stride + 1
phase_strided = np.lib.stride_tricks.as_strided(
phase, shape=(N_rasters, length), strides=(phase.strides[0] * stride, phase.strides[0]), writeable=False
@@ -407,7 +407,7 @@ def phase_tree(
raster(
phase_strided,
- sample_rate=sample_rate if sample_rate is not None else sps,
+ sample_rate=sample_rate if sample_rate is not None else samples_per_symbol,
color=color,
ax=ax,
**kwargs,
diff --git a/src/sdr/plot/_time_domain.py b/src/sdr/plot/_time_domain.py
index 2922b9ecd..4a0ddd319 100644
--- a/src/sdr/plot/_time_domain.py
+++ b/src/sdr/plot/_time_domain.py
@@ -76,7 +76,7 @@ def time_domain( # noqa: D417
.. ipython:: python
- qpsk = sdr.PSK(4, phase_offset=45, sps=10, pulse_shape="srrc"); \
+ qpsk = sdr.PSK(4, phase_offset=45, samples_per_symbol=10, pulse_shape="srrc"); \
pulse_shape = qpsk.pulse_shape
@savefig sdr_plot_time_domain_1.png
diff --git a/tests/conversions/test_ebn0_to_esn0.py b/tests/conversions/test_ebn0_to_esn0.py
index e43c1c90a..d0589736a 100644
--- a/tests/conversions/test_ebn0_to_esn0.py
+++ b/tests/conversions/test_ebn0_to_esn0.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'ebno', 'esno', 'BitsPerSymbol', 2, 'CodingRate', 1/3)'
"""
ebn0 = np.arange(0, 11)
- esn0 = sdr.ebn0_to_esn0(ebn0, 2, rate=1 / 3)
+ esn0 = sdr.ebn0_to_esn0(ebn0, 2, code_rate=1 / 3)
esn0_truth = np.array(
[
-1.760912590556813,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'ebno', 'esno', 'BitsPerSymbol', 3, 'CodingRate', 1/2)'
"""
ebn0 = np.arange(0, 11)
- esn0 = sdr.ebn0_to_esn0(ebn0, 3, rate=1 / 2)
+ esn0 = sdr.ebn0_to_esn0(ebn0, 3, code_rate=1 / 2)
esn0_truth = np.array(
[
1.760912590556812,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'ebno', 'esno', 'BitsPerSymbol', 4, 'CodingRate', 2/3)'
"""
ebn0 = np.arange(0, 11)
- esn0 = sdr.ebn0_to_esn0(ebn0, 4, rate=2 / 3)
+ esn0 = sdr.ebn0_to_esn0(ebn0, 4, code_rate=2 / 3)
esn0_truth = np.array(
[
4.259687322722811,
diff --git a/tests/conversions/test_ebn0_to_snr.py b/tests/conversions/test_ebn0_to_snr.py
index b6309ffc5..b5d613b47 100644
--- a/tests/conversions/test_ebn0_to_snr.py
+++ b/tests/conversions/test_ebn0_to_snr.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'ebno', 'snr', 'BitsPerSymbol', 2, 'CodingRate', 1/3, 'SamplesPerSymbol', 1)'
"""
ebn0 = np.arange(0, 11)
- snr = sdr.ebn0_to_snr(ebn0, 2, rate=1 / 3, sps=1)
+ snr = sdr.ebn0_to_snr(ebn0, 2, code_rate=1 / 3, samples_per_symbol=1)
snr_truth = np.array(
[
-1.760912590556813,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'ebno', 'snr', 'BitsPerSymbol', 3, 'CodingRate', 1/2, 'SamplesPerSymbol', 2)'
"""
ebn0 = np.arange(0, 11)
- snr = sdr.ebn0_to_snr(ebn0, 3, rate=1 / 2, sps=2)
+ snr = sdr.ebn0_to_snr(ebn0, 3, code_rate=1 / 2, samples_per_symbol=2)
snr_truth = np.array(
[
-1.249387366082999,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'ebno', 'snr', 'BitsPerSymbol', 4, 'CodingRate', 2/3, 'SamplesPerSymbol', 4)'
"""
ebn0 = np.arange(0, 11)
- snr = sdr.ebn0_to_snr(ebn0, 4, rate=2 / 3, sps=4)
+ snr = sdr.ebn0_to_snr(ebn0, 4, code_rate=2 / 3, samples_per_symbol=4)
snr_truth = np.array(
[
-1.760912590556813,
diff --git a/tests/conversions/test_esn0_to_ebn0.py b/tests/conversions/test_esn0_to_ebn0.py
index 91d87e974..23c91f836 100644
--- a/tests/conversions/test_esn0_to_ebn0.py
+++ b/tests/conversions/test_esn0_to_ebn0.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'esno', 'ebno', 'BitsPerSymbol', 2, 'CodingRate', 1/3)'
"""
esn0 = np.arange(0, 11)
- ebn0 = sdr.esn0_to_ebn0(esn0, 2, rate=1 / 3)
+ ebn0 = sdr.esn0_to_ebn0(esn0, 2, code_rate=1 / 3)
ebn0_truth = np.array(
[
1.760912590556813,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'esno', 'ebno', 'BitsPerSymbol', 3, 'CodingRate', 1/2)'
"""
esn0 = np.arange(0, 11)
- ebn0 = sdr.esn0_to_ebn0(esn0, 3, rate=1 / 2)
+ ebn0 = sdr.esn0_to_ebn0(esn0, 3, code_rate=1 / 2)
ebn0_truth = np.array(
[
-1.760912590556812,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'esno', 'ebno', 'BitsPerSymbol', 4, 'CodingRate', 2/3)'
"""
esn0 = np.arange(0, 11)
- ebn0 = sdr.esn0_to_ebn0(esn0, 4, rate=2 / 3)
+ ebn0 = sdr.esn0_to_ebn0(esn0, 4, code_rate=2 / 3)
ebn0_truth = np.array(
[
-4.259687322722811,
diff --git a/tests/conversions/test_esn0_to_snr.py b/tests/conversions/test_esn0_to_snr.py
index bca8ac572..c5f9b8809 100644
--- a/tests/conversions/test_esn0_to_snr.py
+++ b/tests/conversions/test_esn0_to_snr.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'esno', 'snr', 'SamplesPerSymbol', 1)'
"""
esn0 = np.arange(0, 11)
- snr = sdr.esn0_to_snr(esn0, sps=1)
+ snr = sdr.esn0_to_snr(esn0, samples_per_symbol=1)
snr_truth = np.array(
[
0,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'esno', 'snr', 'SamplesPerSymbol', 2)'
"""
esn0 = np.arange(0, 11)
- snr = sdr.esn0_to_snr(esn0, sps=2)
+ snr = sdr.esn0_to_snr(esn0, samples_per_symbol=2)
snr_truth = np.array(
[
-3.010299956639812,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'esno', 'snr', 'SamplesPerSymbol', 4)'
"""
esn0 = np.arange(0, 11)
- snr = sdr.esn0_to_snr(esn0, sps=4)
+ snr = sdr.esn0_to_snr(esn0, samples_per_symbol=4)
snr_truth = np.array(
[
-6.020599913279624,
diff --git a/tests/conversions/test_snr_to_ebn0.py b/tests/conversions/test_snr_to_ebn0.py
index 8b16dd327..c9c3d50c5 100644
--- a/tests/conversions/test_snr_to_ebn0.py
+++ b/tests/conversions/test_snr_to_ebn0.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'snr', 'ebno', 'BitsPerSymbol', 2, 'CodingRate', 1/3, 'SamplesPerSymbol', 1)'
"""
snr = np.arange(0, 11)
- ebn0 = sdr.snr_to_ebn0(snr, 2, rate=1 / 3, sps=1)
+ ebn0 = sdr.snr_to_ebn0(snr, 2, code_rate=1 / 3, samples_per_symbol=1)
ebn0_truth = np.array(
[
1.760912590556812,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'snr', 'ebno', 'BitsPerSymbol', 3, 'CodingRate', 1/2, 'SamplesPerSymbol', 2)'
"""
snr = np.arange(0, 11)
- ebn0 = sdr.snr_to_ebn0(snr, 3, rate=1 / 2, sps=2)
+ ebn0 = sdr.snr_to_ebn0(snr, 3, code_rate=1 / 2, samples_per_symbol=2)
ebn0_truth = np.array(
[
1.249387366082999,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'snr', 'ebno', 'BitsPerSymbol', 4, 'CodingRate', 2/3, 'SamplesPerSymbol', 4)'
"""
snr = np.arange(0, 11)
- ebn0 = sdr.snr_to_ebn0(snr, 4, rate=2 / 3, sps=4)
+ ebn0 = sdr.snr_to_ebn0(snr, 4, code_rate=2 / 3, samples_per_symbol=4)
ebn0_truth = np.array(
[
1.760912590556812,
diff --git a/tests/conversions/test_snr_to_esn0.py b/tests/conversions/test_snr_to_esn0.py
index 3b6b258d8..56d08bcde 100644
--- a/tests/conversions/test_snr_to_esn0.py
+++ b/tests/conversions/test_snr_to_esn0.py
@@ -9,7 +9,7 @@ def test_1():
>> convertSNR(0:10, 'snr', 'esno', 'SamplesPerSymbol', 1)'
"""
snr = np.arange(0, 11)
- esn0 = sdr.snr_to_esn0(snr, sps=1)
+ esn0 = sdr.snr_to_esn0(snr, samples_per_symbol=1)
esn0_truth = np.array(
[
0,
@@ -34,7 +34,7 @@ def test_2():
>> convertSNR(0:10, 'snr', 'esno', 'SamplesPerSymbol', 2)'
"""
snr = np.arange(0, 11)
- esn0 = sdr.snr_to_esn0(snr, sps=2)
+ esn0 = sdr.snr_to_esn0(snr, samples_per_symbol=2)
esn0_truth = np.array(
[
3.010299956639812,
@@ -59,7 +59,7 @@ def test_3():
>> convertSNR(0:10, 'snr', 'esno', 'SamplesPerSymbol', 4)'
"""
snr = np.arange(0, 11)
- esn0 = sdr.snr_to_esn0(snr, sps=4)
+ esn0 = sdr.snr_to_esn0(snr, samples_per_symbol=4)
esn0_truth = np.array(
[
6.020599913279624,
diff --git a/tests/dsp/multirate/test_interpolator.py b/tests/dsp/multirate/test_interpolator.py
index eccc60200..1180b1b5e 100644
--- a/tests/dsp/multirate/test_interpolator.py
+++ b/tests/dsp/multirate/test_interpolator.py
@@ -343,11 +343,11 @@ def test_3_zoh():
def test_srrc_0p5_6():
"""
MATLAB:
- >> sps = 4;
- >> h = rcosdesign(0.5, 6, sps);
+ >> samples_per_symbol = 4;
+ >> h = rcosdesign(0.5, 6, samples_per_symbol);
>> s = randi([0 3], 10, 1);
>> x = pskmod(s, 4);
- >> fir = dsp.Interpolator(sps, h);
+ >> fir = dsp.Interpolator(samples_per_symbol, h);
>> y = fir(x);
"""
h = np.array(
@@ -451,11 +451,11 @@ def test_srrc_0p5_6():
def test_srrc_0p9_4():
"""
MATLAB:
- >> sps = 5;
- >> h = rcosdesign(0.9, 4, sps);
+ >> samples_per_symbol = 5;
+ >> h = rcosdesign(0.9, 4, samples_per_symbol);
>> s = randi([0 3], 10, 1);
>> x = pskmod(s, 4);
- >> fir = dsp.Interpolator(sps, h);
+ >> fir = dsp.Interpolator(samples_per_symbol, h);
>> y = fir(x);
"""
h = np.array(
@@ -565,11 +565,11 @@ def test_srrc_0p9_4():
def test_srrc_0p1_7():
"""
MATLAB:
- >> sps = 6;
- >> h = rcosdesign(0.1, 7, sps);
+ >> samples_per_symbol = 6;
+ >> h = rcosdesign(0.1, 7, samples_per_symbol);
>> s = randi([0 3], 10, 1);
>> x = pskmod(s, 4);
- >> fir = dsp.Interpolator(sps, h);
+ >> fir = dsp.Interpolator(samples_per_symbol, h);
>> y = fir(x);
"""
h = np.array(
diff --git a/tests/measurements/test_rms_bandwidth.py b/tests/measurements/test_rms_bandwidth.py
index e8314da18..344171b03 100644
--- a/tests/measurements/test_rms_bandwidth.py
+++ b/tests/measurements/test_rms_bandwidth.py
@@ -9,7 +9,7 @@ def test_psk_rect():
psk = sdr.PSK(2, pulse_shape="rect")
s = rng.integers(0, psk.order, 1_000)
x = psk.modulate(s)
- b_rms = sdr.rms_bandwidth(x, sample_rate=psk.sps)
+ b_rms = sdr.rms_bandwidth(x, sample_rate=psk.samples_per_symbol)
assert b_rms == pytest.approx(0.7461700620944993, rel=1e-3)
@@ -18,5 +18,5 @@ def test_psk_srrc():
psk = sdr.PSK(2, pulse_shape="srrc")
s = rng.integers(0, psk.order, 1_000)
x = psk.modulate(s)
- b_rms = sdr.rms_bandwidth(x, sample_rate=psk.sps)
+ b_rms = sdr.rms_bandwidth(x, sample_rate=psk.samples_per_symbol)
assert b_rms == pytest.approx(0.2900015177082325, rel=1e-3)
diff --git a/tests/measurements/test_rms_integration_time.py b/tests/measurements/test_rms_integration_time.py
index 8b86ea291..f37f71339 100644
--- a/tests/measurements/test_rms_integration_time.py
+++ b/tests/measurements/test_rms_integration_time.py
@@ -6,28 +6,28 @@
def test_psk_rect():
rng = np.random.default_rng(0)
- psk = sdr.PSK(2, pulse_shape="rect", sps=100)
+ psk = sdr.PSK(2, pulse_shape="rect", samples_per_symbol=100)
s = rng.integers(0, psk.order, 1_000) # Signal is 1000 seconds long
x = psk.modulate(s)
- t_rms = sdr.rms_integration_time(x, sample_rate=psk.sps)
+ t_rms = sdr.rms_integration_time(x, sample_rate=psk.samples_per_symbol)
assert t_rms == pytest.approx(288.672247843467, 1e-3)
def test_psk_srrc():
rng = np.random.default_rng(0)
- psk = sdr.PSK(2, pulse_shape="srrc", sps=100)
+ psk = sdr.PSK(2, pulse_shape="srrc", samples_per_symbol=100)
s = rng.integers(0, psk.order, 1_000) # Signal is 1000 seconds long
x = psk.modulate(s)
- t_rms = sdr.rms_integration_time(x, sample_rate=psk.sps)
+ t_rms = sdr.rms_integration_time(x, sample_rate=psk.samples_per_symbol)
assert t_rms == pytest.approx(288.66026000704875, 1e-3)
def test_psk_srrc_parabolic():
rng = np.random.default_rng(0)
- psk = sdr.PSK(2, pulse_shape="srrc", sps=100)
+ psk = sdr.PSK(2, pulse_shape="srrc", samples_per_symbol=100)
s = rng.integers(0, psk.order, 1_000) # Signal is 1000 seconds long
x = psk.modulate(s)
y = x * np.linspace(-1, 1, len(x)) ** 2 # Parabolic pulse shape
y *= np.sqrt(sdr.energy(x) / sdr.energy(y)) # Normalize energy
- t_rms = sdr.rms_integration_time(y, sample_rate=psk.sps)
+ t_rms = sdr.rms_integration_time(y, sample_rate=psk.samples_per_symbol)
assert t_rms == pytest.approx(422.61669635555705, 1e-3)
diff --git a/tests/modulation/cpm/test_modulate.py b/tests/modulation/cpm/test_modulate.py
index 702f4f5b8..1bf01c060 100644
--- a/tests/modulation/cpm/test_modulate.py
+++ b/tests/modulation/cpm/test_modulate.py
@@ -18,9 +18,9 @@ def test_rect_bin():
x = cpm(b);
x
"""
- cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rect", sps=8)
+ cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rect", samples_per_symbol=8)
b = np.array([0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1])
- s = sdr.pack(b, cpm.bps)
+ s = sdr.pack(b, cpm.bits_per_symbol)
x = cpm.modulate(s)
x_truth = np.array(
[
@@ -106,7 +106,7 @@ def test_rect_bin():
0.980785280403231 + 0.195090322016127j,
]
)
- # debug_plot(x, x_truth, cpm.sps)
+ # debug_plot(x, x_truth, cpm.samples_per_symbol)
np.testing.assert_array_almost_equal(x, x_truth)
@@ -124,9 +124,9 @@ def test_rect_gray():
x = cpm(b);
x
"""
- cpm = sdr.CPM(4, index=0.5, symbol_labels="gray", pulse_shape="rect", sps=8)
+ cpm = sdr.CPM(4, index=0.5, symbol_labels="gray", pulse_shape="rect", samples_per_symbol=8)
b = np.array([1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1])
- s = sdr.pack(b, cpm.bps)
+ s = sdr.pack(b, cpm.bits_per_symbol)
x = cpm.modulate(s)
x_truth = np.array(
[
@@ -212,7 +212,7 @@ def test_rect_gray():
0.980785280403231 + 0.195090322016127j,
]
)
- # debug_plot(x, x_truth, cpm.sps)
+ # debug_plot(x, x_truth, cpm.samples_per_symbol)
np.testing.assert_array_almost_equal(x, x_truth)
@@ -230,9 +230,9 @@ def test_rect_gray():
# x = cpm(b);
# x
# """
-# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="sine", sps=8)
+# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="sine", samples_per_symbol=8)
# b = np.array([0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1])
-# s = sdr.pack(b, cpm.bps)
+# s = sdr.pack(b, cpm.bits_per_symbol)
# x = cpm.modulate(s)
# x_truth = np.array(
# [
@@ -318,7 +318,7 @@ def test_rect_gray():
# -0.999809845283299 - 0.019500596775106j,
# ]
# )
-# # debug_plot(x, x_truth, cpm.sps)
+# # debug_plot(x, x_truth, cpm.samples_per_symbol)
# np.testing.assert_array_almost_equal(x, x_truth)
@@ -336,9 +336,9 @@ def test_rect_gray():
# x = cpm(b);
# x
# """
-# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rc", sps=8, alpha=0.2)
+# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rc", samples_per_symbol=8, alpha=0.2)
# b = np.array([1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0])
-# s = sdr.pack(b, cpm.bps)
+# s = sdr.pack(b, cpm.bits_per_symbol)
# x = cpm.modulate(s)
# x_truth = np.array(
# [
@@ -424,7 +424,7 @@ def test_rect_gray():
# -0.990230937912052 - 0.139437045299366j,
# ]
# )
-# # debug_plot(x, x_truth, cpm.sps)
+# # debug_plot(x, x_truth, cpm.samples_per_symbol)
# np.testing.assert_array_almost_equal(x, x_truth)
@@ -442,9 +442,9 @@ def test_rect_gray():
# x = cpm(b);
# x
# """
-# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="gaussian", sps=8, time_bandwidth=0.3)
+# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="gaussian", samples_per_symbol=8, time_bandwidth=0.3)
# b = np.array([0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0])
-# s = sdr.pack(b, cpm.bps)
+# s = sdr.pack(b, cpm.bits_per_symbol)
# x = cpm.modulate(s)
# x_truth = np.array(
# [
@@ -530,7 +530,7 @@ def test_rect_gray():
# -0.883523872531454 - 0.468386129883265j,
# ]
# )
-# # debug_plot(x, x_truth, cpm.sps)
+# # debug_plot(x, x_truth, cpm.samples_per_symbol)
# np.testing.assert_array_almost_equal(x, x_truth)
@@ -548,9 +548,9 @@ def test_rect_gray():
# x = cpm(b);
# x
# """
-# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rect", sps=8)
+# cpm = sdr.CPM(4, index=0.5, symbol_labels="bin", pulse_shape="rect", samples_per_symbol=8)
# b = np.array([1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1])
-# s = sdr.pack(b, cpm.bps)
+# s = sdr.pack(b, cpm.bits_per_symbol)
# x = cpm.modulate(s)
# x_truth = np.array(
# [
@@ -636,11 +636,11 @@ def test_rect_gray():
# -0.855231086688292 + 0.518246841150010j,
# ]
# )
-# # debug_plot(x, x_truth, cpm.sps)
+# # debug_plot(x, x_truth, cpm.samples_per_symbol)
# np.testing.assert_array_almost_equal(x, x_truth)
-def debug_plot(x: np.ndarray, x_truth: np.ndarray, sps: int):
+def debug_plot(x: np.ndarray, x_truth: np.ndarray, samples_per_symbol: int):
# import matplotlib.pyplot as plt
plt.figure()
@@ -648,11 +648,11 @@ def debug_plot(x: np.ndarray, x_truth: np.ndarray, sps: int):
sdr.plot.time_domain(x_truth.real, label="Truth")
plt.figure()
- sdr.plot.phase_tree(x, sps, span=2)
+ sdr.plot.phase_tree(x, samples_per_symbol, span=2)
plt.title("Test")
plt.figure()
- sdr.plot.phase_tree(x_truth, sps, span=2)
+ sdr.plot.phase_tree(x_truth, samples_per_symbol, span=2)
plt.title("Truth")
plt.show()
diff --git a/tests/modulation/psk/test_demodulate.py b/tests/modulation/psk/test_demodulate.py
index f505f0184..41082991c 100644
--- a/tests/modulation/psk/test_demodulate.py
+++ b/tests/modulation/psk/test_demodulate.py
@@ -6,54 +6,54 @@
def test_bpsk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps)
- psk = sdr.PSK(2, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol)
+ psk = sdr.PSK(2, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
def test_bpsk_srrc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.root_raised_cosine(0.5, 16, sps)
- psk = sdr.PSK(2, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.root_raised_cosine(0.5, 16, samples_per_symbol)
+ psk = sdr.PSK(2, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
def test_qpsk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps)
- psk = sdr.PSK(4, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol)
+ psk = sdr.PSK(4, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
def test_qpsk_srrc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.root_raised_cosine(0.5, 16, sps)
- psk = sdr.PSK(4, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.root_raised_cosine(0.5, 16, samples_per_symbol)
+ psk = sdr.PSK(4, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
def test_8psk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps)
- psk = sdr.PSK(8, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol)
+ psk = sdr.PSK(8, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
def test_8psk_srrc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.root_raised_cosine(0.5, 16, sps)
- psk = sdr.PSK(8, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.root_raised_cosine(0.5, 16, samples_per_symbol)
+ psk = sdr.PSK(8, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_demodulation(psk)
diff --git a/tests/modulation/psk/test_modulate.py b/tests/modulation/psk/test_modulate.py
index b155a0b0f..2c9d0c884 100644
--- a/tests/modulation/psk/test_modulate.py
+++ b/tests/modulation/psk/test_modulate.py
@@ -6,54 +6,54 @@
def test_bpsk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps, norm="power")
- psk = sdr.PSK(2, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol, norm="power")
+ psk = sdr.PSK(2, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
def test_bpsk_rc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.raised_cosine(0.5, 16, sps, norm="power")
- psk = sdr.PSK(2, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.raised_cosine(0.5, 16, samples_per_symbol, norm="power")
+ psk = sdr.PSK(2, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
def test_qpsk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps, norm="power")
- psk = sdr.PSK(4, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol, norm="power")
+ psk = sdr.PSK(4, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
def test_qpsk_rc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.raised_cosine(0.5, 16, sps, norm="power")
- psk = sdr.PSK(4, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.raised_cosine(0.5, 16, samples_per_symbol, norm="power")
+ psk = sdr.PSK(4, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
def test_8psk_rect():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.rectangular(sps, norm="power")
- psk = sdr.PSK(8, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.rectangular(samples_per_symbol, norm="power")
+ psk = sdr.PSK(8, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
def test_8psk_rc():
rng = np.random.default_rng()
phi = rng.uniform(0, 360)
- sps = 10
- h = sdr.raised_cosine(0.5, 16, sps, norm="power")
- psk = sdr.PSK(8, phase_offset=phi, sps=sps, pulse_shape=h)
+ samples_per_symbol = 10
+ h = sdr.raised_cosine(0.5, 16, samples_per_symbol, norm="power")
+ psk = sdr.PSK(8, phase_offset=phi, samples_per_symbol=samples_per_symbol, pulse_shape=h)
_verify_modulation(psk)
@@ -64,7 +64,7 @@ def _verify_modulation(psk: sdr.PSK):
x = psk.modulate(s)
offset = psk.pulse_shape.size // 2
- a_hat = x[offset : offset + s.size * psk.sps : psk.sps]
+ a_hat = x[offset : offset + s.size * psk.samples_per_symbol : psk.samples_per_symbol]
# import matplotlib.pyplot as plt
diff --git a/tests/modulation/psk/test_ser.py b/tests/modulation/psk/test_ser.py
index 6a8dc51c7..004ad607d 100644
--- a/tests/modulation/psk/test_ser.py
+++ b/tests/modulation/psk/test_ser.py
@@ -16,7 +16,7 @@
def test_bpsk():
psk = sdr.PSK(2)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0)
ser_truth = np.array(
[
@@ -69,7 +69,7 @@ def test_bpsk():
def test_qpsk():
psk = sdr.PSK(4)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0)
ser_truth = np.array(
[
@@ -122,7 +122,7 @@ def test_qpsk():
def test_8psk():
psk = sdr.PSK(8)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0)
ser_truth = np.array(
[
@@ -175,7 +175,7 @@ def test_8psk():
def test_16psk():
psk = sdr.PSK(16)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0)
ser_truth = np.array(
[
diff --git a/tests/modulation/psk/test_ser_diff.py b/tests/modulation/psk/test_ser_diff.py
index 405967ad3..a95763eb1 100644
--- a/tests/modulation/psk/test_ser_diff.py
+++ b/tests/modulation/psk/test_ser_diff.py
@@ -16,7 +16,7 @@
def test_bpsk():
psk = sdr.PSK(2)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0, diff_encoded=True)
ser_truth = np.array(
[
@@ -69,7 +69,7 @@ def test_bpsk():
def test_qpsk():
psk = sdr.PSK(4)
ebn0 = np.arange(-10, 10.5, 0.5)
- esn0 = sdr.ebn0_to_esn0(ebn0, psk.bps)
+ esn0 = sdr.ebn0_to_esn0(ebn0, psk.bits_per_symbol)
ser = psk.ser(esn0, diff_encoded=True)
ser_truth = np.array(
[