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

Improve calculation of random variable arithmetic #440

Merged
merged 25 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b1fe196
Change `\ldots` to `\dots`
mhostetter Dec 16, 2024
e350ac9
Add `sdr.max_distribution()`
mhostetter Dec 16, 2024
fc258c3
Add unit tests for #435
mhostetter Dec 16, 2024
847b6c8
Update the docstring
mhostetter Dec 16, 2024
e335e41
Clean up `sum_distribution()`
mhostetter Dec 16, 2024
06eed3c
Clean up `sum_distributions()`
mhostetter Dec 16, 2024
5b08f5b
Clean up `multiply_distributions()`
mhostetter Dec 16, 2024
4bd32a0
Minor tweaks
mhostetter Dec 16, 2024
2ef7d3b
Add `sdr.min_distribution()`
mhostetter Dec 16, 2024
6845a13
Add unit test for #438
mhostetter Dec 16, 2024
8d14732
Rename `sum_distribution(s)` to `add_distribution(s)`
mhostetter Dec 16, 2024
7314ba7
Rename files
mhostetter Dec 16, 2024
f7b78cd
Compute exact `add_distribution()` when possible
mhostetter Dec 16, 2024
5ac34b9
Add unit tests for #439
mhostetter Dec 16, 2024
c6a002c
Fix LaTeX
mhostetter Dec 16, 2024
5b37f03
Rename `add_distribution()` to `add_iid_rvs()`
mhostetter Dec 16, 2024
0f130e0
Rename `add_distributions()` to `add_rvs()`
mhostetter Dec 16, 2024
48e0337
Rename `multiply_distributions()` to `multiply_rvs()`
mhostetter Dec 16, 2024
611aae6
Rename `min_distribution()` to `min_iid_rvs()`
mhostetter Dec 16, 2024
02031c9
Rename `max_distribution()` to `max_iid_rvs()`
mhostetter Dec 16, 2024
26ff47f
Rename doc images
mhostetter Dec 16, 2024
72b8aa7
Add `sdr.subtract_rvs()`
mhostetter Dec 16, 2024
c72f46d
Add unit test for #436
mhostetter Dec 16, 2024
40bae67
Fix capitalization
mhostetter Dec 16, 2024
6c9a5d4
Reword
mhostetter Dec 16, 2024
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
6 changes: 3 additions & 3 deletions src/sdr/_detection/_theory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
verify_not_specified,
verify_scalar,
)
from .._probability import sum_distribution
from .._probability import add_iid_rvs


@export
Expand Down Expand Up @@ -244,7 +244,7 @@ def _h0(
h0 = scipy.stats.norm(0, np.sqrt(sigma2_per))
elif detector == "linear":
h0 = scipy.stats.chi(nu, scale=np.sqrt(sigma2_per))
h0 = sum_distribution(h0, n_nc)
h0 = add_iid_rvs(h0, n_nc)
elif detector == "square-law":
h0 = scipy.stats.chi2(nu * n_nc, scale=sigma2_per)

Expand Down Expand Up @@ -490,7 +490,7 @@ def _h1(
else:
# Folded normal distribution has 1 degree of freedom
h1 = scipy.stats.foldnorm(np.sqrt(lambda_), scale=np.sqrt(sigma2_per))
h1 = sum_distribution(h1, n_nc)
h1 = add_iid_rvs(h1, n_nc)
elif detector == "square-law":
h1 = scipy.stats.ncx2(nu * n_nc, lambda_ * n_nc, scale=sigma2_per)

Expand Down
661 changes: 547 additions & 114 deletions src/sdr/_probability.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/sdr/_simulation/_channel_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ def dmc(
x: The input sequence $x$ with $x_i \in \mathcal{X}$.
P: The $m \times n$ transition probability matrix $P$, where $P_{i,j} = \Pr(Y = y_j | X = x_i)$.
X: The input alphabet $\mathcal{X}$ of size $m$. If `None`, it is assumed that
$\mathcal{X} = \{0, 1, \ldots, m-1\}$.
$\mathcal{X} = \{0, 1, \dots, m-1\}$.
Y: The output alphabet $\mathcal{Y}$ of size $n$. If `None`, it is assumed that
$\mathcal{Y} = \{0, 1, \ldots, n-1\}$.
$\mathcal{Y} = \{0, 1, \dots, n-1\}$.
seed: The seed for the random number generator. This is passed to :func:`numpy.random.default_rng`.

Returns:
Expand Down Expand Up @@ -546,8 +546,8 @@ def __init__(

Arguments:
P: The $m \times n$ transition probability matrix $P$, where $P = \Pr(Y = y_j | X = x_i)$.
X: The input alphabet $\mathcal{X}$ of size $m$. If `None`, it is assumed that $\mathcal{X} = \{0, 1, \ldots, m-1\}$.
Y: The output alphabet $\mathcal{Y}$ of size $n$. If `None`, it is assumed that $\mathcal{Y} = \{0, 1, \ldots, n-1\}$.
X: The input alphabet $\mathcal{X}$ of size $m$. If `None`, it is assumed that $\mathcal{X} = \{0, 1, \dots, m-1\}$.
Y: The output alphabet $\mathcal{Y}$ of size $n$. If `None`, it is assumed that $\mathcal{Y} = \{0, 1, \dots, n-1\}$.
seed: The seed for the random number generator. This is passed to :func:`numpy.random.default_rng`.
"""
P = verify_arraylike(P, float=True, ndim=2, inclusive_min=0, inclusive_max=1)
Expand Down
92 changes: 92 additions & 0 deletions tests/probability/test_add_iid_rvs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import numpy as np
import scipy.stats

import sdr


def test_normal():
X = scipy.stats.norm(loc=-1, scale=0.5)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


def test_rayleigh():
X = scipy.stats.rayleigh(scale=1)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


def test_rician():
X = scipy.stats.rice(2)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


def test_exponential():
X = scipy.stats.expon(scale=1)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


def test_gamma():
X = scipy.stats.gamma(a=2, scale=1)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


# def test_poisson():
# X = scipy.stats.poisson(mu=2)
# _verify(X, 2)
# _verify(X, 3)
# _verify(X, 4)


# def test_bernoulli():
# X = scipy.stats.bernoulli(p=0.5)
# _verify(X, 2)
# _verify(X, 3)
# _verify(X, 4)


# def test_geomertic():
# X = scipy.stats.geom(p=0.5)
# _verify(X, 2)
# _verify(X, 3)
# _verify(X, 4)


def test_chi2():
X = scipy.stats.chi2(df=2)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)


def _verify(X, n):
# Numerically compute the distribution
Y = sdr.add_iid_rvs(X, n)

# Empirically compute the distribution
y = X.rvs((100_000, n)).sum(axis=1)
hist, bins = np.histogram(y, bins=101, density=True)
x = bins[1:] - np.diff(bins) / 2

if False:
import matplotlib.pyplot as plt

plt.figure()
plt.plot(x, X.pdf(x), label="X")
plt.plot(x, Y.pdf(x), label="X + ... + X")
plt.hist(y, bins=101, cumulative=False, density=True, histtype="step", label="X + .. + X empirical")
plt.legend()
plt.xlabel("Random variable")
plt.ylabel("Probability density")
plt.title("Sum of distribution")
plt.show()

assert np.allclose(Y.pdf(x), hist, atol=np.max(hist) * 0.1)
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_rayleigh_rician():

def _verify(X, Y):
# Numerically compute the distribution
Z = sdr.sum_distributions(X, Y)
Z = sdr.add_rvs(X, Y)

# Empirically compute the distribution
z = X.rvs(100_000) + Y.rvs(100_000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,30 @@
def test_normal():
X = scipy.stats.norm(loc=-1, scale=0.5)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)
_verify(X, 10)
_verify(X, 100)


def test_rayleigh():
X = scipy.stats.rayleigh(scale=1)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)
_verify(X, 10)
_verify(X, 100)


def test_rician():
X = scipy.stats.rice(2)
_verify(X, 2)
_verify(X, 3)
_verify(X, 4)
_verify(X, 10)
_verify(X, 100)


def _verify(X, n):
# Numerically compute the distribution
Y = sdr.sum_distribution(X, n)
Y = sdr.max_iid_rvs(X, n)

# Empirically compute the distribution
y = X.rvs((100_000, n)).sum(axis=1)
y = X.rvs((100_000, n)).max(axis=1)
hist, bins = np.histogram(y, bins=101, density=True)
x = bins[1:] - np.diff(bins) / 2

Expand All @@ -39,8 +39,10 @@ def _verify(X, n):

plt.figure()
plt.plot(x, X.pdf(x), label="X")
plt.plot(x, Y.pdf(x), label="X + ... + X")
plt.hist(y, bins=101, cumulative=False, density=True, histtype="step", label="X + .. + X empirical")
plt.plot(x, Y.pdf(x), label=r"$\max(X_1, \dots, X_n)$")
plt.hist(
y, bins=101, cumulative=False, density=True, histtype="step", label=r"$\max(X_1, \dots, X_n)$ empirical"
)
plt.legend()
plt.xlabel("Random variable")
plt.ylabel("Probability density")
Expand Down
52 changes: 52 additions & 0 deletions tests/probability/test_min_iid_rvs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import numpy as np
import scipy.stats

import sdr


def test_normal():
X = scipy.stats.norm(loc=-1, scale=0.5)
_verify(X, 2)
_verify(X, 10)
_verify(X, 100)


def test_rayleigh():
X = scipy.stats.rayleigh(scale=1)
_verify(X, 2)
_verify(X, 10)
_verify(X, 100)


def test_rician():
X = scipy.stats.rice(2)
_verify(X, 2)
_verify(X, 10)
_verify(X, 100)


def _verify(X, n):
# Numerically compute the distribution
Y = sdr.min_iid_rvs(X, n)

# Empirically compute the distribution
y = X.rvs((100_000, n)).min(axis=1)
hist, bins = np.histogram(y, bins=101, density=True)
x = bins[1:] - np.diff(bins) / 2

if False:
import matplotlib.pyplot as plt

plt.figure()
plt.plot(x, X.pdf(x), label="X")
plt.plot(x, Y.pdf(x), label=r"$\max(X_1, \dots, X_n)$")
plt.hist(
y, bins=101, cumulative=False, density=True, histtype="step", label=r"$\max(X_1, \dots, X_n)$ empirical"
)
plt.legend()
plt.xlabel("Random variable")
plt.ylabel("Probability density")
plt.title("Sum of distribution")
plt.show()

assert np.allclose(Y.pdf(x), hist, atol=np.max(hist) * 0.1)
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _verify(X, Y):
x = bins[1:] - np.diff(bins) / 2

# Numerically compute the distribution, only do so over the histogram bins (for speed)
Z = sdr.multiply_distributions(X, Y, x)
Z = sdr.multiply_rvs(X, Y, x)

if False:
import matplotlib.pyplot as plt
Expand Down
60 changes: 60 additions & 0 deletions tests/probability/test_subtract_rvs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import numpy as np
import scipy.stats

import sdr


def test_normal_normal():
X = scipy.stats.norm(loc=-1, scale=0.5)
Y = scipy.stats.norm(loc=2, scale=1.5)
_verify(X, Y)


def test_rayleigh_rayleigh():
X = scipy.stats.rayleigh(scale=1)
Y = scipy.stats.rayleigh(loc=1, scale=2)
_verify(X, Y)


def test_rician_rician():
X = scipy.stats.rice(2)
Y = scipy.stats.rice(3)
_verify(X, Y)


def test_normal_rayleigh():
X = scipy.stats.norm(loc=-1, scale=0.5)
Y = scipy.stats.rayleigh(loc=2, scale=1.5)
_verify(X, Y)


def test_rayleigh_rician():
X = scipy.stats.rayleigh(scale=1)
Y = scipy.stats.rice(3)
_verify(X, Y)


def _verify(X, Y):
# Numerically compute the distribution
Z = sdr.subtract_rvs(X, Y)

# Empirically compute the distribution
z = X.rvs(100_000) - Y.rvs(100_000)
hist, bins = np.histogram(z, bins=101, density=True)
x = bins[1:] - np.diff(bins) / 2

if False:
import matplotlib.pyplot as plt

plt.figure()
plt.plot(x, X.pdf(x), label="X")
plt.plot(x, Y.pdf(x), label="Y")
plt.plot(x, Z.pdf(x), label="X - Y")
plt.hist(z, bins=101, cumulative=False, density=True, histtype="step", label="X - Y empirical")
plt.legend()
plt.xlabel("Random variable")
plt.ylabel("Probability density")
plt.title("Difference of two distributions")
plt.show()

assert np.allclose(Z.pdf(x), hist, atol=np.max(hist) * 0.1)
Loading