Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize LaTeX equations #347

Merged
merged 2 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,208 changes: 604 additions & 604 deletions docs/examples/coherent-integration.ipynb

Large diffs are not rendered by default.

1,056 changes: 528 additions & 528 deletions docs/examples/non-coherent-integration.ipynb

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions docs/examples/peak-to-average-power.ipynb

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions docs/examples/phase-locked-loop.ipynb

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions docs/examples/psk.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/examples/pulse-shapes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
"/home/matt/repos/sdr/src/sdr/plot/_filter.py:356: RuntimeWarning: divide by zero encountered in log10\n",
"/home/matt/repos/sdr/src/sdr/plot/_filter.py:357: RuntimeWarning: divide by zero encountered in log10\n",
" H = 10 * np.log10(np.abs(H) ** 2)\n"
]
},
Expand Down
74 changes: 37 additions & 37 deletions src/sdr/_detection/_approximation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
@export
def albersheim(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike = 1) -> npt.NDArray[np.float64]:
r"""
Estimates the minimum signal-to-noise ratio (SNR) required to achieve the desired probability of detection $P_{D}$.
Estimates the minimum signal-to-noise ratio (SNR) required to achieve the desired probability of detection $P_d$.

Arguments:
p_d: The desired probability of detection $P_D$ in $(0, 1)$.
p_fa: The desired probability of false alarm $P_{FA}$ in $(0, 1)$.
n_nc: The number of non-coherent combinations $N_{NC} \ge 1$.
p_d: The desired probability of detection $P_d$ in $(0, 1)$.
p_fa: The desired probability of false alarm $P_{fa}$ in $(0, 1)$.
n_nc: The number of non-coherent combinations $N_{nc} \ge 1$.

Returns:
The minimum required single-sample SNR $\gamma$ in dB.
Expand All @@ -29,21 +29,21 @@ def albersheim(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike = 1)
Notes:
This function implements Albersheim's equation, given by

$$A = \ln \frac{0.62}{P_{FA}}$$
$$A = \ln \frac{0.62}{P_{fa}}$$

$$B = \ln \frac{P_D}{1 - P_D}$$
$$B = \ln \frac{P_d}{1 - P_d}$$

$$
\text{SNR}_{\text{dB}} =
-5 \log_{10} N_{NC} + \left(6.2 + \frac{4.54}{\sqrt{N_{NC} + 0.44}}\right)
-5 \log_{10} N_{nc} + \left(6.2 + \frac{4.54}{\sqrt{N_{nc} + 0.44}}\right)
\log_{10} \left(A + 0.12AB + 1.7B\right) .
$$

The error in the estimated minimum SNR is claimed to be less than 0.2 dB for

$$10^{-7} \leq P_{FA} \leq 10^{-3}$$
$$0.1 \leq P_D \leq 0.9$$
$$1 \le N_{NC} \le 8096 .$$
$$10^{-7} \leq P_{fa} \leq 10^{-3}$$
$$0.1 \leq P_d \leq 0.9$$
$$1 \le N_{nc} \le 8096 .$$

Albersheim's equation approximates a linear detector. However, the difference between linear and square-law
detectors in minimal, so Albersheim's equation finds wide use.
Expand Down Expand Up @@ -71,14 +71,14 @@ def albersheim(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike = 1)
plt.semilogx(p_fa, sdr.albersheim(p_d, p_fa, n_nc=10), linestyle="--"); \
plt.semilogx(p_fa, sdr.albersheim(p_d, p_fa, n_nc=20), linestyle="--"); \
plt.gca().set_prop_cycle(None); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=1, detector="linear"), label="$N_{NC}$ = 1"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=2, detector="linear"), label="$N_{NC}$ = 2"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=10, detector="linear"), label="$N_{NC}$ = 10"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=20, detector="linear"), label="$N_{NC}$ = 20"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=1, detector="linear"), label="$N_{nc}$ = 1"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=2, detector="linear"), label="$N_{nc}$ = 2"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=10, detector="linear"), label="$N_{nc}$ = 10"); \
plt.semilogx(p_fa, sdr.min_snr(p_d, p_fa, n_nc=20, detector="linear"), label="$N_{nc}$ = 20"); \
plt.legend(); \
plt.xlabel("Probability of false alarm, $P_{FA}$"); \
plt.xlabel("Probability of false alarm, $P_{fa}$"); \
plt.ylabel("Minimum required SNR (dB)"); \
plt.title("Minimum required SNR across non-coherent combinations for $P_D = 0.9$\nfrom theory (solid) and Albersheim's approximation (dashed)");
plt.title("Minimum required SNR across non-coherent combinations for $P_d = 0.9$\nfrom theory (solid) and Albersheim's approximation (dashed)");

Compare the theoretical non-coherent gain for a linear detector against the approximation from Albersheim's
equation. This comparison plots curves for various post-integration probabilities of detection.
Expand All @@ -92,18 +92,18 @@ def albersheim(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike = 1)
snr = sdr.min_snr(p_d, p_fa, detector="linear")
ax[0].semilogx(n, sdr.non_coherent_gain(n, snr, p_fa=p_fa, detector="linear", snr_ref="output"), label=p_d)
ax[0].semilogx(n, sdr.coherent_gain(n), color="k", label="Coherent"); \
ax[0].legend(title="$P_D$"); \
ax[0].set_xlabel("Number of samples, $N_{NC}$"); \
ax[0].set_ylabel("Non-coherent gain, $G_{NC}$"); \
ax[0].legend(title="$P_d$"); \
ax[0].set_xlabel("Number of samples, $N_{nc}$"); \
ax[0].set_ylabel("Non-coherent gain, $G_{nc}$"); \
ax[0].set_title("Theoretical");
for p_d in [0.5, 0.8, 0.95]:
g_nc = sdr.albersheim(p_d, p_fa, 1) - sdr.albersheim(p_d, p_fa, n)
ax[1].semilogx(n, g_nc, linestyle="--", label=p_d)
@savefig sdr_albersheim_2.png
ax[1].semilogx(n, sdr.coherent_gain(n), color="k", label="Coherent"); \
ax[1].legend(title="$P_D$"); \
ax[1].set_xlabel("Number of samples, $N_{NC}$"); \
ax[1].set_ylabel("Non-coherent gain, $G_{NC}$"); \
ax[1].legend(title="$P_d$"); \
ax[1].set_xlabel("Number of samples, $N_{nc}$"); \
ax[1].set_ylabel("Non-coherent gain, $G_{nc}$"); \
ax[1].set_title("Albersheim's approximation");

Group:
Expand Down Expand Up @@ -131,15 +131,15 @@ def albersheim(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike = 1)
@export
def peebles(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike) -> npt.NDArray[np.float64]:
r"""
Estimates the non-coherent integration gain for a given probability of detection $P_{D}$ and false alarm $P_{FA}$.
Estimates the non-coherent integration gain for a given probability of detection $P_d$ and false alarm $P_{fa}$.

Arguments:
p_d: The desired probability of detection $P_D$ in $(0, 1)$.
p_fa: The desired probability of false alarm $P_{FA}$ in $(0, 1)$.
n_nc: The number of non-coherent combinations $N_{NC} \ge 1$.
p_d: The desired probability of detection $P_d$ in $(0, 1)$.
p_fa: The desired probability of false alarm $P_{fa}$ in $(0, 1)$.
n_nc: The number of non-coherent combinations $N_{nc} \ge 1$.

Returns:
The non-coherent integration gain $G_{NC}$.
The non-coherent integration gain $G_{nc}$.

See Also:
sdr.non_coherent_gain
Expand All @@ -148,14 +148,14 @@ def peebles(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike) -> npt
This function implements Peebles' equation, given by

$$
G_{NC} = 6.79 \cdot \left(1 + 0.253 \cdot P_D\right) \cdot \left(1 + \frac{\log_{10}(1 / P_{FA})}{46.6}\right) \cdot \log_{10}(N_{NC}) \cdot \left(1 - 0.14 \cdot \log_{10}(N_{NC}) + 0.0183 \cdot (\log_{10}(n_{nc}))^2\right)
G_{nc} = 6.79 \cdot \left(1 + 0.253 \cdot P_d\right) \cdot \left(1 + \frac{\log_{10}(1 / P_{fa})}{46.6}\right) \cdot \log_{10}(N_{nc}) \cdot \left(1 - 0.14 \cdot \log_{10}(N_{nc}) + 0.0183 \cdot (\log_{10}(n_{nc}))^2\right)
$$

The error in the estimated non-coherent integration gain is claimed to be less than 0.8 dB for

$$10^{-10} \leq P_{FA} \leq 10^{-2}$$
$$0.5 \leq P_D \leq 0.999$$
$$1 \le N_{NC} \le 100 .$$
$$10^{-10} \leq P_{fa} \leq 10^{-2}$$
$$0.5 \leq P_d \leq 0.999$$
$$1 \le N_{nc} \le 100 .$$

Peebles' equation approximates the non-coherent integration gain using a square-law detector.

Expand All @@ -176,17 +176,17 @@ def peebles(p_d: npt.ArrayLike, p_fa: npt.ArrayLike, n_nc: npt.ArrayLike) -> npt
snr = sdr.min_snr(p_d, p_fa, detector="square-law")
ax[0].semilogx(n, sdr.non_coherent_gain(n, snr, p_fa=p_fa, detector="square-law", snr_ref="output"), label=p_d)
ax[0].semilogx(n, sdr.coherent_gain(n), color="k", label="Coherent"); \
ax[0].legend(title="$P_D$"); \
ax[0].set_xlabel("Number of samples, $N_{NC}$"); \
ax[0].set_ylabel("Non-coherent gain, $G_{NC}$"); \
ax[0].legend(title="$P_d$"); \
ax[0].set_xlabel("Number of samples, $N_{nc}$"); \
ax[0].set_ylabel("Non-coherent gain, $G_{nc}$"); \
ax[0].set_title("Theoretical");
for p_d in [0.5, 0.8, 0.95]:
ax[1].semilogx(n, sdr.peebles(p_d, p_fa, n), linestyle="--", label=p_d)
@savefig sdr_peebles_1.png
ax[1].semilogx(n, sdr.coherent_gain(n), color="k", label="Coherent"); \
ax[1].legend(title="$P_D$"); \
ax[1].set_xlabel("Number of samples, $N_{NC}$"); \
ax[1].set_ylabel("Non-coherent gain, $G_{NC}$"); \
ax[1].legend(title="$P_d$"); \
ax[1].set_xlabel("Number of samples, $N_{nc}$"); \
ax[1].set_ylabel("Non-coherent gain, $G_{nc}$"); \
ax[1].set_title("Peebles's approximation");

Group:
Expand Down
18 changes: 9 additions & 9 deletions src/sdr/_detection/_coherent_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,28 @@ def coherent_gain(time_bandwidth: npt.ArrayLike) -> npt.NDArray[np.float32]:

Arguments:
time_bandwidth: The time-bandwidth product $T_C B_C$ in seconds-Hz (unitless). If the signal bandwidth equals
the sample rate, the argument equals the number of samples $N_C$ to coherently integrate.
the sample rate, the argument equals the number of samples $N_c$ to coherently integrate.

Returns:
The coherent gain $G_C$ in dB.
The coherent gain $G_c$ in dB.

Notes:
The signal $x[n]$ is coherently integrated over $N_C$ samples to produce the output $y[n]$.
The signal $x[n]$ is coherently integrated over $N_c$ samples to produce the output $y[n]$.

$$y[n] = \sum_{m=0}^{N_C-1} x[n-m]$$
$$y[n] = \sum_{m=0}^{N_c-1} x[n-m]$$

The coherent integration gain is the reduction in SNR of $x[n]$ compared to $y[n]$, such that both signals
have the same detection performance.

$$\text{SNR}_{y,\text{dB}} = \text{SNR}_{x,\text{dB}} + G_C$$
$$\text{SNR}_{y,\text{dB}} = \text{SNR}_{x,\text{dB}} + G_c$$

The coherent integration gain is the time-bandwidth product

$$G_C = 10 \log_{10} (T_C B_C) .$$
$$G_c = 10 \log_{10} (T_C B_C) .$$

If the signal bandwidth equals the sample rate, the coherent gain is simply

$$G_C = 10 \log_{10} N_C .$$
$$G_c = 10 \log_{10} N_c .$$

Examples:
See the :ref:`coherent-integration` example.
Expand All @@ -63,8 +63,8 @@ def coherent_gain(time_bandwidth: npt.ArrayLike) -> npt.NDArray[np.float32]:
@savefig sdr_coherent_gain_1.png
plt.figure(); \
plt.semilogx(n_c, sdr.coherent_gain(n_c)); \
plt.xlabel("Number of samples, $N_C$"); \
plt.ylabel("Coherent gain (dB), $G_C$"); \
plt.xlabel("Number of samples, $N_c$"); \
plt.ylabel("Coherent gain (dB), $G_c$"); \
plt.title("Coherent gain as a function of the number of integrated samples");

Group:
Expand Down
60 changes: 30 additions & 30 deletions src/sdr/_detection/_correlator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ class ReplicaCorrelator:

where $\mathcal{E}$ is the received energy $\mathcal{E} = \sum\limits_{n=0}^{N-1} \left| s[n] \right|^2$.

The probability of detection $P_D$, probability of false alarm $P_{FA}$, and detection threshold
The probability of detection $P_d$, probability of false alarm $P_{fa}$, and detection threshold
$\gamma'$ are given by:

$$P_D = Q\left( Q^{-1}(P_{FA}) - \sqrt{\frac{2 \mathcal{E}}{\sigma^2}} \right)$$
$$P_{FA} = Q\left(\frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E} / 2}}\right)$$
$$\gamma' = \sqrt{\sigma^2 \mathcal{E} / 2} Q^{-1}(P_{FA})$$
$$P_d = Q\left( Q^{-1}(P_{fa}) - \sqrt{\frac{2 \mathcal{E}}{\sigma^2}} \right)$$
$$P_{fa} = Q\left(\frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E} / 2}}\right)$$
$$\gamma' = \sqrt{\sigma^2 \mathcal{E} / 2} Q^{-1}(P_{fa})$$

References:
- Steven Kay, *Fundamentals of Statistical Signal Processing: Detection Theory*, Sections 4.3.2 and 13.3.1.
Expand All @@ -62,8 +62,8 @@ class ReplicaCorrelator:
# Initializes the energy detector.

# Arguments:
# N_nc: The number of samples $N_{NC}$ to non-coherently integrate.
# p_fa: The desired probability of false alarm $P_{FA}$.
# N_nc: The number of samples $N_{nc}$ to non-coherently integrate.
# p_fa: The desired probability of false alarm $P_{fa}$.
# """
# if not isinstance(N_nc, int):
# raise TypeError(f"Argument 'N_nc' must be an integer, not {type(N_nc)}.")
Expand Down Expand Up @@ -94,13 +94,13 @@ def roc(

Arguments:
enr: The received energy-to-noise ratio $\mathcal{E}/\sigma^2$ in dB.
p_fa: The probability of false alarm $P_{FA}$. If `None`, the ROC curve is computed for
p_fa: The probability of false alarm $P_{fa}$. If `None`, the ROC curve is computed for
`p_fa = np.logspace(-10, 0, 101)`.
complex: Indicates whether the signal is complex.

Returns:
- The probability of false alarm $P_{FA}$.
- The probability of detection $P_D$.
- The probability of false alarm $P_{fa}$.
- The probability of detection $P_d$.

Examples:
.. ipython:: python
Expand Down Expand Up @@ -133,24 +133,24 @@ def p_d(
complex: bool = True,
) -> npt.NDArray[np.float64]:
r"""
Computes the probability of detection $P_D$.
Computes the probability of detection $P_d$.

Arguments:
enr: The received energy-to-noise ratio $\mathcal{E}/\sigma^2$ in dB.
p_fa: The probability of false alarm $P_{FA}$.
p_fa: The probability of false alarm $P_{fa}$.
complex: Indicates whether the signal is complex.

Returns:
The probability of detection $P_D$.
The probability of detection $P_d$.

Notes:
For real signals:

$$P_D = Q\left( Q^{-1}(P_{FA}) - \sqrt{\frac{\mathcal{E}}{\sigma^2}} \right)$$
$$P_d = Q\left( Q^{-1}(P_{fa}) - \sqrt{\frac{\mathcal{E}}{\sigma^2}} \right)$$

For complex signals:

$$P_D = Q\left( Q^{-1}(P_{FA}) - \sqrt{\frac{\mathcal{E}}{\sigma^2 / 2}} \right)$$
$$P_d = Q\left( Q^{-1}(P_{fa}) - \sqrt{\frac{\mathcal{E}}{\sigma^2 / 2}} \right)$$

References:
- Steven Kay, *Fundamentals of Statistical Signal Processing: Detection Theory*,
Expand All @@ -163,13 +163,13 @@ def p_d(

@savefig sdr_ReplicaCorrelator_p_d_1.png
plt.figure(); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-1), label="$P_{FA} = 10^{-1}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-2), label="$P_{FA} = 10^{-2}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-3), label="$P_{FA} = 10^{-3}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-4), label="$P_{FA} = 10^{-4}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-5), label="$P_{FA} = 10^{-5}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-6), label="$P_{FA} = 10^{-6}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-7), label="$P_{FA} = 10^{-7}$");
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-1), label="$P_{fa} = 10^{-1}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-2), label="$P_{fa} = 10^{-2}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-3), label="$P_{fa} = 10^{-3}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-4), label="$P_{fa} = 10^{-4}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-5), label="$P_{fa} = 10^{-5}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-6), label="$P_{fa} = 10^{-6}$"); \
sdr.plot.p_d(enr, sdr.ReplicaCorrelator.p_d(enr, 1e-7), label="$P_{fa} = 10^{-7}$");
"""
enr = np.asarray(enr)
p_fa = np.asarray(p_fa)
Expand All @@ -192,7 +192,7 @@ def p_fa(
complex: bool = True,
) -> npt.NDArray[np.float64]:
r"""
Computes the probability of false alarm $P_{FA}$.
Computes the probability of false alarm $P_{fa}$.

Arguments:
threshold: The threshold $\gamma'$.
Expand All @@ -201,16 +201,16 @@ def p_fa(
complex: Indicates whether the signal is complex.

Returns:
The probability of false alarm $P_{FA}$.
The probability of false alarm $P_{fa}$.

Notes:
For real signals:

$$P_{FA} = Q\left( \frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E}}} \right)$$
$$P_{fa} = Q\left( \frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E}}} \right)$$

For complex signals:

$$P_{FA} = Q\left( \frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E} / 2}} \right)$$
$$P_{fa} = Q\left( \frac{\gamma'}{\sqrt{\sigma^2 \mathcal{E} / 2}} \right)$$

References:
- Steven Kay, *Fundamentals of Statistical Signal Processing: Detection Theory*,
Expand Down Expand Up @@ -238,7 +238,7 @@ def threshold(
Computes the threshold $\gamma'$.

Arguments:
p_fa: The probability of false alarm $P_{FA}$.
p_fa: The probability of false alarm $P_{fa}$.
energy: The received energy $\mathcal{E} = \sum_{i=0}^{N-1} \left| s[n] \right|^2$.
sigma2: The noise variance $\sigma^2$.
complex: Indicates whether the signal is complex.
Expand All @@ -249,11 +249,11 @@ def threshold(
Notes:
For real signals:

$$\gamma' = \sqrt{\sigma^2 \mathcal{E}} Q^{-1}(P_{FA})$$
$$\gamma' = \sqrt{\sigma^2 \mathcal{E}} Q^{-1}(P_{fa})$$

For complex signals:

$$\gamma' = \sqrt{\sigma^2 \mathcal{E} / 2} Q^{-1}(P_{FA})$$
$$\gamma' = \sqrt{\sigma^2 \mathcal{E} / 2} Q^{-1}(P_{fa})$$

References:
- Steven Kay, *Fundamentals of Statistical Signal Processing: Detection Theory*,
Expand Down Expand Up @@ -306,14 +306,14 @@ def threshold(
# @property
# def N_nc(self) -> int:
# """
# The number of samples $N_{NC}$ to non-coherently integrate.
# The number of samples $N_{nc}$ to non-coherently integrate.
# """
# return self._N_nc

# @property
# def desired_p_fa(self) -> float:
# """
# The desired probability of false alarm $P_{FA}$.
# The desired probability of false alarm $P_{fa}$.
# """
# return self._p_fa

Expand Down
Loading
Loading