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( [