Skip to content

Commit 97f35e1

Browse files
Merge pull request #53 from mathematicalmichael/hotfix/testing-domain
harden testing
2 parents 498f71b + 0fe5acd commit 97f35e1

12 files changed

+223
-84
lines changed

docs/conf.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@
147147

148148
# -- Options for HTML output -------------------------------------------------
149149

150-
#html_theme = "alabaster"
150+
# html_theme = "alabaster"
151151

152-
#html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"}
152+
# html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"}
153153

154154

155155
html_theme = "furo"
@@ -203,8 +203,8 @@
203203

204204

205205
html_css_files = [
206-
"custom.css",
207-
]
206+
"custom.css",
207+
]
208208

209209
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
210210
# using the given strftime format.

docs/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ appdirs
55
configupdater
66
packaging
77
furo
8-
setuptools>=38.3
8+
setuptools>=58.3
99
setuptools_scm
1010
sphinx>=3.2.1
1111
sphinx-copybutton

src/mud/__init__.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
if sys.version_info[:2] >= (3, 8):
44
# TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
5-
from importlib.metadata import PackageNotFoundError, version # pragma: no cover
5+
from importlib.metadata import (PackageNotFoundError, # pragma: no cover
6+
version)
67
else:
7-
from importlib_metadata import PackageNotFoundError, version # pragma: no cover
8+
from importlib_metadata import (PackageNotFoundError, # pragma: no cover
9+
version)
810

911
try:
1012
# Change here if project is renamed and does not equal the package name

src/mud/base.py

+32-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import List, Union
2+
13
import numpy as np
24
from scipy.stats import distributions as dist
35
from scipy.stats import gaussian_kde as gkde
@@ -19,7 +21,7 @@ class DensityProblem(object):
1921
>>> Y = np.repeat(X, num_obs, 1)
2022
>>> y = np.ones(num_obs)*0.5 + np.random.randn(num_obs)*0.05
2123
>>> W = wme(Y, y)
22-
>>> B = DensityProblem(X, W, np.array([[0,1], [0,1]]))
24+
>>> B = DensityProblem(X, W, np.array([[0,1]]))
2325
>>> np.round(B.mud_point()[0],1)
2426
0.5
2527
@@ -46,6 +48,15 @@ def _n_features(self):
4648
def _n_samples(self):
4749
return self.y.shape[0]
4850

51+
def set_weights(self, weights: Union[np.ndarray, List]):
52+
if weights is not None:
53+
assert (
54+
len(weights) == self._n_samples
55+
), f"`weights` must size {self._n_samples}"
56+
if isinstance(weights, list):
57+
weights = np.array(weights)
58+
self._weights = weights # / weights.sum()
59+
4960
def set_observed(self, distribution=dist.norm()):
5061
self._ob = distribution.pdf(self.y).prod(axis=1)
5162

@@ -63,20 +74,30 @@ def set_initial(self, distribution=None):
6374
self._up = None
6475
self._pr = None
6576

66-
def set_predicted(self, distribution=None, **kwargs):
67-
if "weights" not in kwargs:
68-
kwargs["weights"] = self._weights
69-
else:
70-
self._weights = kwargs["weights"]
77+
def set_predicted(self, distribution=None, bw_method=None, weights=None, **kwargs):
78+
"""
79+
If no distribution is passed, `scipy.stats.gaussian_kde` is used and the
80+
arguments `bw_method` and `weights` will be passed to it.
81+
If `weights` is specified, it will be saved as the `self._weights`
82+
attribute in the class. If omitted, `self._weights` will be used in its place.
83+
84+
85+
Note: `distribution` should be a frozen distribution if using `scipy`.
86+
"""
87+
if weights is None:
88+
weights = self._weights
89+
else: # TODO: log this to the user as INFO
90+
self.set_weights(weights)
91+
weights = self._weights
7192

7293
if distribution is None:
7394
# Reweight kde of predicted by weights from previous iteration if present
74-
distribution = gkde(self.y.T, **kwargs)
75-
pred_pdf = distribution.pdf(self.y.T).T
95+
distribution = gkde(self.y.T, bw_method=bw_method, weights=weights)
96+
pred_pdf_values = distribution.pdf(self.y.T).T
7697
else:
77-
pred_pdf = distribution.pdf(self.y, **kwargs)
98+
pred_pdf_values = distribution.pdf(self.y, **kwargs)
7899

79-
self._pr = pred_pdf
100+
self._pr = pred_pdf_values
80101
self._up = None
81102

82103
def fit(self, **kwargs):
@@ -125,7 +146,7 @@ class BayesProblem(object):
125146
>>> num_obs = 50
126147
>>> Y = np.repeat(X, num_obs, 1)
127148
>>> y = np.ones(num_obs)*0.5 + np.random.randn(num_obs)*0.05
128-
>>> B = BayesProblem(X, Y, np.array([[0,1], [0,1]]))
149+
>>> B = BayesProblem(X, Y, np.array([[0,1]]))
129150
>>> B.set_likelihood(ds.norm(loc=y, scale=0.05))
130151
>>> np.round(B.map_point()[0],1)
131152
0.5

src/mud/funs.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
"""
66

77
import argparse
8-
import sys
98
import logging
9+
import sys
10+
1011
import numpy as np
12+
from scipy.stats import distributions as dists
1113

1214
from mud import __version__
13-
from mud.base import DensityProblem, BayesProblem
14-
from scipy.stats import distributions as dists
15+
from mud.base import BayesProblem, DensityProblem
1516

1617
__author__ = "Mathematical Michael"
1718
__copyright__ = "Mathematical Michael"

src/mud/plot.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from matplotlib import pyplot as plt
21
import numpy as np
2+
from matplotlib import pyplot as plt
3+
34
from mud.util import null_space
45

56

tests/conftest.py

+61-28
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,57 @@
77
https://pytest.org/latest/plugins.html
88
"""
99

10+
import numpy as np
1011
import pytest
11-
from mud.base import DensityProblem, BayesProblem
12-
from mud.funs import wme
1312
from scipy.stats import distributions as ds
14-
import numpy as np
13+
14+
from mud.base import BayesProblem, DensityProblem
15+
from mud.funs import wme
1516

1617

1718
@pytest.fixture
18-
def identity_1D_50_wme():
19-
X = np.random.rand(100, 1)
20-
num_observations = 50
21-
y_pred = np.repeat(X, num_observations, 1)
22-
y_true = 0.5
23-
noise = 0.05
24-
y_observed = y_true * np.ones(num_observations) + noise * np.random.randn(
25-
num_observations
26-
)
27-
Y = wme(y_pred, y_observed, sd=noise)
28-
return (X, Y)
19+
def dist_wo_weights():
20+
class Dist:
21+
@classmethod
22+
def pdf(self, x, **kwargs):
23+
return []
24+
25+
return Dist
26+
27+
28+
@pytest.fixture
29+
def problem_generator_identity_1D():
30+
def identity_uniform_1D(
31+
num_samples=2000, num_obs=20, y_true=0.5, noise=0.05, weights=None
32+
):
33+
"""
34+
Sets up an inverse problem using the unit domain and uniform distribution
35+
under an identity map. This is equivalent to studying a
36+
\"steady state\" signal over time, or taking repeated measurements
37+
of the same quantity to reduce variance in the uncertainty.
38+
"""
39+
dist = ds.uniform(loc=0, scale=1)
40+
X = dist.rvs(size=(num_samples, 1))
41+
y_pred = np.repeat(X, num_obs, 1)
42+
# data is truth + noise
43+
y_observed = y_true * np.ones(num_obs) + noise * np.random.randn(num_obs)
44+
Y = wme(y_pred, y_observed, sd=noise)
45+
# analytical construction of predicted domain under identity map.
46+
y_domain = np.repeat(np.array([[0], [1]]), num_obs, 1)
47+
mn, mx = wme(y_domain, y_observed, sd=noise)
48+
loc, scale = mn, mx - mn
49+
dist = ds.uniform(loc=loc, scale=scale)
50+
51+
D = DensityProblem(X, Y, np.array([[0, 1]]), weights=weights)
52+
D.set_predicted(dist)
53+
return D
54+
55+
return identity_uniform_1D
2956

3057

3158
@pytest.fixture
32-
def identity_problem_mud_1D(identity_1D_50_wme):
33-
X, Y = identity_1D_50_wme
34-
return DensityProblem(X, Y, np.array([[0, 1], [0, 1]]))
59+
def identity_problem_mud_1D(problem_generator_identity_1D):
60+
return problem_generator_identity_1D()
3561

3662

3763
@pytest.fixture
@@ -44,22 +70,29 @@ def identity_problem_map_1D():
4470
y_observed = y_true * np.ones(num_observations) + noise * np.random.randn(
4571
num_observations
4672
)
47-
B = BayesProblem(X, y_pred, np.array([[0, 1], [0, 1]]))
73+
B = BayesProblem(X, y_pred, np.array([[0, 1]]))
4874
B.set_likelihood(ds.norm(loc=y_observed, scale=noise))
4975
return B
5076

5177

5278
@pytest.fixture
53-
def identity_problem_mud_1D_equal_weights(identity_1D_50_wme):
54-
X, Y = identity_1D_50_wme
55-
weights = np.ones(X.shape[0])
56-
return DensityProblem(X, Y, np.array([[0, 1], [0, 1]]), weights=weights)
79+
def identity_problem_mud_1D_equal_weights(problem_generator_identity_1D):
80+
num_samples = 5000
81+
return problem_generator_identity_1D(
82+
num_samples=num_samples,
83+
weights=np.ones(num_samples),
84+
)
5785

5886

5987
@pytest.fixture
60-
def identity_problem_mud_1D_bias_weights(identity_1D_50_wme):
61-
X, Y = identity_1D_50_wme
62-
weights = np.ones(X.shape[0])
63-
weights[X[:, 0] < 0.2] = 0.1
64-
weights[X[:, 0] > 0.8] = 0.1
65-
return DensityProblem(X, Y, np.array([[0, 1], [0, 1]]), weights=weights)
88+
def identity_problem_mud_1D_bias_weights(problem_generator_identity_1D):
89+
num_samples = 5000
90+
weights = np.ones(num_samples)
91+
D = problem_generator_identity_1D(
92+
num_samples=num_samples,
93+
weights=np.ones(num_samples),
94+
)
95+
weights[D.X[:, 0] < 0.2] = 0.1
96+
weights[D.X[:, 0] > 0.8] = 0.1
97+
D.set_weights(weights)
98+
return D

tests/test_base.py

+2-29
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,11 @@ def test_identity_mud_problem_1D(identity_problem_mud_1D):
1313

1414
# Act
1515
mud_point = D.estimate()
16-
updated_density = D._up
17-
ratio = D._r
18-
19-
# Assert
20-
assert np.round(mud_point, 1) == 0.5
21-
assert np.sum(updated_density) > 0
22-
assert np.mean(ratio) > 0
23-
24-
25-
def test_we_can_set_weights_in_predicted(identity_problem_mud_1D_equal_weights):
26-
"""Mimicks existing usage in mud-examples"""
27-
# Arrange
28-
# weights were used for initialization
29-
D = identity_problem_mud_1D_equal_weights
30-
D.set_initial() # domain has been set -> uniform as default
31-
# want to make sure we can set weights on predicted
32-
weights = np.random.rand(D._n_samples)
33-
D.set_predicted(weights=weights)
34-
35-
# Act
36-
mud_point = D.estimate()
37-
updated_density = D._up
3816
ratio = D._r
3917

4018
# Assert
41-
# ensure weights were set correctly
42-
assert np.linalg.norm(weights - D._weights) == 0
4319
assert np.round(mud_point, 1) == 0.5
44-
assert np.sum(updated_density) > 0
45-
assert np.mean(ratio) > 0
20+
assert np.abs(np.mean(ratio) - 1) < 0.2
4621

4722

4823
def test_identity_mud_1D_with_equal_weights(identity_problem_mud_1D_equal_weights):
@@ -51,13 +26,11 @@ def test_identity_mud_1D_with_equal_weights(identity_problem_mud_1D_equal_weight
5126

5227
# Act
5328
mud_point = D.estimate()
54-
updated_density = D._up
5529
ratio = D._r
5630

5731
# Assert
5832
assert np.round(mud_point, 1) == 0.5
59-
assert np.sum(updated_density) > 0
60-
assert np.mean(ratio) > 0
33+
assert np.abs(np.mean(ratio) - 1) < 0.2
6134

6235

6336
def test_identity_mud_1D_with_biased_weights(identity_problem_mud_1D_bias_weights):

tests/test_funs.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# -*- coding: utf-8 -*-
22

33
import unittest
4-
import mud.funs as mdf
4+
55
import numpy as np
66

7+
import mud.funs as mdf
8+
79
__author__ = "Mathematical Michael"
810
__copyright__ = "Mathematical Michael"
911
__license__ = "mit"
@@ -35,13 +37,14 @@ def test_solutions_with_orthogonal_map(self):
3537
err_map = sol_map - t
3638

3739
# Assert
38-
assert np.linalg.norm(err_mud) < 1e-8
39-
assert np.linalg.norm(err_alt) < 1e-8
40+
assert np.linalg.norm(err_mud) < 1e-6
41+
assert np.linalg.norm(err_alt) < 1e-6
4042
assert np.linalg.norm(err_mud) < np.linalg.norm(err_map)
4143

4244
def test_updated_cov_has_R_equal_zero_for_full_rank_A(self):
4345
up_cov = mdf.updated_cov(self.A, self.id, self.id)
44-
assert np.linalg.norm(up_cov - np.linalg.inv(self.A.T @ self.A)) < 1e-6
46+
absolute_error = np.linalg.norm(up_cov - np.linalg.inv(self.A.T @ self.A))
47+
assert absolute_error / len(up_cov) < 1e-12
4548

4649

4750
class TestWME(unittest.TestCase):

tests/test_norm.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# -*- coding: utf-8 -*-
22

33
import unittest
4-
import mud.norm as mdn
4+
55
import numpy as np
66

7+
import mud.norm as mdn
8+
79
__author__ = "Mathematical Michael"
810
__copyright__ = "Mathematical Michael"
911
__license__ = "mit"

0 commit comments

Comments
 (0)