diff --git a/examples/big_mono_tests.py b/examples/big_mono_tests.py index d844dff8..4c4ac01d 100644 --- a/examples/big_mono_tests.py +++ b/examples/big_mono_tests.py @@ -3,10 +3,6 @@ import random import numpy as np - -from examples.config import MAX_WORKERS, TESTS_OPTIMIZERS -from examples.mono_test_generator import generate_mono_test -from examples.utils import Clicker, Test, init_solver, run_tests, save_results from mpest.models import ( AModelWithGenerator, ExponentialModel, @@ -14,6 +10,10 @@ WeibullModelExp, ) +from examples.config import MAX_WORKERS, TESTS_OPTIMIZERS +from examples.mono_test_generator import generate_mono_test +from examples.utils import Clicker, Test, init_solver, run_tests, save_results + if __name__ == "__main__": random.seed(42) np.random.seed(42) diff --git a/examples/diff_test.py b/examples/diff_test.py index 582b7773..905752d4 100644 --- a/examples/diff_test.py +++ b/examples/diff_test.py @@ -3,12 +3,12 @@ import random import numpy as np +from mpest import Distribution, MixtureDistribution, Problem +from mpest.models import GaussianModel, WeibullModelExp from examples.config import MAX_WORKERS, TESTS_OPTIMIZERS from examples.mono_test_generator import Clicker from examples.utils import Test, init_solver, run_tests, save_results -from mpest import Distribution, MixtureDistribution, Problem -from mpest.models import GaussianModel, WeibullModelExp # Gaussian diff --git a/examples/mono_test_generator.py b/examples/mono_test_generator.py index f3c681b1..d6f86908 100644 --- a/examples/mono_test_generator.py +++ b/examples/mono_test_generator.py @@ -4,14 +4,14 @@ from collections.abc import Iterable import numpy as np - -from examples.utils import Clicker, Test from mpest.core.distribution import Distribution from mpest.core.mixture_distribution import MixtureDistribution from mpest.core.problem import Problem from mpest.em import EM from mpest.models import AModel, AModelWithGenerator +from examples.utils import Clicker, Test + def generate_mono_test( model_t: type[AModelWithGenerator], diff --git a/examples/prepare_result.py b/examples/prepare_result.py index ee9e7162..2a532188 100644 --- a/examples/prepare_result.py +++ b/examples/prepare_result.py @@ -4,13 +4,13 @@ import numpy as np import pandas as pd +from mpest.annotations import Samples +from mpest.core.mixture_distribution import DistributionInMixture, MixtureDistribution from tqdm.contrib.concurrent import process_map from examples.config import MAX_WORKERS from examples.mono_test_generator import Clicker from examples.utils import SingleSolverResult, TestResult -from mpest.annotations import Samples -from mpest.core.mixture_distribution import DistributionInMixture, MixtureDistribution def nll(samples: Samples, mixture: MixtureDistribution) -> float: diff --git a/examples/quick_test.py b/examples/quick_test.py index 434fdf60..86a464b4 100644 --- a/examples/quick_test.py +++ b/examples/quick_test.py @@ -3,10 +3,6 @@ import random import numpy as np - -from examples.config import MAX_WORKERS -from examples.mono_test_generator import generate_mono_test -from examples.utils import Clicker, Test, init_solver, run_tests, save_results from mpest.models import ( AModelWithGenerator, ExponentialModel, @@ -15,6 +11,10 @@ ) from mpest.optimizers import ALL_OPTIMIZERS +from examples.config import MAX_WORKERS +from examples.mono_test_generator import generate_mono_test +from examples.utils import Clicker, Test, init_solver, run_tests, save_results + def run_test(): """Runs the mixture distributions of single model quick test""" diff --git a/examples/readme_example/example.py b/examples/readme_example/example.py index 4379d10c..2d7db73e 100644 --- a/examples/readme_example/example.py +++ b/examples/readme_example/example.py @@ -3,7 +3,6 @@ import matplotlib.pyplot as plt import numpy as np import seaborn as sns - from mpest import Distribution, MixtureDistribution, Problem from mpest.em import EM from mpest.em.breakpointers import StepCountBreakpointer diff --git a/examples/readme_example/example_ml.py b/examples/readme_example/example_ml.py index c055ee4d..ff842065 100644 --- a/examples/readme_example/example_ml.py +++ b/examples/readme_example/example_ml.py @@ -7,11 +7,6 @@ import numpy as np import pandas as pd import seaborn as sns -from scipy.stats import entropy, wasserstein_distance -from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans -from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score, silhouette_score -from sklearn.neighbors import NearestNeighbors - from mpest import Distribution, MixtureDistribution, Problem from mpest.em import EM from mpest.em.breakpointers import StepCountBreakpointer @@ -20,6 +15,10 @@ from mpest.em.methods.method import Method from mpest.models import GaussianModel, WeibullModelExp from mpest.optimizers import ScipyCG +from scipy.stats import entropy, wasserstein_distance +from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans +from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score, silhouette_score +from sklearn.neighbors import NearestNeighbors os.makedirs("results", exist_ok=True) os.makedirs("results/plots", exist_ok=True) diff --git a/examples/utils.py b/examples/utils.py index 06984d1c..9984b945 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -7,9 +7,6 @@ from typing import ClassVar, NamedTuple import numpy as np -from tqdm.contrib.concurrent import process_map - -from examples.config import RESULTS_FOLDER from mpest.annotations import Samples from mpest.core.mixture_distribution import MixtureDistribution from mpest.core.problem import Problem, Result @@ -21,6 +18,9 @@ ) from mpest.em.methods.likelihood_method import LikelihoodMethod from mpest.optimizers import TOptimizer +from tqdm.contrib.concurrent import process_map + +from examples.config import RESULTS_FOLDER np.seterr(all="ignore") diff --git a/mpest/em/methods/moments_method.py b/mpest/em/methods/moments_method.py new file mode 100644 index 00000000..acf51cd7 --- /dev/null +++ b/mpest/em/methods/moments_method.py @@ -0,0 +1,82 @@ +"""The module in which the moments method is presented""" + +import numpy as np + +from mpest import Samples +from mpest.core.distribution import Distribution +from mpest.core.mixture_distribution import MixtureDistribution +from mpest.core.problem import Problem, Result +from mpest.em.methods.abstract_steps import AMaximization +from mpest.exceptions import MStepError +from mpest.utils import ResultWithError + +EResult = tuple[Problem, np.ndarray] | ResultWithError[MixtureDistribution] + + +class MomentsMStep(AMaximization[EResult]): + """ + Class which calculate new params using matrix with indicator from E step. + """ + + def calc_order_moment_of_index_element(self, order: int, i: int, samples: Samples, indicators: np.ndarray) -> float: + """ + A function that calculates the list of n-th moments of each distribution. + + :param order: Order of Moment. + :param i: The number of the distribution for which we count the moment. + :param samples: Ndarray with samples. + :param indicators: Matrix with indicators + + :return: order-Moment of index element. + """ + + sum_j_row_probabilities = np.sum(indicators[i]) + + if sum_j_row_probabilities == 0: + return 0 + + moment_values = samples**order + + numerator = np.sum(moment_values * indicators[i]) + + return numerator / sum_j_row_probabilities + + def step(self, e_result: EResult) -> Result: + """ + A function that performs M step + + :param e_result: Tuple with problem, new_priors and indicators. + """ + + if isinstance(e_result, ResultWithError): + return e_result + + problem, indicators = e_result + + samples = problem.samples + + mixture = problem.distributions + + new_priors = np.sum(indicators, axis=1) / len(samples) + + max_params_count = max(len(d.params) for d in mixture) + moments = np.zeros(shape=[len(mixture), max_params_count]) + + for j, d in enumerate(mixture): + for r in range(len(d.params)): + moments[j][r] = self.calc_order_moment_of_index_element(r + 1, j, samples, indicators) + + for i, d in enumerate(mixture): + if d.model.name == "WeibullExp" and (moments[i][0] * moments[i][1] < 0): + error = MStepError("The Weibull distribution degenerated in the first step.") + return ResultWithError(mixture.distributions, error) + + new_distributions = [] + + for j, d in enumerate(mixture): + new_params = d.model.calc_moments_params(moments[j]) + new_d = Distribution(d.model, d.model.params_convert_to_model(new_params)) + new_distributions.append(new_d) + + new_mixture = MixtureDistribution.from_distributions(new_distributions, new_priors) + return ResultWithError(new_mixture) diff --git a/mpest/models/exponential.py b/mpest/models/exponential.py index 67f9e5bb..884dc3c7 100644 --- a/mpest/models/exponential.py +++ b/mpest/models/exponential.py @@ -65,3 +65,13 @@ def calc_params(self, moments: list[float]): lm = 1 / moments[0] return np.array([lm]) + + def calc_moments_params(self, moments: list[float]): + """ + The function for calculating params using moments + """ + + # Calculate lambda parameter + lm = 1 / moments[0] + + return np.array([lm]) diff --git a/mpest/models/gaussian.py b/mpest/models/gaussian.py index bb079305..72e8c2c2 100644 --- a/mpest/models/gaussian.py +++ b/mpest/models/gaussian.py @@ -74,3 +74,21 @@ def calc_params(self, moments: list[float]) -> np.ndarray: variance = m2 * np.sqrt(np.pi) return np.array([mean, variance]) + + def calc_moments_params(self, moments: list[float]) -> np.ndarray: + """ + The function for calculating params using moments + """ + + m1 = moments[0] + m2 = moments[1] + + # Calculate mean parameter + mu = m1 + + # Calculate variance parameter + variance = m2 - m1**2 + + sigma = np.sqrt(variance) + + return np.array([mu, sigma]) diff --git a/mpest/models/weibull.py b/mpest/models/weibull.py index 14eede86..6280ad75 100644 --- a/mpest/models/weibull.py +++ b/mpest/models/weibull.py @@ -3,6 +3,8 @@ import math import numpy as np +from scipy.optimize import root_scalar +from scipy.special import gamma from scipy.stats import weibull_min from mpest.annotations import Params, Samples @@ -86,3 +88,25 @@ def calc_params(self, moments: list[float]): lm = m1 / math.gamma(1 + 1 / k) return np.array([k, lm]) + + def calc_moments_params(self, moments: list[float]): + """ + The function for calculating params using moments + """ + + m1, m2 = moments[0], moments[1] + + moments_ratio = m2 / (m1**2) + + def equation_for_k(k): + return gamma(1 + 2 / k) / (gamma(1 + 1 / k) ** 2) - moments_ratio + + solution = root_scalar(equation_for_k, method="brentq", bracket=[0.02, 100]) + if not solution.converged: + raise RuntimeError(f"Error in calculating the equation: m1={m1}, m2={m2}") + + k = solution.root + + lm = m1 / gamma(1 + 1 / k) + + return np.array([k, lm]) diff --git a/tests/tests_moments/moments_utils.py b/tests/tests_moments/moments_utils.py new file mode 100644 index 00000000..1c68005e --- /dev/null +++ b/tests/tests_moments/moments_utils.py @@ -0,0 +1,21 @@ +from mpest.core.problem import Problem, Result +from mpest.em import EM +from mpest.em.breakpointers import ParamDifferBreakpointer, StepCountBreakpointer +from mpest.em.distribution_checkers import ( + FiniteChecker, + PriorProbabilityThresholdChecker, +) +from mpest.em.methods.likelihood_method import BayesEStep +from mpest.em.methods.method import Method +from mpest.em.methods.moments_method import MomentsMStep + + +def run_test(problem: Problem, deviation: float) -> Result: + method = Method(BayesEStep(), MomentsMStep()) + em_algo = EM( + StepCountBreakpointer() + ParamDifferBreakpointer(deviation=deviation), + FiniteChecker() + PriorProbabilityThresholdChecker(), + method, + ) + + return em_algo.solve(problem=problem) diff --git a/tests/tests_moments/test_any_distributions_complex.py b/tests/tests_moments/test_any_distributions_complex.py new file mode 100644 index 00000000..6c09e05a --- /dev/null +++ b/tests/tests_moments/test_any_distributions_complex.py @@ -0,0 +1,111 @@ +"""Unit test module which tests mixture of several different distribution parameter estimation""" + +# pylint: disable=duplicate-code +# pylint: disable=too-many-arguments + +import numpy as np +import pytest +from mpest import Distribution, MixtureDistribution, Problem +from mpest.models import ( + AModelWithGenerator, + ExponentialModel, + GaussianModel, + WeibullModelExp, +) +from tests.tests_moments.moments_utils import run_test +from tests.utils import ( + check_for_params_error_tolerance, + check_for_priors_error_tolerance, +) + + +def idfunc(vals): + """Function for customizing pytest ids""" + + if isinstance(vals, list): + if issubclass(type(vals[0]), AModelWithGenerator): + return str([d.name for d in vals]) + return vals + return f"{vals}" + + +@pytest.mark.parametrize( + "models, params, start_params, prior_probabilities, size, deviation, expected_params_error,expected_priors_error", + [ + ( + [WeibullModelExp(), GaussianModel()], + [[0.5, 1.0], [5.0, 1.0]], + [[1.5, 0.4], [3.0, 4.0]], + [0.33, 0.66], + 1000, + 0.01, + 0.52, # It was 0.28 and the test did not pass. + 0.1, + ), + ( + [ExponentialModel(), GaussianModel()], + [[0.5], [5.0, 1.0]], + [[1.0], [3.0, 1.5]], + [0.33, 0.66], + 1000, + 0.01, + 0.15, + 0.1, + ), + ( + [ExponentialModel(), WeibullModelExp()], + [[0.5], [5.0, 1.0]], + [[1.0], [7.0, 2.0]], + [0.66, 0.33], + 1000, + 0.01, + 0.3, + 0.2, + ), + ( + [ExponentialModel(), GaussianModel(), WeibullModelExp()], + [[1.0], [5.0, 1.0], [4.0, 1.0]], + [[2.0], [3.0, 3.0], [2.0, 2.0]], + [0.25, 0.25, 0.5], + 1000, + 0.01, + 0.2, + 0.1, + ), + ], + ids=idfunc, +) +def test_any_distributions_complex( + models, + params, + start_params, + prior_probabilities, + size, + deviation, + expected_params_error, + expected_priors_error, +): + """Runs mixture of several different distributions parameter estimation unit test""" + + np.random.seed(42) + + params = [np.array(param) for param in params] + start_params = [np.array(param) for param in start_params] + + base_mixture = MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, params)], + prior_probabilities, + ) + + x = base_mixture.generate(size) + + problem = Problem( + samples=x, + distributions=MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, start_params)] + ), + ) + + result = run_test(problem=problem, deviation=deviation) + assert check_for_params_error_tolerance([result], base_mixture, expected_params_error) + assert check_for_priors_error_tolerance([result], base_mixture, expected_priors_error) diff --git a/tests/tests_moments/test_any_distributions_simple.py b/tests/tests_moments/test_any_distributions_simple.py new file mode 100644 index 00000000..b2f9f676 --- /dev/null +++ b/tests/tests_moments/test_any_distributions_simple.py @@ -0,0 +1,96 @@ +"""Unit test module which tests mixture of several different distribution parameter estimation""" + +# pylint: disable=duplicate-code +# pylint: disable=too-many-arguments + +import numpy as np +import pytest +from mpest import Distribution, MixtureDistribution, Problem +from mpest.models import ( + AModelWithGenerator, + ExponentialModel, + GaussianModel, + WeibullModelExp, +) +from tests.tests_moments.moments_utils import run_test +from tests.utils import check_for_params_error_tolerance + + +def idfunc(vals): + """Function for customizing pytest ids""" + + if isinstance(vals, list): + if issubclass(type(vals[0]), AModelWithGenerator): + return str([d.name for d in vals]) + return vals + return f"{vals}" + + +@pytest.mark.parametrize( + "models, params, start_params, size, deviation, expected_error", + [ + ( + [WeibullModelExp(), GaussianModel()], + [[0.5, 1.0], [5.0, 1.0]], + [[0.5, 1.5], [3.0, 3.0]], + 1000, + 0.01, + 0.37, + ), + ( + [ExponentialModel(), GaussianModel()], + [[0.5], [5.0, 1.0]], + [[1.0], [0.0, 5.0]], + 1000, + 0.01, + 0.25, + ), + ( + [ExponentialModel(), WeibullModelExp()], + [[0.5], [5.0, 1.0]], + [[2.0], [3.0, 1.5]], + 1000, + 0.01, + 0.2, + ), + ( + [ExponentialModel(), GaussianModel(), WeibullModelExp()], + [[1.0], [5.0, 1.0], [4.0, 1.0]], + [[3.0], [2.0, 5.0], [2.0, 2.0]], + 1000, + 0.01, + 0.7, + ), + ], + ids=idfunc, +) +def test_any_distributions_simple( + models, + params, + start_params, + size, + deviation, + expected_error, +): + """Runs mixture of several different distributions parameter estimation unit test""" + + np.random.seed(42) + + params = [np.array(param) for param in params] + start_params = [np.array(param) for param in start_params] + + base_mixture = MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, params)], + ) + + x = base_mixture.generate(size) + + problem = Problem( + samples=x, + distributions=MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, start_params)] + ), + ) + + result = run_test(problem=problem, deviation=deviation) + assert check_for_params_error_tolerance([result], base_mixture, expected_error) diff --git a/tests/tests_moments/test_one_distribution.py b/tests/tests_moments/test_one_distribution.py new file mode 100644 index 00000000..f06c14f8 --- /dev/null +++ b/tests/tests_moments/test_one_distribution.py @@ -0,0 +1,69 @@ +"""Unit test module which tests mixture of one distribution parameter estimation""" + +# pylint: disable=duplicate-code +# pylint: disable=too-many-arguments + +import numpy as np +import pytest +from mpest.core.distribution import Distribution +from mpest.core.mixture_distribution import MixtureDistribution +from mpest.core.problem import Problem +from mpest.models import ( + AModelWithGenerator, + ExponentialModel, + GaussianModel, + WeibullModelExp, +) +from tests.tests_moments.moments_utils import run_test +from tests.utils import check_for_params_error_tolerance + + +def idfunc(vals): + """Function for customizing pytest ids""" + + if issubclass(type(vals), AModelWithGenerator): + return vals.name + if isinstance(vals, tuple): + return vals + return f"{vals}" + + +@pytest.mark.parametrize( + "model, params, start_params, size, deviation, expected_error", + [ + (WeibullModelExp(), (0.5, 0.5), (0.3, 2.0), 500, 0.01, 0.22), + (WeibullModelExp(), (1.0, 1.0), (0.5, 2.0), 500, 0.01, 0.05), + (GaussianModel(), (0.0, 5.0), (-1.0, 4.0), 500, 0.01, 0.16), + (GaussianModel(), (1.0, 5.0), (2.0, 1.0), 500, 0.01, 0.15), + (ExponentialModel(), (1.0,), (0.5,), 500, 0.01, 0.05), + (ExponentialModel(), (2.0,), (3.0,), 500, 0.01, 0.05), + ], + ids=idfunc, +) +def test_one_distribution( + model: AModelWithGenerator, + params, + start_params, + size: int, + deviation: float, + expected_error: float, +): + """Runs mixture of one distribution parameter estimation unit test""" + + np.random.seed(42) + + params = np.array(params) + start_params = np.array(start_params) + + base_model = Distribution(model, params) + x = base_model.generate(size) + + problem = Problem( + samples=x, + distributions=MixtureDistribution.from_distributions([Distribution(model, start_params)]), + ) + + result = run_test(problem=problem, deviation=deviation) + assert check_for_params_error_tolerance( + [result], MixtureDistribution.from_distributions([base_model]), expected_error + ) diff --git a/tests/tests_moments/test_two_same_distributions_complex.py b/tests/tests_moments/test_two_same_distributions_complex.py new file mode 100644 index 00000000..418744a4 --- /dev/null +++ b/tests/tests_moments/test_two_same_distributions_complex.py @@ -0,0 +1,138 @@ +"""Unit test module which tests mixture of two distributions parameter estimation""" + +# pylint: disable=duplicate-code +# pylint: disable=too-many-arguments +# pylint: disable=too-many-locals + +import numpy as np +import pytest +from mpest.core.distribution import Distribution +from mpest.core.mixture_distribution import MixtureDistribution +from mpest.core.problem import Problem +from mpest.models import ( + AModelWithGenerator, + ExponentialModel, + GaussianModel, + WeibullModelExp, +) +from mpest.utils import Factory +from tests.tests_moments.moments_utils import run_test +from tests.utils import ( + check_for_params_error_tolerance, + check_for_priors_error_tolerance, +) + + +def idfunc(vals): + """Function for customizing pytest ids""" + + if isinstance(vals, Factory): + return vals.cls().name + if isinstance(vals, list): + return vals + return f"{vals}" + + +@pytest.mark.parametrize( + "model_factory, params, start_params, prior_probability, size, deviation," + "expected_params_error, expected_priors_error", + [ + ( + Factory(WeibullModelExp), + [(0.5, 1.0), (1.0, 0.5)], + [(1.0, 1.0), (1.5, 0.5)], + [0.56, 0.44], + 500, + 0.01, + 0.27, + 0.1, + ), + ( + Factory(WeibullModelExp), + [(0.5, 0.5), (2.0, 1.0)], + [(1.0, 1.5), (3.0, 2.5)], + [0.27, 0.73], + 500, + 0.01, + 0.28, + 0.15, + ), + ( + Factory(GaussianModel), + [(0.0, 5.0), (1.0, 1.0)], + [(-1.0, 7.0), (2.0, 1.5)], + [0.1, 0.9], + 500, + 0.01, + 0.7, + 0.15, + ), + ( + Factory(GaussianModel), + [(0.0, 5.0), (2.0, 2.0)], + [(1.0, 3.0), (1.0, 3.5)], + [0.3, 0.7], + 500, + 0.01, + 1.35, + 0.37, + ), + ( + Factory(ExponentialModel), + [(1.0,), (2.0,)], + [(0.2,), (1.0,)], + [0.3, 0.7], + 500, + 0.01, + 0.2, + 0.1, + ), + ( + Factory(ExponentialModel), + [(2.0,), (5.0,)], + [(1.0,), (7.0,)], + [0.645, 0.355], + 500, + 0.01, + 0.25, + 0.3, + ), + ], + ids=idfunc, +) +def test_two_same_distributions_complex( + model_factory: Factory[AModelWithGenerator], + params, + start_params, + prior_probability: list[float], + size: int, + deviation: float, + expected_params_error, + expected_priors_error, +): + """Runs mixture of two distributions parameter estimation unit test""" + + np.random.seed(42) + + models = [model_factory.construct() for _ in range(len(params))] + + params = [np.array(param) for param in params] + start_params = [np.array(param) for param in start_params] + + base_mixture = MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, params)], + prior_probability, + ) + + x = base_mixture.generate(size) + + problem = Problem( + samples=x, + distributions=MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, start_params)] + ), + ) + + result = run_test(problem=problem, deviation=deviation) + assert check_for_params_error_tolerance([result], base_mixture, expected_params_error) + assert check_for_priors_error_tolerance([result], base_mixture, expected_priors_error) diff --git a/tests/tests_moments/test_two_same_distributions_simple.py b/tests/tests_moments/test_two_same_distributions_simple.py new file mode 100644 index 00000000..bf783701 --- /dev/null +++ b/tests/tests_moments/test_two_same_distributions_simple.py @@ -0,0 +1,125 @@ +""" +Unit test module which tests mixture of two distributions parameter estimation +with equally probable prior probabilities +""" + +# pylint: disable=duplicate-code +# pylint: disable=too-many-arguments +# pylint: disable=too-many-locals + +import numpy as np +import pytest +from mpest.core.distribution import Distribution +from mpest.core.mixture_distribution import MixtureDistribution +from mpest.core.problem import Problem +from mpest.models import ( + AModelWithGenerator, + ExponentialModel, + GaussianModel, + WeibullModelExp, +) +from mpest.utils import Factory +from tests.tests_moments.moments_utils import run_test +from tests.utils import check_for_params_error_tolerance + + +def idfunc(vals): + """Function for customizing pytest ids""" + + if isinstance(vals, Factory): + return vals.cls().name + if isinstance(vals, list): + return vals + return f"{vals}" + + +@pytest.mark.parametrize( + "model_factory, params, start_params, size, deviation, expected_error", + [ + ( + Factory(WeibullModelExp), + [(0.5, 1.0), (1.0, 0.5)], + [(1.0, 1.0), (0.5, 1.5)], + 500, + 0.01, + 0.84, + ), + ( + Factory(WeibullModelExp), + [(0.5, 0.5), (2.0, 1.0)], + [(0.1, 1.0), (1.0, 2.0)], + 500, + 0.01, + 0.91, + ), + ( + Factory(GaussianModel), + [(0.0, 5.0), (1.0, 1.0)], + [(1.0, 5.0), (-1.0, 5.0)], + 500, + 0.01, + 0.33, + ), + ( + Factory(GaussianModel), + [(4.0, 5.0), (3.0, 2.0)], + [(3.0, 5.0), (3.5, 3.0)], + 500, + 0.01, + 0.52, + ), + ( + Factory(ExponentialModel), + [(1.0,), (2.0,)], + [(0.5,), (1.5,)], + 500, + 0.01, + 0.1, + ), + ( + Factory(ExponentialModel), + [(2.0,), (5.0,)], + [(3.0,), (1.0,)], + 500, + 0.01, + 0.25, + ), + ], + ids=idfunc, +) +def test_two_same_distributions_simple( + model_factory: Factory[AModelWithGenerator], + params, + start_params, + size: int, + deviation: float, + expected_error: float, +): + """Runs mixture of two distributions parameter estimation unit test""" + + np.random.seed(42) + + models = [model_factory.construct() for _ in range(len(params))] + + params = [np.array(param) for param in params] + start_params = [np.array(param) for param in start_params] + + x = [] + for model, param in zip(models, params): + x += list(model.generate(param, size, normalized=0)) + np.random.shuffle(x) + x = np.array(x) + + base_mixture = MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, params)] + ) + + problem = Problem( + samples=x, + distributions=MixtureDistribution.from_distributions( + [Distribution(model, param) for model, param in zip(models, start_params)] + ), + ) + + result = run_test(problem=problem, deviation=deviation) + assert check_for_params_error_tolerance([result], base_mixture, expected_error)