Skip to content

Commit d8b9c5f

Browse files
authored
fix: deterministic expenv behaviour (#35)
1 parent 430cd21 commit d8b9c5f

File tree

14 files changed

+851
-18
lines changed

14 files changed

+851
-18
lines changed

experimental_env/analysis/analyze_summarizers/error_summarizer.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ def calculate(self, results: list[ExperimentDescription]) -> tuple:
3838

3939
errors.append(error)
4040

41-
mean = np.sum(errors) / len(errors)
42-
standart_deviation = np.sqrt(np.sum([(x - mean) ** 2 for x in errors]) / len(errors))
41+
if not errors:
42+
return 0, 0, 0
4343

44-
errors.sort()
45-
median = errors[len(errors) // 2]
44+
mean = np.mean(errors)
45+
std = np.std(errors)
46+
median = np.median(errors)
4647

47-
return float(mean), float(standart_deviation), float(median)
48+
return float(mean), float(std), float(median)
4849

4950
def analyze_method(self, results: list[ExperimentDescription], method: str):
5051
mean, deviation, median = self.calculate(results)

experimental_env/experiment/estimators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def __init__(self, brkpointer, dst_checker):
9090

9191
@property
9292
def name(self):
93-
return "LM-EM"
93+
return "ELM"
9494

9595
def _helper(self, problem: OrderedProblem):
9696
"""

experimental_env/experiment/experiment_executors/abstract_executor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""A module that provides an abstract class for performing the 2nd stage of the experiment"""
22

3+
import random
34
import warnings
45
from abc import ABC, abstractmethod
56
from pathlib import Path
@@ -20,7 +21,7 @@ class AExecutor(ABC):
2021
as well as the implementation of the execute method, to implement the 2nd stage of the experiment.
2122
"""
2223

23-
def __init__(self, path: Path, cpu_count: int, seed):
24+
def __init__(self, path: Path, cpu_count: int, seed: int):
2425
"""
2526
Class constructor
2627
@@ -31,6 +32,8 @@ def __init__(self, path: Path, cpu_count: int, seed):
3132
self._out_dir = path
3233
self._cpu_count = cpu_count
3334
self._seed = seed
35+
36+
random.seed(self._seed)
3437
np.random.seed(self._seed)
3538

3639
@abstractmethod

experimental_env/experiment/experiment_executors/random_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def init_problems(self, ds_descriptions, models):
1919
return [
2020
Problem(
2121
descr.samples,
22-
RandomMixtureGenerator(self._seed).create_mixture(models),
22+
RandomMixtureGenerator().create_mixture(models),
2323
)
2424
for i, descr in enumerate(ds_descriptions)
2525
]

experimental_env/experiment/experiment_executors/standart_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def init_problems(self, ds_descriptions, models):
1919
return [
2020
Problem(
2121
descr.samples,
22-
StandartMixtureGenerator(self._seed).create_mixture(models),
22+
StandartMixtureGenerator().create_mixture(models),
2323
)
2424
for i, descr in enumerate(ds_descriptions)
2525
]

experimental_env/mixture_generators/abstract_generator.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""A module that provides an abstract class for generating a mixture."""
22

3-
import random
43
from abc import ABC, abstractmethod
54

65
from mpest import Distribution, MixtureDistribution
@@ -12,9 +11,6 @@ class AMixtureGenerator(ABC):
1211
An abstract class for generating mixtures.
1312
"""
1413

15-
def __init__(self, seed: int = 42):
16-
random.seed(seed)
17-
1814
@abstractmethod
1915
def generate_priors(self, models: list[type[AModel]]) -> list[float | None]:
2016
"""

experimental_env/mixture_generators/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ def generate_standart_params(models: list[type[AModel]]) -> list[Distribution]:
1616
params = [1.0]
1717
elif m == GaussianModel:
1818
params = [0.0, 1.0]
19-
else:
20-
params = [1.0, 1.5]
19+
else: # Weibull
20+
params = [1.0, 1.0]
2121

2222
dists.append(Distribution.from_params(m, params))
2323

@@ -34,7 +34,7 @@ def generate_uniform_params(models: list[type[AModel]]) -> list[Distribution]:
3434
params = [uniform(0.1, 5.0)]
3535
elif m == GaussianModel:
3636
params = [uniform(-5.0, 5.0), uniform(0.1, 5.0)]
37-
else:
37+
else: # Weibull
3838
params = [uniform(0.1, 5.0), uniform(0.1, 5.0)]
3939

4040
dists.append(Distribution.from_params(m, params))

experimental_env/preparation/dataset_generator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ def __init__(self, seed: int = 42):
2626
"""
2727
Setting seed for determined result.
2828
"""
29-
random.seed(seed)
3029
self._seed = seed
3130

31+
random.seed(self._seed)
32+
np.random.seed(self._seed)
33+
3234
def generate(
3335
self,
3436
samples_size: int,
@@ -59,7 +61,6 @@ class ConcreteDatasetGenerator:
5961
"""
6062

6163
def __init__(self, seed: int = 42):
62-
np.random.seed(seed)
6364
self._dists: list[Distribution] = []
6465
self._priors: list[float | None] = []
6566

tests/core/__init__.py

Whitespace-only changes.

tests/core/test_distribution.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
from unittest.mock import MagicMock, Mock
2+
3+
import numpy as np
4+
import pytest
5+
from hypothesis import given
6+
from hypothesis import strategies as st
7+
8+
from mpest.core.distribution import Distribution
9+
from mpest.models import AModel, AModelWithGenerator
10+
11+
12+
@st.composite
13+
def valid_params(draw, min_size=1, max_size=5):
14+
size = draw(st.integers(min_value=min_size, max_value=max_size))
15+
params_list = draw(
16+
st.lists(
17+
st.floats(min_value=-100, max_value=100, allow_nan=False, allow_infinity=False),
18+
min_size=size,
19+
max_size=size,
20+
)
21+
)
22+
return np.array(params_list)
23+
24+
25+
@st.composite
26+
def valid_x(draw):
27+
return draw(st.floats(min_value=-100, max_value=100, allow_nan=False, allow_infinity=False))
28+
29+
30+
@st.composite
31+
def valid_size(draw):
32+
return draw(st.integers(min_value=1, max_value=100))
33+
34+
35+
class MockModel(AModel):
36+
@property
37+
def name(self):
38+
return "MockModel"
39+
40+
def pdf(self, x, params):
41+
return 0.1 * x * sum(params)
42+
43+
def lpdf(self, x, params):
44+
return np.log(self.pdf(x, params))
45+
46+
def params_convert_to_model(self, params):
47+
return params
48+
49+
def params_convert_from_model(self, params):
50+
return params
51+
52+
53+
class MockModelWithGenerator(AModelWithGenerator):
54+
@property
55+
def name(self):
56+
return "MockModelWithGenerator"
57+
58+
def pdf(self, x, params):
59+
return 0.1 * x * sum(params)
60+
61+
def lpdf(self, x, params):
62+
return np.log(self.pdf(x, params))
63+
64+
def params_convert_to_model(self, params):
65+
return params
66+
67+
def params_convert_from_model(self, params):
68+
return params
69+
70+
def generate(self, params, size=1, **kwargs):
71+
return np.random.uniform(0, 1, size=size)
72+
73+
74+
class TestModuleDistribution:
75+
def test_init(self):
76+
model = Mock()
77+
params = np.array([1.0, 2.0])
78+
79+
dist = Distribution(model=model, params=params)
80+
81+
assert dist._model is model
82+
assert np.array_equal(dist._params, params)
83+
84+
def test_from_params(self):
85+
MockModelClass = Mock()
86+
mock_instance = Mock()
87+
MockModelClass.return_value = mock_instance
88+
params = [1.0, 2.0]
89+
90+
dist = Distribution.from_params(MockModelClass, params)
91+
92+
MockModelClass.assert_called_once()
93+
assert dist._model is mock_instance
94+
assert np.array_equal(dist._params, np.array(params))
95+
96+
def test_model_property(self):
97+
model = Mock()
98+
params = np.array([1.0, 2.0])
99+
100+
dist = Distribution(model=model, params=params)
101+
102+
assert dist.model is model
103+
104+
def test_params_property(self):
105+
model = Mock()
106+
params = np.array([1.0, 2.0])
107+
108+
dist = Distribution(model=model, params=params)
109+
110+
assert dist.params is params
111+
assert np.array_equal(dist.params, params)
112+
113+
def test_has_generator_property_true(self):
114+
model = MagicMock(spec=AModelWithGenerator)
115+
params = np.array([1.0, 2.0])
116+
117+
dist = Distribution(model=model, params=params)
118+
119+
assert dist.has_generator is True
120+
121+
def test_has_generator_property_false(self):
122+
model = MagicMock(spec=AModel)
123+
params = np.array([1.0, 2.0])
124+
125+
dist = Distribution(model=model, params=params)
126+
127+
assert dist.has_generator is False
128+
129+
@given(valid_x(), valid_params())
130+
def test_pdf_calls_model_pdf_correctly(self, x, params):
131+
model = Mock()
132+
return_value = 0.1
133+
converted_params = np.array([3.0, 4.0])
134+
model.params_convert_to_model.return_value = converted_params
135+
model.pdf.return_value = return_value
136+
137+
dist = Distribution(model=model, params=params)
138+
result = dist.pdf(x)
139+
140+
model.params_convert_to_model.assert_called_once_with(params)
141+
model.pdf.assert_called_once_with(x, converted_params)
142+
assert result == return_value
143+
144+
@given(valid_size(), valid_params())
145+
def test_generate_with_generator_model(self, size, params):
146+
model = MagicMock(spec=AModelWithGenerator)
147+
converted_params = np.array([3.0, 4.0])
148+
model.params_convert_to_model.return_value = converted_params
149+
generated_samples = np.random.uniform(0, 1, size=size)
150+
model.generate.return_value = generated_samples
151+
152+
dist = Distribution(model=model, params=params)
153+
result = dist.generate(size=size)
154+
155+
model.params_convert_to_model.assert_called_once_with(params)
156+
model.generate.assert_called_once_with(converted_params, size=size)
157+
assert np.array_equal(result, generated_samples)
158+
159+
def test_generate_without_generator_raises_typeerror(self):
160+
model = MagicMock(spec=AModel)
161+
params = np.array([1.0, 2.0])
162+
163+
dist = Distribution(model=model, params=params)
164+
165+
with pytest.raises(TypeError):
166+
dist.generate(size=3)
167+
168+
169+
class TestIntegrationDistribution:
170+
@given(valid_x(), valid_params())
171+
def test_pdf_integration(self, x, params):
172+
model = MockModel()
173+
dist = Distribution(model=model, params=params)
174+
175+
converted_params = model.params_convert_to_model(params)
176+
expected = model.pdf(x, converted_params)
177+
actual = dist.pdf(x)
178+
179+
assert actual == pytest.approx(expected)
180+
181+
@given(valid_size(), valid_params())
182+
def test_generate_integration(self, size, params):
183+
model = MockModelWithGenerator()
184+
185+
dist = Distribution(model=model, params=params)
186+
result = dist.generate(size=size)
187+
188+
assert result.shape == (size,)
189+
assert result.dtype == np.float64
190+
assert np.all(result >= 0)
191+
assert np.all(result < 1)
192+
193+
def test_generate_without_generator_raises_typeerror_integration(self):
194+
model = MockModel()
195+
params = np.array([1.0, 2.0])
196+
197+
dist = Distribution(model=model, params=params)
198+
199+
with pytest.raises(TypeError):
200+
dist.generate(size=3)
201+
202+
@given(valid_x(), valid_params())
203+
def test_pdf_consistent_results(self, x, params):
204+
model = MockModel()
205+
dist = Distribution(model=model, params=params)
206+
207+
result1 = dist.pdf(x)
208+
result2 = dist.pdf(x)
209+
210+
assert result1 == pytest.approx(result2)

0 commit comments

Comments
 (0)