diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..994d91bc2e9e06b8bce244ebbd00804733809a9e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,169 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+doc/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+*.csv
+*.npy
+*.json
+wandb/
+assets/
+variables/
+*.pb
+*.v2
+distribs
diff --git a/G_SPC/__init__.py b/G_SPC/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/G_SPC/exception.py b/G_SPC/exception.py
new file mode 100644
index 0000000000000000000000000000000000000000..d97f82e73e3566b0f84d112d5618a9c901d345a1
--- /dev/null
+++ b/G_SPC/exception.py
@@ -0,0 +1,70 @@
+class GSPCError(Exception):
+    """Exception raised when an error raises in the Generalized Statistical Process Control package."""
+
+    def __init__(self, message="Error within the GSPC package"):
+        """Initializes the GSPCError
+
+        Parameters
+        ----------
+        message: str
+            The error message
+        """
+        self.message = message
+        super().__init__(self.message)
+
+
+class DistributionNotFeasibleError(GSPCError):
+    """Exception raised when no distribution of the given type with the given
+    parameters is feasible to fulfill the stability requirements.
+
+    """
+
+    def __init__(
+        self,
+        message="Not possible to parametrize the given distribution to given h and tolerance limits",
+    ):
+        """Initializes the DistributionNotFeasibleError
+
+        Parameters
+        ----------
+        message: str
+            The error message
+        """
+        self.message = message
+        super().__init__(self.message)
+
+
+class SumOfWeightsExceededError(GSPCError):
+    """Exception raised when the row sum of at least one row of the k matrix is
+    one or greater.
+
+    """
+
+    def __init__(
+        self, message="Sum within at least one row of the k matrix is one or greater.",
+    ):
+        """Initializes the SumOfWeightsExceededError
+
+        Parameters
+        ----------
+        message: str
+            The error message
+        """
+        self.message = message
+        super().__init__(self.message)
+
+
+class KMatrixError(GSPCError):
+    """All errors related to k matrix."""
+
+
+class InitializationError(GSPCError):
+    """All errors related to initialization of Distribution Mixing"""
+
+
+class SolvingError(GSPCError):
+    """All errors related to solving the optimization problem"""
+
+
+class ParameterError(GSPCError):
+    """All errors related to wrongly specified parameter sets"""
diff --git a/G_SPC/mixture/__init__.py b/G_SPC/mixture/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/G_SPC/mixture/distribution/__init__.py b/G_SPC/mixture/distribution/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/G_SPC/mixture/distribution/impl.py b/G_SPC/mixture/distribution/impl.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c191ce21365ec1b9bbfc9fb970291940ec447e7
--- /dev/null
+++ b/G_SPC/mixture/distribution/impl.py
@@ -0,0 +1,991 @@
+"""This module contains concrete implementations"""
+
+from __future__ import annotations
+
+import math
+from typing import Any
+
+import numpy as np
+from scipy import stats
+
+from G_SPC.mixture.distribution import parametrize
+from G_SPC.mixture.distribution.interface import AbstractDistribution, AbstractMixture
+
+
+class NpMixture(AbstractMixture):
+    def __init__(
+        self,
+        weights: list[float] | np.ndarray | None = None,
+        components: list[AbstractDistribution] | None = None,
+        weights_err_tol: float = 0.0001,
+    ) -> None:
+        if weights is not None:
+            self._weights = np.array(weights) / np.sum(weights)  # scale sum to 1
+            self._weights = self._weights.tolist()
+        else:
+            self._weights = weights
+
+        self._components = components
+        self._weights_err_tol = weights_err_tol
+
+    def parametrize(
+        self,
+        weights: list[float] | np.ndarray,
+        components: list[AbstractDistribution],
+        weights_err_tol: None | float = 0.0001,
+    ) -> None:
+        self._weights = np.array(weights) / np.sum(weights)  # scale sum to 1
+        self._weights = self._weights.tolist()
+        self._components = components
+        self._weights_err_tol = (
+            weights_err_tol if weights_err_tol is not None else self.weights_err_tol
+        )
+
+    def _sample_mixture(self, n_size: int) -> np.ndarray:
+        # a probability mixture sampling is equivalent to sampling from each distribution
+        #   component separately and then draw n_size elements from all samples according
+        #   to the mixture weights as probability for each element to be drawn
+        samples = np.array(
+            [distrib.take_sample(n_size) for distrib in self._components]
+        ).flatten()
+
+        # We need a coefficient vector, where each vector element represents its probability
+        #   to be chosen from the above sample. The above sample takes n_size samples from the
+        #   first distribution, then n_size samples from the second, and so on.
+        #   So we need a coefficient vector, where the weight of the first distribution is
+        #   repeated n_size times, then the second weight repeated n_size times, and so on.
+        #   E.g. when self.weights = [0.1, 0.2, 0.7] and n_size=3, then we need:
+        #   coeffs=[0.1, 0.1, 0.1, 0.2, 0.2, 0.2, 0.7, 0.7, 0.7]
+        coeffs = np.tile(np.array(self.weights), (n_size, 1)).T.flatten()
+        coeffs /= np.sum(coeffs)  # scale sum to 1
+
+        # probability for each element to be drawn according to mixture weights
+        return np.random.choice(samples, n_size, p=coeffs)
+
+    @property
+    def weights(self) -> list[float]:
+        return self._weights
+
+    @property
+    def components(self) -> list[AbstractDistribution]:
+        return self._components
+
+    @property
+    def weights_err_tol(self) -> float:
+        return self._weights_err_tol
+
+
+class SciPyNormal(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyNormal:
+        distrib = SciPyNormal(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, loc: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        loc: float or None,
+            if the distribution is used to generate OK data, then loc can be set to None, because the loc
+            parameter will be re-set during self.parametrize() anyways
+        scale: float or None
+            see loc
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+        Returns
+        -------
+        None
+        """
+        self.loc = loc
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.norm.rvs(loc=self.loc, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.norm.cdf(quantile, loc=self.loc, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_loc: float, _scale: float) -> SciPyNormal:
+            self.__init__(_loc, _scale, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            loc, scale = parametrize.center_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+                parametrize_nok=parametrize_nok,
+            )
+        else:
+            loc, scale = parametrize.rnd_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+                parametrize_nok=parametrize_nok,
+            )
+        self.__init__(loc, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "loc": self.loc,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.loc,
+            "param_2": self.scale,
+        }
+
+
+class SciPyLogistic(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyLogistic:
+        distrib = SciPyLogistic(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, loc: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        loc: float or None,
+            if the distribution is used to generate OK data, then loc can be set to None, because the loc
+            parameter will be re-set during self.parametrize() anyways
+        scale: float or None
+            see loc
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.loc = loc
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.logistic.rvs(loc=self.loc, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.logistic.cdf(quantile, loc=self.loc, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_loc: float, _scale: float) -> SciPyLogistic:
+            self.__init__(_loc, _scale, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            loc, scale = parametrize.center_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+                parametrize_nok=parametrize_nok,
+            )
+        else:
+            loc, scale = parametrize.rnd_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+            )
+        self.__init__(loc, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "loc": self.loc,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.loc,
+            "param_2": self.scale,
+        }
+
+
+class SciPyWeibull(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyWeibull:
+        distrib = SciPyWeibull(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, shape: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        shape: float or None,
+            if the distribution is used to generate OK data, then concentration can be set to None, because the
+            concentration parameter will be re-set during self.parametrize() anyways
+        scale: float or None,
+            see concentration
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.shape = shape
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.weibull_min.rvs(
+            c=self.shape, loc=0, scale=self.scale, size=sample_size
+        )
+
+    def cdf(self, quantile: float) -> float:
+        return stats.weibull_min.cdf(quantile, c=self.shape, loc=0, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_shape: float, _scale: float) -> SciPyWeibull:
+            self.__init__(_shape, _scale, self.parametrize_centered)
+            return self
+
+        shape, scale = parametrize.weibull(
+            factory,
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_centered=self.parametrize_centered,
+            parametrize_nok=parametrize_nok,
+        )
+
+        self.__init__(shape, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "shape": self.shape,
+            "scale": self.scale,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.shape,
+            "param_2": self.scale,
+        }
+
+
+class SciPyLogNorm(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyLogNorm:
+        distrib = SciPyLogNorm(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, loc: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        loc: float or None,
+            if the distribution is used to generate OK data, then loc can be set to None, because the loc
+            parameter will be re-set during self.parametrize() anyways
+        scale: float or None
+            see loc
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.loc = loc
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.lognorm.rvs(
+            s=self.scale, loc=0, scale=math.exp(self.loc), size=sample_size
+        )
+
+    def cdf(self, quantile: float) -> float:
+        return stats.lognorm.cdf(
+            quantile, s=self.scale, loc=0, scale=math.exp(self.loc)
+        )
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_loc: float, _scale: float) -> SciPyLogNorm:
+            self.__init__(_loc, _scale, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            loc, scale = parametrize.center_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=True,
+                parametrize_nok=parametrize_nok,
+            )
+        else:
+            loc, scale = parametrize.rnd_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=True,
+                parametrize_nok=parametrize_nok,
+            )
+        self.__init__(loc, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "loc": self.loc,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.loc,
+            "param_2": self.scale,
+        }
+
+
+class SciPyUniform(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyUniform:
+        distrib = SciPyUniform(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, low: float | None, high: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        low: float or None,
+            if the distribution is used to generate OK data, then low can be set to None, because the low
+            parameter will be re-set during self.parametrize() anyways
+        high: float or None
+            see low
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.low = low
+        self.high = high
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.uniform.rvs(
+            loc=self.low, scale=(self.high - self.low), size=sample_size
+        )
+
+    def cdf(self, quantile: float) -> float:
+        return stats.uniform.cdf(quantile, loc=self.low, scale=(self.high - self.low))
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_low: float, _high: float) -> SciPyUniform:
+            self.__init__(_low, _high, self.parametrize_centered)
+            return self
+
+        low, high = parametrize.uniform(
+            factory,
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_centered=self.parametrize_centered,
+            parametrize_nok=parametrize_nok,
+        )
+
+        self.__init__(low, high, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "low": self.low,
+            "high": self.high,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.low,
+            "param_2": self.high,
+        }
+
+
+class SciPyPareto(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyPareto:
+        distrib = SciPyPareto(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, scale: float | None, shape: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        scale: float or None,
+            if the distribution is used to generate OK data, then scale can be set to None, because the scale
+            parameter will be re-set during self.parametrize() anyways
+        shape: float or None
+            see scale
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.scale = scale
+        self.shape = shape
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.pareto.rvs(b=self.shape, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.pareto.cdf(quantile, b=self.shape, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_scale: float, _shape: float) -> SciPyPareto:
+            self.__init__(_scale, _shape, self.parametrize_centered)
+            return self
+
+        scale, shape = parametrize.pareto(
+            factory,
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_centered=self.parametrize_centered,
+            parametrize_nok=parametrize_nok,
+        )
+
+        self.__init__(scale, shape, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "shape": self.shape,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.scale,
+            "param_2": self.shape,
+        }
+
+
+class SciPyCauchy(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyCauchy:
+        distrib = SciPyCauchy(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, loc: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        loc: float or None,
+            if the distribution is used to generate OK data, then loc can be set to None, because the loc
+            parameter will be re-set during self.parametrize() anyways
+        scale: float or None
+            see loc
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.loc = loc
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.cauchy.rvs(loc=self.loc, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.cauchy.cdf(quantile, loc=self.loc, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_loc: float, _scale: float) -> SciPyCauchy:
+            self.__init__(_loc, _scale, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            loc, scale = parametrize.center_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+                parametrize_nok=parametrize_nok,
+            )
+        else:
+            loc, scale = parametrize.rnd_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                log_loc=False,
+                parametrize_nok=parametrize_nok,
+            )
+
+        self.__init__(loc, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "loc": self.loc,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.loc,
+            "param_2": self.scale,
+        }
+
+
+class SciPyLogLogistic(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyLogLogistic:
+        distrib = SciPyLogLogistic(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, scale: float | None, shape: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        scale: float or None,
+            if the distribution is used to generate OK data, then scale can be set to None, because the scale
+            parameter will be re-set during self.parametrize() anyways
+        shape: float or None
+            see scale
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.shape = shape
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.fisk.rvs(c=self.shape, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.fisk.cdf(quantile, c=self.shape, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_scale: float, _shape: float) -> SciPyLogLogistic:
+            self.__init__(_scale, _shape, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            scale, shape = parametrize.log_logistic_center(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                parametrize_nok=parametrize_nok,
+            )
+        else:
+            scale, shape = parametrize.rnd_log_logistic(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                parametrize_nok=parametrize_nok,
+            )
+
+        self.__init__(scale, shape, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "shape": self.shape,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.scale,
+            "param_2": self.shape,
+        }
+
+
+class SciPyLaplace(AbstractDistribution):
+    @classmethod
+    def make_nok(
+        cls,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+    ) -> SciPyLaplace:
+        distrib = SciPyLaplace(None, None, False)
+        distrib.parametrize(
+            lower_limit,
+            median,
+            upper_limit,
+            sigma_level,
+            max_iter,
+            parametrize_nok=True,
+        )
+        return distrib
+
+    def __init__(
+        self, loc: float | None, scale: float | None, parametrize_centered: bool
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        loc: float or None,
+            if the distribution is used to generate OK data, then loc can be set to None, because the loc
+            parameter will be re-set during self.parametrize() anyways
+        scale: float or None
+            see loc
+        parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+
+        Returns
+        -------
+        None
+        """
+        self.loc = loc
+        self.scale = scale
+        self.parametrize_centered = parametrize_centered
+
+    @property
+    def is_centered(self) -> bool:
+        return self.parametrize_centered
+
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        return stats.laplace.rvs(loc=self.loc, scale=self.scale, size=sample_size)
+
+    def cdf(self, quantile: float) -> float:
+        return stats.laplace.cdf(quantile, loc=self.loc, scale=self.scale)
+
+    def parametrize(
+        self,
+        lower_limit: float,
+        median,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        def factory(_loc: float, _scale: float) -> SciPyLaplace:
+            self.__init__(_loc, _scale, self.parametrize_centered)
+            return self
+
+        if self.parametrize_centered:
+            loc, scale = parametrize.center_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                parametrize_nok,
+            )
+        else:
+            loc, scale = parametrize.rnd_loc_scale(
+                factory,
+                lower_limit,
+                median,
+                upper_limit,
+                sigma_level,
+                max_iter,
+                parametrize_nok,
+            )
+        self.__init__(loc, scale, self.parametrize_centered)
+
+    @property
+    def parameters(self) -> dict[str, Any]:
+        return {
+            "type": str(type(self)),
+            "scale": self.scale,
+            "loc": self.loc,
+            "parametrize_centered": self.parametrize_centered,
+            "param_1": self.loc,
+            "param_2": self.scale,
+        }
diff --git a/G_SPC/mixture/distribution/interface.py b/G_SPC/mixture/distribution/interface.py
new file mode 100644
index 0000000000000000000000000000000000000000..756072011a40ebdfc2ea8e274faa5ec6a43a88de
--- /dev/null
+++ b/G_SPC/mixture/distribution/interface.py
@@ -0,0 +1,194 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Any
+
+import numpy as np
+
+from G_SPC.exception import SumOfWeightsExceededError, InitializationError
+
+
+class AbstractDistribution(ABC):
+    # noinspection PyUnresolvedReferences
+    """Protocol interface for base distributions (no mixing)
+
+    Attributes
+    ----------
+    parameters: dict
+
+    """
+
+    @property
+    @abstractmethod
+    def is_centered(self) -> bool:
+        ...
+
+    @abstractmethod
+    def take_sample(self, sample_size: int) -> np.ndarray:
+        ...
+
+    @abstractmethod
+    def cdf(self, quantile: float) -> float:
+        """Cumulative density function
+
+        Parameters
+        ----------
+        quantile: float
+            quantile value for which the value of the cumulative density function should be evaluated
+
+        Returns
+        -------
+        float
+            value of the cumulative density function at the given quantile value
+        """
+        ...
+
+    @abstractmethod
+    def parametrize(
+        self,
+        lower_limit: float,
+        median: float,
+        upper_limit: float,
+        sigma_level: float,
+        max_iter: int,
+        parametrize_nok: bool = False,
+    ) -> None:
+        """Sets parameters for the OK distributions
+
+        For centered distributions the parameters of the widest distribution fulfilling the stability criteria are
+        calculated (doubling/halving the parameter iterative). For non-centered distributions parameters are set on
+        a random basis fulfilling boundary conditions varying for the different distribution (for details see the
+        parametrize functions of the implementations of the distributions). The procedure is iterated until parameters
+        are found so that the resulting distribution fulfills the stability criteria or the maximum number of iterations
+        is reached. In latter case the programs exits with an error.
+
+        If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+        criteria.
+
+        Parameters
+        ----------
+        lower_limit: float
+            Lower limit of the tolerance interval
+        median: float
+            Median value given for the mixture distribution
+        upper_limit: float
+            Upper limit of the tolerance interval
+        sigma_level: float, > 0
+            Measure for which proportion of the probability density function has to be within the tolerance interval
+        max_iter: int
+            Maximal number of tries per distribution to find parameters leading to a stable distribution according to
+            the research paper
+        parametrize_nok: bool
+            Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+        Returns
+        -------
+        None
+        """
+        ...
+
+    @property
+    @abstractmethod
+    def parameters(self) -> dict[str, Any]:
+        """Distribution parameter dictionary
+
+        Returns
+        -------
+        parameters: dict
+            kwargs dict used for init
+        """
+        return ...
+
+
+class AbstractMixture(ABC):
+    @abstractmethod
+    def __init__(
+        self,
+        weights: list[float] | np.ndarray | None,
+        components: list[AbstractDistribution] | None,
+        weights_err_tol: float = 0.0001,
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        weights: list of floats or array
+            weights of components, must sum up to 1
+        components: list of distribution (instance objects)
+            distribution instance objects, each must inherit from AbstractDistribution,
+            list must have same number of elements as list of weights
+        weights_err_tol: None or float, default 0.0001
+            error tolerance during summing up list of weights,
+            because float addition has some inherent error (floating point error)
+
+        Returns
+        -------
+        None
+        """
+        ...
+
+    @abstractmethod
+    def parametrize(
+        self,
+        weights: list[float] | np.ndarray,
+        components: list[AbstractDistribution],
+        weights_err_tol: None | float = 0.0001,
+    ) -> None:
+        """Re-parametrize mixture
+
+        Parameters
+        ----------
+        weights: list of floats or array
+            weights of components, must sum up to 1
+        components: list of distribution (instance objects)
+            distribution instance objects, each must inherit from AbstractDistribution,
+            list must have same number of elements as list of weights
+        weights_err_tol: None or float, default 0.0001
+            error tolerance during summing up list of weights,
+            because float addition has some inherent error (floating point error)
+
+        Returns
+        -------
+        None
+
+        Notes
+        -----
+        In the concrete implementation, it might be handy to implement:
+            weights /= sum(weights)
+        to scale weights such that weights sum up to 1
+        """
+        ...
+
+    @abstractmethod
+    def _sample_mixture(self, n_size: int) -> np.ndarray:
+        # this is the main sampling method, which has to be implemented by the user
+        ...
+
+    def sample(self, n_size: int) -> np.ndarray:
+        # this is only a wrapper around self._sample_mixture with an additional check beforehand
+
+        if self.weights is None or self.components is None:
+            raise InitializationError("weights and components not yet set.")
+
+        w_err_tol = 0 if self.weights_err_tol is None else self.weights_err_tol
+        if not np.isclose(np.sum(self.weights), 1, atol=w_err_tol):
+            # comparing floats is nasty (e.g. 3*0.1=0.30000000000000004 ant not 0.3)
+            #   so we need a little tolerance...
+            raise SumOfWeightsExceededError("self.weights must sum up to 1.")
+
+        return self._sample_mixture(n_size)
+
+    @property
+    @abstractmethod
+    def weights(self) -> list[float]:
+        ...
+
+    @property
+    @abstractmethod
+    def components(self) -> list[AbstractDistribution]:
+        ...
+
+    @property
+    @abstractmethod
+    def weights_err_tol(self) -> float:
+        ...
diff --git a/G_SPC/mixture/distribution/parametrize.py b/G_SPC/mixture/distribution/parametrize.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd621e3ca67f5194aa2e2b57ee7f063bf593b5bd
--- /dev/null
+++ b/G_SPC/mixture/distribution/parametrize.py
@@ -0,0 +1,659 @@
+from typing import Callable
+
+import numpy as np
+
+from G_SPC.exception import DistributionNotFeasibleError
+from G_SPC.mixture.distribution.interface import AbstractDistribution
+from G_SPC.normal_quantile import lower_quantile, upper_quantile
+
+
+def bi_parametric(
+    sample_fnc: Callable[[], tuple[float, float, AbstractDistribution]],
+    lower_limit: float,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+) -> tuple[float, float]:
+    """Generic parametrization logic for distributions with two parameters
+
+    Parameters
+    ----------
+    sample_fnc: callback function,
+        signature: sample_fnc() -> param_1, param_2, AbstractDistribution
+        sample_fnc should internally rely on factory_func_distrib, see beneath functions in this module.
+        This callback function implements the sampling logic and returns the parameters with which the
+        distribution shall be initialized, and the instantiated distribution itself.
+        This callback function takes no inputs. If a state shall be contained, then use a class method (the
+        class holding the corresponding state): see center_loc_scale().
+        Else, see weibull().
+        The order of param_1 and param_2 should be the same as the input arguments of factory_func_distrib.
+    lower_limit: float
+        Lower limit of the tolerance interval
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter
+
+    Returns
+    -------
+    param_1: float
+    param_2: float
+    """
+    # beneath line moved outside for loop to avoid
+    #   "variable might be assigned before reference" in the exception statement in last line
+    param_1, param_2, distrib = sample_fnc()
+
+    for _ in range(max_iter - 1):  # -1 because we take a first sample in above line
+        cdfs_ll = distrib.cdf(lower_limit)
+        cdfs_ul = distrib.cdf(upper_limit)
+
+        if cdfs_ll < lower_quantile(sigma_level) and cdfs_ul > upper_quantile(
+            sigma_level
+        ):
+            return param_1, param_2
+
+        param_1, param_2, distrib = sample_fnc()
+
+    raise DistributionNotFeasibleError(
+        f"Not possible to parametrize {distrib.parameters['type']} to given median (in case of a centered distribution)"
+        f" and specification limits within {max_iter} iterations."
+    )
+
+
+def bi_parametric_nok(
+    sample_fnc: Callable[[], tuple[float, float, AbstractDistribution]],
+    lower_limit: float,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+) -> tuple[float, float]:
+    """Generic parametrization logic for nok  distributions with two parameters
+
+    Parameters
+    ----------
+    sample_fnc: callback function,
+        signature: sample_fnc() -> param_1, param_2, AbstractDistribution
+        sample_fnc should internally rely on factory_func_distrib, see beneath functions in this module.
+        This callback function implements the sampling logic and returns the parameters with which the
+        distribution shall be initialized, and the instantiated distribution itself.
+        This callback function takes no inputs. If a state shall be contained, then use a class method (the
+        class holding the corresponding state): see center_loc_scale().
+        Else, see weibull().
+        The order of param_1 and param_2 should be the same as the input arguments of factory_func_distrib.
+    lower_limit: float
+        Lower limit of the tolerance interval
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter
+
+    Returns
+    -------
+    param_1: float
+    param_2: float
+    """
+    # beneath line moved outside for loop to avoid
+    #   "variable might be assigned before reference" in the exception statement in last line
+    param_1, param_2, distrib = sample_fnc()
+
+    for _ in range(max_iter - 1):  # -1 because we take a first sample in above line
+        cdfs_ll = distrib.cdf(lower_limit)
+        cdfs_ul = distrib.cdf(upper_limit)
+
+        if cdfs_ll > lower_quantile(sigma_level) or cdfs_ul < upper_quantile(
+            sigma_level
+        ):
+            return param_1, param_2
+
+        param_1, param_2, distrib = sample_fnc()
+
+    raise DistributionNotFeasibleError(
+        f"Not possible to parametrize a nok distribution within {max_iter} iterations."
+    )
+
+
+def center_loc_scale(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median: float,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    log_loc: bool = False,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "Normal", "LogNorm", "Logistic", "Cauchy" "Laplace" with centered loc
+
+    Parametrize distributions which have a loc and scale parameter.
+    Identifies the highest possible scale parameter for the centered distribution so that it fulfills the stability
+    criteria given by this paper. Starting point is the size of the tolerance interval. In each iteration the scale
+    is halved until the stability criteria are met.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take loc and scale as input arguments and must return a
+        distribution instance object (initialized accordingly), which inherits from AbstractDistribution
+        signature: factory_func_distrib(loc, scale) -> AbstractDistribution instance
+    lower_limit: float
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get a distribution fulfilling the stability requirements of the paper
+    log_loc: bool
+        Whether the median for the function is defined by loc or log(loc). log(log) if True.
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    loc: float,
+        if log_loc=True, this is the logarithmic loc
+    scale: float
+    """
+    scale = upper_limit - lower_limit
+    loc = median
+
+    if log_loc:
+        loc = np.log(loc)
+
+    class Sampler:
+        def __init__(self, init_loc: float, init_scale: float) -> None:
+            self._loc = init_loc
+            self._scale = init_scale
+
+        def sample(self) -> tuple[float, float, AbstractDistribution]:
+            self._scale /= 2
+            _distrib = factory_func_distrib(loc, self._scale)
+            return self._loc, self._scale, _distrib
+
+    sampler = Sampler(init_loc=loc, init_scale=scale)
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sampler.sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(
+            sampler.sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+
+
+def rnd_loc_scale(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    log_loc: bool = False,
+    rel_median_offset: float = 0.67,
+    min_rel_scale: float = 0.1,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "Normal", "LogNorm", "Logistic", "Cauchy" "Laplace" with random loc
+
+    Parametrize distributions which have a loc and scale parameter.
+    Using the maximal scale parameter of the centered distribution as initial scale, random loc and scale parameter for
+    a distribution are generated.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take loc and scale as input arguments and must return a
+        distribution instance object (initialized accordingly), which inherits from AbstractDistribution
+        signature: factory_func_distrib(loc, scale) -> AbstractDistribution instance
+    lower_limit: float
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get the distribution fulfilling the stability requirements of the paper
+    log_loc: bool
+        Whether the median for the function is defined by loc or log(loc). log(log) if True
+    rel_median_offset: float
+        Proportion of the maximal scale parameter (so that the centered distribution fulfills the stability criteria)
+        the mean is shifted as maximum
+    min_rel_scale: float
+        Minimal scale parameter of the resulting distribution expressed in the proportion of the maximal scale parameter
+        (so that the centered distribution fulfills the stability criteria)
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    loc: float,
+        if log_loc=True, this is the logarithmic loc
+    scale: float
+    """
+
+    _, init_scale = center_loc_scale(
+        factory_func_distrib,
+        lower_limit,
+        median,
+        upper_limit,
+        sigma_level,
+        max_iter,
+        log_loc,
+    )
+
+    def sample() -> tuple[float, float, AbstractDistribution]:
+        # this function is only for convenience
+
+        _loc = np.random.uniform(
+            median - rel_median_offset * init_scale,
+            median + rel_median_offset * init_scale,
+        )
+
+        if log_loc:
+            _loc = np.log(_loc)
+        if parametrize_nok:
+            _scale = np.random.uniform(init_scale * min_rel_scale, init_scale*1/min_rel_scale)
+        else:
+            _scale = np.random.uniform(init_scale * min_rel_scale, init_scale)
+        _distrib = factory_func_distrib(_loc, _scale)
+        return _loc, _scale, _distrib
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(sample, lower_limit, upper_limit, sigma_level, max_iter)
+
+
+def weibull(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    parametrize_centered: bool = True,
+    min_concentration: float = 0.1,
+    max_concentration: float = 100,
+    rel_offset: float = 0.1,
+    min_scale: float = 0.01,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "Weibull"
+
+    Setting random values of the scale parameter the correspondent shape parameter is calculated so that the
+    distribution has the correct median. If not parametrized centered a random offset is added to the shape parameter.
+    Repeated until a distribution fulfilling the stability criteria defined within the research paper is found ot the
+    maximal number of iterations is reached.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take concentration and scale as input arguments and must return a
+        distribution instance object (initialized accordingly, in this case it's only a Weibull distribution),
+        which inherits from AbstractDistribution
+        signature: factory_func_distrib(concentration, scale) -> AbstractDistribution instance (Weibull)
+    lower_limit: float, >= 0
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get the centered distribution fulfilling the stability requirements of the paper
+    parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+    min_concentration: float, > 0
+        Minimal value of the concentration parameter
+    max_concentration: float, > min_concentration
+        Maximal value of the concentration parameter
+    rel_offset: float, >= 0
+        Defines the maximal offset of the distribution by influencing the scale parameter.
+        Ignored if parametrized centered
+    min_scale: float, > 0
+        Minimal value of the scale parameter
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    concentration: float
+    scale: float
+    """
+    if lower_limit < 0:
+        raise ValueError("Weibull distribution only defined for positive values.")
+
+    def sample() -> tuple[float, float, AbstractDistribution]:
+        _concentration = np.random.uniform(min_concentration, max_concentration)
+
+        _upper_limit_scale = median / (np.log(2)) ** (1 / _concentration)
+        if not parametrize_centered:
+            _upper_limit_scale += np.random.uniform(
+                -(median - lower_limit) * rel_offset,
+                (upper_limit - median) * rel_offset,
+            )
+        _scale = max(min_scale, _upper_limit_scale)
+
+        _distrib = factory_func_distrib(_concentration, _scale)
+        return _concentration, _scale, _distrib
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(sample, lower_limit, upper_limit, sigma_level, max_iter)
+
+
+def pareto(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    parametrize_centered: bool = True,
+    max_shape: float = 10,
+    min_shape: float = 0.01,
+    rel_offset: float = 0.1,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "Pareto"
+
+    Setting random values of the scale parameter the correspondent shape parameter is calculated so that the
+    distribution has the correct median. If not parametrized centered a random offset is added to the shape parameter.
+    Repeated until a distribution fulfilling the stability criteria defined within the research paper is found ot the
+    maximal number of iterations is reached.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take concentration and scale as input arguments and must return a
+        distribution instance object (initialized accordingly, in this case it's only a Pareto distribution),
+        which inherits from AbstractDistribution
+        signature: factory_func_distrib(concentration, scale) -> AbstractDistribution instance (Pareto)
+    lower_limit: float, >= 0
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get the centered distribution fulfilling the stability requirements of the paper
+    parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+    max_shape: float, > 0
+        Maximal value of the shape parameter
+    min_shape: float, > 0
+        Minimal value of the shape parameter
+    rel_offset: float, >= 0
+        Defines the maximal offset of the distribution by influencing the scale parameter.
+        Ignored if parametrized centered
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    concentration: float
+    scale: float
+    """
+
+    def sample() -> tuple[float, float, AbstractDistribution]:
+        _shape = np.random.uniform(min_shape, max_shape)
+        _scale = median / (2 ** (1 / _shape))
+        if not parametrize_centered:
+            _scale += np.random.uniform(
+                -(median - lower_limit) * rel_offset,
+                (upper_limit - median) * rel_offset,
+            )
+        _distrib = factory_func_distrib(_scale, _shape)
+        return _scale, _shape, _distrib
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(sample, lower_limit, upper_limit, sigma_level, max_iter)
+
+
+def uniform(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    parametrize_centered: bool = True,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "Uniform"
+
+    Setting random values of the scale parameter. If not parametrized centered a random offset is added to the shape
+    parameter. Repeated until a distribution fulfilling the stability criteria defined within the research paper is
+    found ot the maximal number of iterations is reached.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take low and high as input arguments and must return a
+        distribution instance object (initialized accordingly, in this case it's only a Uniform distribution),
+        which inherits from AbstractDistribution
+        signature: factory_func_distrib(low, high) -> AbstractDistribution instance (Uniform)
+    lower_limit: float, >= 0
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get the centered distribution fulfilling the stability requirements of the paper
+    parametrize_centered: bool
+            True: parametrize s.t. distribution is centered w.r.t. specification limits
+            False: random left-right shift within specification limits, but still complies to process
+                stability criteria defined within the research paper
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    low: float
+    high: float
+    """
+
+    def sample() -> tuple[float, float, AbstractDistribution]:
+        max_scale = min(median - lower_limit, upper_limit - median)
+        scale = np.random.uniform(0, max_scale)
+
+        loc = (
+            median
+            if parametrize_centered
+            else np.random.uniform(median - max_scale, median + max_scale)
+        )
+
+        low = loc - scale
+        high = loc + scale
+
+        distrib = factory_func_distrib(low, high)
+        return low, high, distrib
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(sample, lower_limit, upper_limit, sigma_level, max_iter)
+
+
+def log_logistic_center(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median: float,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "LogLogistic" with centered loc
+
+    Identifies the lowest possible shape parameter for the centered distribution so that it fulfills the stability
+    criteria given by this paper. Starting point is the size of the tolerance interval. In each iteration the shape
+    is doubled until the stability criteria are met.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take loc and scale as input arguments and must return a
+        distribution instance object (initialized accordingly), which inherits from AbstractDistribution
+        signature: factory_func_distrib(loc, scale) -> AbstractDistribution instance
+    lower_limit: float
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get a distribution fulfilling the stability requirements of the paper
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    scale: float
+    shape: float
+    """
+    shape = upper_limit - lower_limit
+    scale = median
+
+    class Sampler:
+        def __init__(self, init_scale: float, init_shape: float) -> None:
+            self._shape = init_shape
+            self._scale = init_scale
+
+        def sample(self) -> tuple[float, float, AbstractDistribution]:
+            self._shape *= 2
+            _distrib = factory_func_distrib(scale, self._shape)
+            return self._scale, self._shape, _distrib
+
+    sampler = Sampler(init_scale=scale, init_shape=shape)
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sampler.sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(
+            sampler.sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+
+
+def rnd_log_logistic(
+    factory_func_distrib: Callable[[float, float], AbstractDistribution],
+    lower_limit: float,
+    median,
+    upper_limit: float,
+    sigma_level: float,
+    max_iter: int,
+    rel_median_offset: float = 0.67,
+    max_shape: float = 100,
+    parametrize_nok: bool = False,
+) -> tuple[float, float]:
+    """Parametrize "LogLogistic" with random loc
+
+    Using the minimal shape parameter of the centered distribution as initial shape, random shape and scale parameter
+    for a distribution are generated.
+
+    If parametrize_nok is True, the same logic is used to parametrize a distribution not fulfilling the stability
+    criteria.
+
+    Parameters
+    ----------
+    factory_func_distrib: callback function
+        factory_func_distrib must take loc and scale as input arguments and must return a
+        distribution instance object (initialized accordingly), which inherits from AbstractDistribution
+        signature: factory_func_distrib(loc, scale) -> AbstractDistribution instance
+    lower_limit: float
+        Lower limit of the tolerance interval
+    median: float
+        Median value given for the mixture distribution
+    upper_limit: float
+        Upper limit of the tolerance interval
+    sigma_level: float, > 0
+        Measure for which proportion of the probability density function has to be within the tolerance interval
+    max_iter: int, > 0
+        Maximal tries to get the distribution fulfilling the stability requirements of the paper
+    rel_median_offset: float
+        Proportion of the maximal scale parameter (so that the centered distribution fulfills the stability criteria)
+        the mean is shifted as maximum
+    max_shape: float
+        Maximal value for the shape parameter
+    parametrize_nok: bool
+        Whether an ok distribution (False) or a nok distribution (True) is to be parametrized.
+
+    Returns
+    -------
+    scale: float,
+    shape: float
+
+    """
+
+    _, init_shape = log_logistic_center(
+        factory_func_distrib, lower_limit, median, upper_limit, sigma_level, max_iter,
+    )
+
+    def sample() -> tuple[float, float, AbstractDistribution]:
+        # this function is only for convenience
+
+        _scale = np.random.uniform(
+            median - rel_median_offset * init_shape,
+            median + rel_median_offset * init_shape,
+        )
+
+        _shape = np.random.uniform(init_shape, max_shape)
+        _distrib = factory_func_distrib(_scale, _shape)
+        return _scale, _shape, _distrib
+
+    if parametrize_nok:
+        return bi_parametric_nok(
+            sample, lower_limit, upper_limit, sigma_level, max_iter
+        )
+    else:
+        return bi_parametric(sample, lower_limit, upper_limit, sigma_level, max_iter)
diff --git a/G_SPC/mixture/generator.py b/G_SPC/mixture/generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..001380f2686a35c43b4c8bce04ef66f5223d9baa
--- /dev/null
+++ b/G_SPC/mixture/generator.py
@@ -0,0 +1,687 @@
+# noinspection PyTupleAssignmentBalance
+"""
+Examples
+--------
+>>> generators = factory_oknok_cyclic_generators(...)
+>>> x, y, ... = generate_cycles(generators, ...)
+"""
+import warnings
+from typing import Any, Union
+
+import numpy as np
+from tqdm import tqdm
+from joblib import Parallel, delayed
+
+from G_SPC.exception import KMatrixError, SolvingError, ParameterError, GSPCError
+from G_SPC.mixture.distribution.impl import NpMixture
+from G_SPC.mixture.distribution.interface import AbstractDistribution, AbstractMixture
+from G_SPC.mixture.kmat.construction import merge_kmat, initialize_kmat
+from G_SPC.mixture.kmat.solution import solve_kmat_timestep
+
+
+def _check_kmat_shape(kmat: np.ndarray, nb_max: int, i_max: int):
+    """Check whether kmat shape is compliant."""
+    if kmat.shape != (nb_max, i_max):
+        raise KMatrixError(
+            f"Shape of kmat mismatching. Got {kmat.shape}. "
+            f"Needs {(nb_max, i_max)}. "
+            f"This is likely to a faulty parameter k_initial setting."
+        )
+
+
+def _parse_kmat(
+    nb_max: int,
+    k_initial: Union[dict[tuple[int, int], float], np.ndarray, None],
+    i_max: int,
+    is_centered_list: list[bool],
+) -> np.ndarray:
+    if isinstance(k_initial, np.ndarray):
+        return k_initial
+
+    k_initial = {} if k_initial is None else k_initial
+    return initialize_kmat(k_initial, nb_max, i_max, is_centered_list)
+
+
+class RampGenerator:
+    def __init__(
+        self,
+        lower_limit: float,
+        upper_limit: float,
+        median: float,
+        sigma: float,
+        ok_distribs: list[AbstractDistribution],
+        nok_distribs: list[AbstractDistribution],
+        mixture: Union[AbstractMixture, None],
+        revert: bool,
+        max_iter_parametrize: Union[None, int] = 1000,
+        nb_cores: int = None,
+    ) -> None:
+        """Initializes RampGenerator to generate synthetic data for the research paper
+
+        Using the generate function synthetic data for two phases (OK- and NOK-phase) are generated.
+        Within the OK-phase for each timestep a sample of size 1 is drawn from a mixture distribution fulfilling
+        the stability criteria defined in the research paper. This mixture distribution is generated by a weighted
+        combination of probability distributions (ok_distribs).
+        Within the NOK-phase the mixture distribution is superposed by one or multiple distributions not fulfilling
+        the stability criteria.
+        If revert == False, the OK-phase is followed by the NOK-phase. In this case, the weights of the NOK-
+        distributions are linearly increasing until reaching 1 / number of NOK-distributions at the last timestep.
+        The weights of the OK-distributions so that there sum added to the sum of the NOK-distributions equals 1 for
+        each timestep.
+        If revert == True, the NOK-phase is followed by the OK-phase. In this case the weights of the NOK-distributions
+        are ramping down during the NOK-phase reaching 0 at the first step of the OK-phase. The scaling is done the
+        same way as if revert == False.
+
+        Parameters
+        ----------
+        lower_limit: float
+            Lower limit of the tolerance interval
+        median: float
+            Median value given for the mixture distribution
+        upper_limit: float
+            Upper limit of the tolerance interval
+        sigma: float, > 0
+            Measure for which proportion of the probability density function has to be within the tolerance interval
+        ok_distribs: list[AbstractDistribution]
+            List of the probability distributions that shall fulfill the stability criteria. May be initialized without
+            parameters when generation is called with parametrize = True
+        nok_distribs: list[AbstractDistribution]
+            List of the probability distributions that do not fulfill the stability criteria. Have to be initialized
+            with concrete parameter values
+        mixture: Union[AbstractMixture, None]
+            The instance of the mixture distribution used to generate the data. Weights of the distributions are updated
+            every time step
+        max_iter_parametrize: int | None
+            max number of tries for parametrizing OK distributions. Within each try for the distribution to fit the
+            necessary parameters are generated once. Next iteration necessary if resulting distribution fails to
+            fulfill the stability criteria.Necessary when distributions initialized without
+            parameter values. See Notes.
+        nb_cores: int
+            Number of cores used for multi-threading. If not provided all available cores will be used.
+            Defaults to None.
+
+        Returns
+        -------
+        None
+
+        Notes
+        -----
+        **Parametrization**:
+        For centered distributions the parameters of the widest distribution fulfilling the stability criteria are
+        calculated (doubling/halving the parameter iterative). For non-centered distributions parameters are set on
+        a random basis fulfilling boundary conditions varying for the different distribution (for details see the
+        parametrize functions of the implementations of the distributions). The procedure is iterated until parameters
+        are found so that the resulting distribution fulfills the stability criteria or the maximum number of iterations
+        is reached. In latter case the programs exits with an error.
+        """
+        self.lower_limit: float = lower_limit
+        self.upper_limit: float = upper_limit
+        self.median: float = median
+        self.sigma: float = sigma
+        self.ok_distribs = ok_distribs
+        self.nok_distribs = nok_distribs
+        self.mixture = mixture if mixture is not None else NpMixture(None, None)
+        self.max_iter_parametrize = max_iter_parametrize
+        self.revert = revert
+        self.nb_cores = nb_cores or -1
+        # if intended initialize distributions randomly such that they comply with
+        #   process stability criteria defined in the paper
+        if max_iter_parametrize is not None:
+            args = (lower_limit, median, upper_limit, sigma, max_iter_parametrize)
+            for dist in self.ok_distribs:
+                dist.parametrize(*args)
+
+    def generate(
+        self,
+        t_inflection: int,
+        nb_max: int,
+        k_initial: Union[dict[tuple[int, int], float], np.ndarray, None],
+        revert: Union[bool, None] = False,
+        nb_retry: int = 5,
+    ) -> tuple[
+        np.ndarray,
+        np.ndarray,
+        np.ndarray,
+        list[list[dict[str, Any]]],
+        list[list[AbstractDistribution]],
+    ]:
+        """Function to generate synthetic time series data
+
+        Generates a time-series with data generated from a mixture distribution that is changed every time-step.
+        If revert == False:
+            Until t_inflection (OK-phase) only the distributions that fulfill the stability criteria defined in the
+            research paper (ok_distributions) are used to build a stable mixture distribution. For this an optimization
+            problem is solved to determine the weights of the different distributions in the mixture distribution.
+            The goal function is designed in a way that the weights of the distributions are as similar as possible.
+            Weights that are defined within k_initial are only changed when the optimization problem does not solve
+            successfully with the given weights. After t_inflection (NOK-phase) the weights of the OK distributions
+            are weighted such that the sum of the weights of the OK and NOK distributions equals one. The weights of
+            the NOK distributions ramp up during the NOK-phase reaching one divided by the number of NOK distributions
+            at the last time-step. This leads to a mixture distribution not fulfilling the stability criteria defined
+            in the research paper, assuming that the provided NOK distributions in fact are not fulfilling the stability
+            criteria.
+        Else:
+            The ordering of OK- NOK-phase is swapped
+
+        Parameters
+        ----------
+        t_inflection: int
+            inflection point, where NOK distributions starts to ramp up. t_inflection is 0-index based.
+            timestep t_inflection already counts to the NOK distributions itself (t_inflect inclusive
+            in NOK and exclusive to OK):
+                t_inflection = 3 --> [0, 1, 2] in OK and [3, 4, 5, ...] in NOK
+        nb_max: int
+            max time steps
+        k_initial: dict, np.ndarray or None
+            | Defines initial weights within the OK and for the OK-phase distributions within the NOK-phase that are not
+              changed within the weight optimization procedure. The weights of the OK distributions of the NOK-phase are
+              adjusted after the optimization procedure so that the sum of these weights and the weights of the NOK
+              distributions equals one. Using the np.ndarray option the values are directly given. Using the dictionary
+              option time-step, index of the (0-indexed) distribution and value have to be specified. Between two given
+              values for different time-steps of one distribution linear interpolation is performed. The interpolation
+              is inactive when there is a weight of the distribution set tp np.inf between the given values.
+              Then only the given values will be set.
+            | Be careful: do not define more weights as necessary as this narrows the search space and may cause the
+              optimization procedure to fail
+            | dict[Tuple[int, int], float] -> tuple of (time step, component index of ok_distribution) as key
+              and weight as value
+            | np.ndarray -> shape(t_max, number of ok_distributions)
+            | None -> as if empty dict
+        revert: bool
+            If True OK- and NOK-phase are swapped
+        nb_retry: int
+            number of tries for solving optimization problem until initialized weights are ignored
+
+        Returns
+        -------
+        x: np.ndarray, shape(t_max,)
+            synthetic data points (e.g. part measurements over time, 1D only)
+        y: np.ndarray, length t_max
+            corresponding boolean encoding to "x", OK=True --> x is within the tolerance interval, NOK=False --> x
+            is outside the tolerance interval
+        kmat: np.ndarray
+        distrib_param: list[list[dict[str, Any]]]
+            list of list of parameters of distributions per time step,
+            parameters stored in dict (parameters as used in __init__ of distributions)
+            ordering according to self.ok_distribs followed by self.nok_distribs
+        distribs: list[list[AbstractDistribution]]
+            List containing a list of the distribution objects for each time-step
+        """
+
+        if t_inflection <= 0:
+            raise GSPCError(
+                "t_inflection needs to be above 0 so that there are thw phases"
+            )
+
+        if t_inflection >= nb_max:
+            raise GSPCError(
+                "t_inflection needs to be lower than nb_max by at least 1 so that there are two phases"
+            )
+
+        # extract information which of the distributions are centered and which not
+        is_centered_list = [distrib.is_centered for distrib in self.ok_distribs]
+
+        revert = self.revert if revert is None else revert
+        if not revert:
+            return self._generate(
+                t_inflection, nb_max, k_initial, is_centered_list, nb_retry
+            )
+
+        # revert k_initial
+        i_max = len(self.ok_distribs)
+
+        kmat = _parse_kmat(nb_max, k_initial, i_max, is_centered_list)[::-1, :]
+        _check_kmat_shape(kmat, nb_max, i_max)
+
+        # revert t_inflection
+        t_inflection = nb_max - t_inflection
+
+        x, y, kmat, distrib_params, distribs = self._generate(
+            t_inflection, nb_max, kmat, is_centered_list, nb_retry
+        )
+
+        # revert results
+        x = x[::-1]
+        y = y[::-1]
+        kmat = kmat[::-1]
+        distrib_params.reverse()
+        distribs.reverse()
+
+        return x, y, kmat, distrib_params, distribs
+
+    def _generate(
+        self,
+        t_inflection: int,
+        nb_max: int,
+        k_initial: Union[dict[tuple[int, int], float], np.ndarray, None],
+        is_centered_list: list[bool],
+        nb_retry: int = 5,
+    ) -> tuple[
+        np.ndarray,
+        np.ndarray,
+        np.ndarray,
+        list[list[dict[str, Any]]],
+        list[list[AbstractDistribution]],
+    ]:
+        """
+
+        Parameters
+        ----------
+        t_inflection: int
+            inflection point, where NOK distributions starts to ramp up. t_inflection is 0-index based.
+            timestep t_inflection already counts to the NOK distributions itself (t_inflect inclusive
+            in NOK and exclusive to OK):
+                t_inflection = 3 --> [0, 1, 2] in OK and [3, 4, 5, ...] in NOK
+        nb_max: int
+            max time steps
+        k_initial: dict, np.ndarray or None
+            | Defines initial weights within the OK and for the OK-phase distributions within the NOK-phase that are not
+              changed within the weight optimization procedure. The weights of the OK distributions of the NOK-phase are
+              adjusted after the optimization procedure so that the sum of these weights and the weights of the NOK
+              distributions equals one. Using the np.ndarray option the values are directly given. Using the dictionary
+              option time-step, index of the (0-indexed) distribution and value have to be specified. Between two given
+              values for different time-steps of one distribution linear interpolation is performed. The interpolation
+              is inactive when there is a weight of the distribution set tp np.inf between the given values.
+              Then only the given values will be set.
+            | Be careful: do not define more weights as necessary as this narrows the search space and may cause the
+              optimization procedure to fail
+            | dict[Tuple[int, int], float] -> tuple of (time step, component index of ok_distribution) as key
+              and weight as value
+            | np.ndarray -> shape(t_max, number of ok_distributions)
+            | None -> as if empty dict
+        is_centered_list: bool
+            List indicating which of the distributions in the k-matrix are centered
+        nb_retry: int
+            number of tries for solving optimization problem until initialized weights are ignored
+
+        Returns
+        -------
+        x: np.ndarray, shape(t_max,)
+            synthetic data points (e.g. part measurements over time, 1D only)
+        ok_nok_y: np.ndarray, length t_max
+            corresponding boolean encoding to "x", OK=True, NOK=False
+        kmat: np.ndarray
+        distrib_param: list[list[dict[str, Any]]]
+            list of list of parameters of distributions per time step,
+            parameters stored in dict (parameters as used in __init__ of distributions)
+            ordering according to self.ok_distribs followed by self.nok_distribs
+        distribs: list[list[AbstractDistribution]]
+            List containing a list of the distribution objects for each time-step
+        """
+        i_max = len(self.ok_distribs)
+
+        kmat = _parse_kmat(nb_max, k_initial, i_max, is_centered_list)
+        _check_kmat_shape(kmat, nb_max, i_max)
+
+        # generate data
+        ok_x, ok_y, ok_kmat = self._generate_ok(kmat[:t_inflection], nb_retry)
+        nok_x, nok_y, nok_kmat = self._generate_nok(kmat[t_inflection:])
+
+        # merge data
+        x = np.concatenate((ok_x, nok_x), axis=0)  # 1D arrays
+        y = np.concatenate((ok_y, nok_y), axis=0)  # 1D arrays
+        kmat = merge_kmat(ok_kmat, nok_kmat)
+
+        distrib_params_ok = [
+            [distrib.parameters for distrib in self.ok_distribs]
+        ] * t_inflection
+
+        distrib_params_nok = [
+            [distrib.parameters for distrib in self.ok_distribs]
+            + [distrib.parameters for distrib in self.nok_distribs]
+        ] * (nb_max - t_inflection)
+
+        distrib_params = distrib_params_ok + distrib_params_nok
+
+        distribs = [self.ok_distribs + self.nok_distribs] * nb_max
+
+        return x, y, kmat, distrib_params, distribs
+
+    def _generate_ok(
+        self, kmat_ok: np.ndarray, nb_retry: int = 5
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+        """Generate data from OK distributions
+
+        Parameters
+        ----------
+        kmat_ok: np.ndarray
+            kmat with initial weights of the OK distributions from k_initial for the NOK phase
+            (should be cut to shape in advance, e.g. if not all entries of kmat shall be overwritten)
+        nb_retry: int
+            number of tries for solving optimization problem until initialized weights are ignored
+
+        Returns
+        -------
+        x: np.ndarray, shape(len(kmat),)
+            synthetic data points (e.g. part measurements over time, 1D only)
+        ok_y: np.ndarray, shape(len(kmat),)
+            corresponding boolean encoding to "x", OK=True, NOK=False
+        kmat: np.ndarray, same shape as input
+            kmat with the weights of the OK distributions during the OK-phase after solving the optimization problem
+        """
+
+        n_steps = len(kmat_ok)
+
+        # pre-allocate array, which is more efficient
+        x = np.zeros((n_steps,), dtype=float)
+        ok_y = np.zeros((n_steps,), dtype=bool)
+
+        kwargs = {
+            "ll": self.lower_limit,
+            "ul": self.upper_limit,
+            "h": self.median,
+            "sigma_level": self.sigma,
+            "distributions": self.ok_distribs,
+        }
+
+        def closure(kmat_ok, mixture, nb_retry, t, kwargs):
+            k_star = None
+            for i in range(nb_retry):
+                try:
+                    k_star = solve_kmat_timestep(
+                        k_mat=kmat_ok,
+                        timestep=t,
+                        **kwargs,
+                    )
+                except Exception as e:
+                    warnings.warn(
+                        f"Model did not solve successfully in iterartion {i}. Original traceback :{e}"
+                    )
+                if k_star is not None:
+                    break
+            if k_star is None:
+                warnings.warn(
+                    "The optimization model did not solve successfully for the current time-step with the "
+                    "given initial weights. Retrying without consideration of initial weights."
+                )
+                if len(kmat_ok.shape) == 2:
+                    kmat_ok[t] = np.array([np.NaN for _ in range(kmat_ok[t].size)])
+                elif len(kmat_ok.shape) == 1:
+                    kmat_ok[:] = np.NaN
+                else:
+                    raise ValueError(
+                        f"kmat_ok has to be 1 or 2 dimensional but {len(kmat_ok.shape)} are given."
+                    )
+
+                try:
+                    k_star = solve_kmat_timestep(
+                        k_mat=kmat_ok,
+                        timestep=t,
+                        **kwargs,
+                    )
+                except Exception as e:
+                    raise SolvingError(
+                        "With the given distributions the optimization problem is not solvable."
+                        f" Original Traceback: {e}"
+                    ) from e
+            kmat_ok_t = k_star
+            mixture.parametrize(
+                weights=k_star, components=kwargs["distributions"], weights_err_tol=None
+            )
+            x_t = mixture.sample(1)
+            # True --> OK = within limits
+            ok_y_t = kwargs["ul"] > x_t > kwargs["ll"]
+            return x_t, ok_y_t, kmat_ok_t
+
+        results = Parallel(n_jobs=self.nb_cores)(
+            delayed(closure)(kmat_ok, self.mixture, nb_retry, t, kwargs)
+            for t in tqdm(range(n_steps))
+        )
+
+        kmat_ok = np.zeros((n_steps, len(self.ok_distribs)), dtype=float)
+        for t in range(len(results)):
+            x[t] = results[t][0]
+            ok_y[t] = results[t][1]
+            kmat_ok[t] = results[t][2]
+
+        return x, ok_y, kmat_ok
+
+    def _inflected_kmat(self, n_steps: int, kmat: np.ndarray) -> np.ndarray:
+        """Create k matrix for the NOK time-steps
+
+        First creates k matrix for the OK distributions for the NOK time-steps with weights adding up to one.
+        The weights for the NOK distributions for the NOK time-steps linearly increase from zero to one divided by the
+        number of NOK distributions. The weights of the k matrix for each time step are afterwards weighted with one
+        minus the sum of the weights of the NOK distributions respective time-step. Finally, the weights of the OK and
+        NOK distributions are horizontally concatenated.
+
+        Parameters
+        ----------
+        n_steps: int
+            Number of rows of the inflected k matrix. Equals the number of NOK time-steps
+        kmat: np.ndarray
+            k matrix with the initial weights
+
+        Returns
+        -------
+        np.ndarray
+            The k matrix for the NOK time-steps
+        """
+        # Solve optimization problem to get weights of the OK distributions adding up to one
+        _, _, kmat_ok = self._generate_ok(kmat)
+        # shape=kmat.shape -> (nb nok steps, nb ok distribs)
+
+        i_max = len(self.nok_distribs)
+
+        # define increasing weights of the NOK distributions
+        # start at 0
+        start_vec = np.zeros((i_max,))
+        # target: steadily increase until all NOK weights cumulatively take 100% of the weights
+        end_vec = np.full((i_max,), 1 / i_max)
+        # first value is dismissed as NOK phase starts with weights of the NOK distributions already being higher than
+        # zero
+        nok_weights = np.linspace(start_vec, end_vec, n_steps + 1)[1:, :]
+        # shape=(n_steps, nb nok distribs)
+
+        # weight the weights of the OK distributions so that the weights of OK and NOK distributions add up to one
+        ok_factors = np.ones((nok_weights.shape[0],)) - np.sum(nok_weights, axis=1)
+        ok_factors = ok_factors[:, np.newaxis]  # promote from row vec to column vec
+        ok_weights = kmat_ok * ok_factors
+
+        # concatenate weights of the OK and NOK distributions
+        # return np.hstack((kmat_ok_distribs_weighted, nok_weights))
+        infl_kmat = np.hstack((ok_weights, nok_weights))
+
+        _check_kmat_shape(infl_kmat, n_steps, kmat_ok.shape[1] + i_max)
+        return infl_kmat
+
+    def _generate_nok(
+        self, kmat_nok: np.ndarray
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+        """Generate data from NOK distributions
+
+        Parameters
+        ----------
+        kmat_nok: np.ndarray
+            kmat with initial weights of the OK distributions from k_initial for the NOK phase
+            (should be cut to shape in advance, e.g. if not all entries of kmat shall be overwritten)
+
+        Returns
+        -------
+        x: np.ndarray, shape(n_steps,)
+            synthetic data points (e.g. part measurements over time, 1D only)
+        nok_y: np.ndarray, length n_steps
+            corresponding boolean encoding to "x", OK=True, NOK=False
+        infl_kmat: np.ndarray, shape(n_steps, number self.ok_distribs + number self.nok_distribs)
+            Weights matrix after inflection. Separate weight matrix which is only used for the
+            NOK-phase.
+        """
+        n_steps = len(kmat_nok)
+
+        # pre-allocate array, which is more efficient
+        x = np.zeros((n_steps,), dtype=float)
+        nok_y = np.zeros((n_steps,), dtype=bool)
+
+        infl_kmat = self._inflected_kmat(n_steps, kmat_nok)
+
+        kwargs = {
+            "ll": self.lower_limit,
+            "ul": self.upper_limit,
+            "h": self.median,
+            "sigma_level": self.sigma,
+            "ok_distributions": self.ok_distribs,
+            "nok_distributions": self.nok_distribs,
+            "mixture": self.mixture,
+        }
+
+        def closure(infl_kmat, t, kwargs):
+            kwargs["mixture"].parametrize(
+                infl_kmat[t, :],
+                kwargs["ok_distributions"] + kwargs["nok_distributions"],
+                weights_err_tol=None,
+            )
+            x = kwargs["mixture"].sample(1)
+
+            # True --> OK = within limits, False --> NOK = outside limits
+            nok_y = kwargs["ul"] > x > kwargs["ll"]
+
+            return x, nok_y
+
+        results = Parallel(n_jobs=self.nb_cores)(
+            delayed(closure)(infl_kmat, t, kwargs) for t in range(n_steps)
+        )
+        for t in range(len(results)):
+            x[t] = results[t][0]
+            nok_y[t] = results[t][1]
+        return x, nok_y, infl_kmat
+
+
+def _check_cycle_args(
+    generators: list[RampGenerator],
+    k_initial_list: list[Union[dict[tuple[int, int], float], np.ndarray, None]],
+    t_inflection_list: list[int],
+    nb_steps_list: list[int],
+) -> None:
+
+    if not (
+        len(generators)
+        == len(t_inflection_list)
+        == len(nb_steps_list)
+        == len(k_initial_list)
+    ):
+        raise ParameterError(
+            "Length of generators t_inflection_list, nb_steps_list and k_initial_list need to be"
+            "equal. But provided length differ"
+        )
+
+
+def generate_cycles(
+    generators: list[RampGenerator],
+    k_initial_list: list[Union[dict[tuple[int, int], float], np.ndarray, None]],
+    t_inflection_list: list[int],
+    nb_steps_list: list[int],
+) -> tuple[
+    np.ndarray,
+    np.ndarray,
+    np.ndarray,
+    list[list[dict[str, Any]]],
+    list[list[AbstractDistribution]],
+]:
+    """Function to generate cyclic synthetic data.
+
+    To generate cyclic synthetic data with alternating OK- and NOK-phases each cycle consists of two ramps.
+    The first ramp of each cycle starts with an OK-phase followed by ramping up NOK-distributions within the
+    NOK-phase. The second ramp of each cycle starts with a NOK-phase ramping down to an OK-phase.
+    Between the first and second ramp of a cycle the NOK distributions are not changed. The same holds between
+    the second ramp of a cycle and the first ramp of the next cycle for the OK distributions.
+
+    Parameters
+    ----------
+    generators: list[RampGenerator]
+        list of RampGenerators used to realize the different cycles within the data generation
+    k_initial_list: list[dict, np.ndarray or None]
+        List of initial weights for the different ramps during cyclic data generation. For each ramp holds:
+        | Defines initial weights within the OK and for the OK-phase distributions within the NOK-phase that are not
+          changed within the weight optimization procedure. The weights of the OK distributions of the NOK-phase are
+          adjusted after the optimization procedure so that the sum of these weights and the weights of the NOK
+          distributions equals one. Using the np.ndarray option the values are directly given. Using the dictionary
+          option time-step, index of the (0-indexed) distribution and value have to be specified. Between two given
+          values for different time-steps of one distribution linear interpolation is performed. The interpolation
+          is inactive when there is a weight of the distribution set tp np.inf between the given values.
+          Then only the given values will be set.
+        | Be careful: do not define more weights as necessary as this narrows the search space and may cause the
+          optimization procedure to fail
+        | dict[Tuple[int, int], float] -> tuple of (time step, component index of ok_distribution) as key
+          and weight as value
+        | np.ndarray -> shape(t_max, number of ok_distributions)
+        | None -> as if empty dict
+    t_inflection_list: list[int]
+        List of the inflection points for the different ramps. For each ramp holds:
+        inflection point, where NOK distributions starts to ramp up. t_inflection is 0-index based.
+        timestep t_inflection already counts to the NOK distributions itself (t_inflect inclusive
+        in NOK and exclusive to OK):
+            t_inflection = 3 --> [0, 1, 2] in OK and [3, 4, 5, ...] in NOK
+    nb_steps_list: list[int]
+        List of the number of time steps for each ramp during cyclic data generation.
+
+
+    Returns
+    -------
+
+    """
+    _check_cycle_args(generators, k_initial_list, t_inflection_list, nb_steps_list)
+
+    x = np.array([], dtype=np.float64)
+    y = np.array([], dtype=np.float64)
+    kmat = np.empty((0, 0))
+    distrib_params = []
+    distribs = []
+
+    for i, (gen_i, t_infl_i, nb_i, k_i) in enumerate(
+        tqdm(zip(generators, t_inflection_list, nb_steps_list, k_initial_list))
+    ):
+        x_i, y_i, kmat_i, d_params_i, distribs_i = gen_i.generate(
+            t_inflection=t_infl_i, nb_max=nb_i, k_initial=k_i
+        )
+        x = np.concatenate((x, x_i), axis=0)
+        y = np.concatenate((y, y_i), axis=0)
+        kmat = merge_kmat(kmat, kmat_i)
+        distrib_params.extend(d_params_i)
+        distribs.extend(distribs_i)
+
+    return x, y, kmat, distrib_params, distribs
+
+
+def factory_oknok_cyclic_generators(
+    lower_limit: float,
+    upper_limit: float,
+    median: float,
+    sigma: float,
+    ok_distribs_list: list[list[AbstractDistribution]],
+    nok_distribs_list: list[list[AbstractDistribution]],
+    mixture: AbstractMixture,
+    max_iter_parametrize: int = 1000,
+) -> [list[RampGenerator]]:
+
+    generators = []
+
+    for i in range(len(ok_distribs_list)):
+
+        if i == 0:
+            parsed_ok_distrib = ok_distribs_list[i]
+            parsed_nok_distrib = nok_distribs_list[i]
+            parsed_revert = False
+        elif i % 2 == 0:
+            parsed_ok_distrib = ok_distribs_list[i - 1]
+            parsed_nok_distrib = nok_distribs_list[i // 2]
+            parsed_revert = False
+        else:
+            parsed_ok_distrib = ok_distribs_list[(i + 1) // 2]
+            parsed_nok_distrib = nok_distribs_list[(i - 1) // 2]
+            parsed_revert = True
+
+        # noinspection PyNoneFunctionAssignment
+        gen_i = RampGenerator(
+            lower_limit=lower_limit,
+            upper_limit=upper_limit,
+            median=median,
+            sigma=sigma,
+            ok_distribs=parsed_ok_distrib,
+            nok_distribs=parsed_nok_distrib,
+            max_iter_parametrize=max_iter_parametrize,
+            mixture=mixture,
+            revert=parsed_revert,
+        )
+        generators.append(gen_i)
+
+    return generators
diff --git a/G_SPC/mixture/kmat/__init__.py b/G_SPC/mixture/kmat/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/G_SPC/mixture/kmat/construction.py b/G_SPC/mixture/kmat/construction.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdb148e387dc5a768171cb639d5f5abb3461b50d
--- /dev/null
+++ b/G_SPC/mixture/kmat/construction.py
@@ -0,0 +1,244 @@
+from typing import Tuple
+import warnings
+
+import numpy as np
+from scipy.interpolate import interp1d
+
+from G_SPC.exception import SumOfWeightsExceededError
+
+
+def check_kmat_weights(k: np.ndarray, is_centered_list: list[bool]) -> None:
+    """Checks whether for each row of k the weights fulfill the requirements:
+    - sum of weights < 1
+    - sum of weights of non-centered distributions < 0.49
+    - sum of weights of non-centered distributions < (1- sum of weights of all centered distributions)/2
+
+    Parameters
+    ----------
+    k: np.ndarray
+        Weight matrix
+    is_centered_list: bool
+        List indicating which of the distributions in the k-matrix are centered
+    Returns
+    -------
+    None
+    """
+    if (row_sum_exceeded := np.argwhere(np.nansum(k, axis=1) >= 1)).any():
+        raise SumOfWeightsExceededError(
+            f"Row sum is too large for time steps: {row_sum_exceeded.flatten()}"
+        )
+
+    k_non_centered = np.delete(k, np.where(is_centered_list)[0], axis=1)
+    k_centered = k[:, np.where(is_centered_list)[0]]
+    if (
+        row_sum_exceeded := np.argwhere(np.nansum(k_non_centered, axis=1) > 0.49)
+    ).any():
+        raise SumOfWeightsExceededError(
+            f"Row sum of non-centered distributions is too large for time steps: {row_sum_exceeded.flatten()}"
+        )
+
+    if (
+        row_sum_exceeded := np.argwhere(
+            np.nansum(k_non_centered, axis=1)
+            > (0.99 - np.nansum(k_centered, axis=1)) / 2
+        )
+    ).any():
+        raise SumOfWeightsExceededError(
+            f"Row sum of non-centered distributions considering the sum of weights for the centered distributions is "
+            f"too large for time steps: {row_sum_exceeded.flatten()}"
+        )
+
+
+def interpol_kmat(k_mat: np.ndarray, is_centered_list: list[bool]) -> np.ndarray:
+    """Fills the weight matrix column-wise with interpolated values between the fixed initial weights.
+    If there is a np.inf between two values, no interpolation is performed.
+
+    Parameters
+    ----------
+    k_mat: np.ndarray
+        Weight matrix to interpolate within
+    is_centered_list: bool
+        List indicating which of the distributions in the k-matrix are centered
+
+    Returns
+    -------
+    np.array:
+        The weight matrix after interpolation
+    """
+    for i in range(k_mat.shape[1]):  # k_mat.shape[1] --> i_max
+        k_mat = _interpol_column(k_mat, i)
+
+    k_mat[k_mat == np.inf] = np.nan
+
+    check_kmat_weights(k_mat, is_centered_list)
+
+    return k_mat
+
+
+def fill_kmat(
+    k_initial: dict[Tuple[int, int], float], t_max: int, i_max: int
+) -> np.ndarray:
+    """Builds the weight matrix based on the fixed initial values.
+
+    Parameters
+    ----------
+    k_initial: dict[Tuple[int, int], float]
+        The fixed initial values
+    t_max: int
+        Number of time steps for which the weights should be calculated
+    i_max: int
+        Number of distributions to generate the mixture distribution from
+
+    Returns
+    -------
+    np.ndarray
+        The weight matrix
+    """
+    # initialize matrix with type np.nan (size: t_max x i_max) and insert initial k values.
+    k = np.full([t_max, i_max], np.nan)
+
+    if not k_initial:
+        return k
+
+    # extract indices out of k_initial dict_keys to use as broadcasting index for k array
+    k_idx = tuple(np.array([*np.fromiter(k_initial.keys(), dtype=tuple)]).T)
+    try:
+        k[k_idx] = np.fromiter(k_initial.values(), dtype=float)
+    except IndexError:
+        warnings.warn(
+            "At least one given index is out-of-bound of the weights matrix and therefore k"
+            " initial is ignored."
+        )
+
+    return k
+
+
+def _start_end_rows_interpol(k: np.ndarray, col: int) -> list[tuple[int, int]]:
+    """Determines for the given column based on the k matrix with the initialized weights the indices
+    of the weights to be interpolated between. Interpolation is to be done between initialized values,
+    if no np.inf is initialized between them (intentional option to disable interpolation).
+
+    Parameters
+    ----------
+    k: np.ndarray
+        The weight matrix with initial weights
+    col: int
+        The column to interpolate within
+
+    Returns
+    -------
+    list[tuple[int, int]]
+        List of index pairs between whose values to interpolate
+    """
+
+    # get time step of each float and np.inf value in column i of k
+    t_inf = np.where(np.isinf(k[:, col]))[0]
+    t_float = np.where(~np.isinf(k[:, col]) & ~np.isnan(k[:, col]))[0]
+
+    # put the float time steps together pairwise
+    t_pair = [(t_float[j], t_float[j + 1]) for j in range(len(t_float) - 1)]
+
+    # for each pair check whether there is a np.inf between, only keep pairs where no np.inf is between
+    mask = np.array(
+        [any(pair[0] < j < pair[1] for j in t_inf) for pair in t_pair], dtype=bool
+    )
+
+    return np.array(t_pair)[~mask]
+
+
+def _interpol_column(k: np.ndarray, col: int) -> np.ndarray:
+    """Linear interpolation between the values given by the initial weights for the given column of the weight matrix.
+
+    Parameters
+    ----------
+    k: np.ndarray
+        Weight matrix
+    col: int
+        Index of the column to interpolate within
+
+    Returns
+    -------
+    np.ndarray
+        Weight matrix with interpolated values for the given column
+    """
+    for start_row, end_row in _start_end_rows_interpol(k, col):
+        x_interp = [start_row, end_row]
+        y_interp = [k[start_row, col], k[end_row, col]]
+        f = interp1d(x_interp, y_interp)
+        interpolation_points = np.linspace(
+            start_row, end_row, end_row - start_row, endpoint=False
+        )
+        mask = f(interpolation_points)[1:]
+        k[start_row + 1 : end_row, col] = mask
+
+    return k
+
+
+def merge_kmat(kmat_1: np.ndarray, kmat_2: np.ndarray) -> np.ndarray:
+    """Concatenates two matrices vertically. The column number equals that of the matrix with more columns.
+    The missing columns of the  matrix with fewer columns are filled with zeros.
+
+    Parameters
+    ----------
+    kmat_1: np.ndarray, shape(m_1, n_1)
+        First matrix.
+    kmat_2: np.ndarray, shape(m_2, n_2)
+        Second matrix with n_2 >= n_1.
+
+    Returns
+    -------
+    np.ndarray, shape(m_1 + m_2, n_2)
+        Concatenated matrix.
+    """
+    # example:
+    # >>> a
+    # array([[1, 2],
+    #        [3, 4],
+    #        [5, 6]])
+    # >>> b
+    # array([[ 7,  8,  9, 10],
+    #        [11, 12, 13, 14]])
+    # >>> merge_kmat(a, b)
+    # array([[ 1.,  2.,  0.,  0.],
+    #        [ 3.,  4.,  0.,  0.],
+    #        [ 5.,  6.,  0.,  0.],
+    #        [ 7.,  8.,  9., 10.],
+    #        [ 11., 12., 13., 14.]])
+
+    nb_rows = kmat_1.shape[0] + kmat_2.shape[0]
+    nb_cols = max(kmat_1.shape[1], kmat_2.shape[1])
+
+    kmat = np.zeros((nb_rows, nb_cols))
+
+    kmat[: kmat_1.shape[0], : kmat_1.shape[1]] = kmat_1
+    kmat[-kmat_2.shape[0] :, : kmat_2.shape[1]] = kmat_2
+
+    return kmat
+
+
+def initialize_kmat(
+    k_initial: dict[tuple[int, int], float],
+    t_max: int,
+    i_max: int,
+    is_centered_list: list[bool],
+) -> np.ndarray:
+    """Stack :py:func:`fill_kmat` (first) and :py:func:`interpol_kmat` (second)
+    to initialize a kmat from k_init dict
+
+    Parameters
+    ----------
+    k_initial: dict[Tuple[int, int], float]
+        The fixed initial values
+    t_max: int
+        Number of time steps for which the weights should be calculated
+    i_max: int
+        Number of distributions to generate the mixture distribution from
+    is_centered_list: bool
+        List indicating which of the distributions in the k-matrix are centered
+
+    Returns
+    -------
+    np.ndarray:
+        The weight matrix
+    """
+    return interpol_kmat(fill_kmat(k_initial, t_max, i_max), is_centered_list)
diff --git a/G_SPC/mixture/kmat/solution.py b/G_SPC/mixture/kmat/solution.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ffcca12138d4ff926ffcab89f67bdd1b4f7c888
--- /dev/null
+++ b/G_SPC/mixture/kmat/solution.py
@@ -0,0 +1,392 @@
+import gc
+
+import attrs
+import numpy as np
+from docplex.mp.dvar import Var
+from docplex.mp.model import Model
+
+from G_SPC.mixture.distribution.interface import AbstractDistribution
+from G_SPC.normal_quantile import lower_quantile, upper_quantile
+
+
+@attrs.define
+class ProblemParameters:
+    """Dataclass for data exchange only within this module in order to cluster parameters"""
+
+    k_fix: np.ndarray
+    """The fixed weights, treated as constants within the optimization problem"""
+
+    cdfs_ll_var: np.ndarray
+    """The values of the cumulative density function of the probability distributions which weights are to be optimized 
+    at the lower limit of the tolerance interval"""
+
+    cdfs_median_var: np.ndarray
+    """The values of the cumulative density function of the probability distributions which weights are to be optimized 
+    at the expected median of the mixture distribution"""
+
+    cdfs_ul_var: np.ndarray
+    """The values of the cumulative density function of the probability distributions which weights are to be optimized 
+    at the upper limit of the tolerance interval"""
+
+    sum_k_fix: float
+    """Sum of the fixed weights"""
+
+    lower_fix: float
+    """Sum of the values of the cumulative density functions of the probability distributions which weights fixed 
+    at the lower limit of the tolerance interval weighted with the fixed weights"""
+
+    upper_fix: float
+    """Sum of the values of the cumulative density functions of the probability distributions which weights fixed 
+    at the upper limit of the tolerance interval weighted with the fixed weights"""
+
+    median_fix: float
+    """Sum of the values of the cumulative density functions of the probability distributions which weights fixed 
+    at the the expected median of the mixture distribution weighted with the fixed weights"""
+
+    n_fix: int
+    """Number of fixed weights"""
+
+    n_var: int
+    """Number of weights that are to be optimized"""
+
+    fix_ind: list[int]
+    """Indices of the fixed weights"""
+
+    var_ind: list[int]
+    """Indices of the variable weights"""
+
+    sigma_level: float
+    """Defines the proportion of values of the mixture distribution that has to be within the tolerance limits"""
+
+
+def _compute_problem_params(
+    k_mat: np.ndarray,
+    timestep: int,
+    ll: float,
+    ul: float,
+    h: float,
+    sigma_level: float,
+    distributions: list[AbstractDistribution],
+) -> ProblemParameters:
+
+    k_fix, k_var, _ = _extract_weights_from_k_matrix(k_mat, timestep)
+    fix_ind, var_ind = _extract_indices_of_weights(k_mat, timestep)
+
+    # Calculate the numbers of fixed weights (handled as constants within the optimization problem)
+    # and the weights to be optimized
+    n_fix, n_var = (k_fix.size, k_var.size)
+
+    cdfs_ll, cdfs_median, cdfs_ul = _calculate_cumulative_density_values(
+        distributions, h, ll, ul
+    )
+
+    (
+        cdfs_ll_fix,
+        cdfs_ll_var,
+        cdfs_median_fix,
+        cdfs_median_var,
+        cdfs_ul_fix,
+        cdfs_ul_var,
+    ) = _extract_cumulative_density_values_variable_weights(
+        cdfs_ll, cdfs_median, cdfs_ul, fix_ind, var_ind
+    )
+
+    (
+        lower_fix,
+        median_fix,
+        sum_k_fix,
+        upper_fix,
+    ) = _calculate_weighted_sums_for_fixed_weights(
+        cdfs_ll_fix, cdfs_median_fix, cdfs_ul_fix, k_fix
+    )
+
+    return ProblemParameters(
+        k_fix=k_fix,
+        cdfs_ll_var=cdfs_ll_var,
+        cdfs_median_var=cdfs_median_var,
+        cdfs_ul_var=cdfs_ul_var,
+        sum_k_fix=sum_k_fix,
+        lower_fix=lower_fix,
+        upper_fix=upper_fix,
+        median_fix=median_fix,
+        n_fix=n_fix,
+        n_var=n_var,
+        fix_ind=fix_ind,
+        var_ind=var_ind,
+        sigma_level=sigma_level,
+    )
+
+
+def _extract_indices_of_weights(
+    k_mat: np.ndarray, timestep: int
+) -> tuple[np.ndarray, np.ndarray]:
+    # Extracts the indices for the fixed and variable weights for matching after optimization
+    if len(k_mat.shape) == 2:
+        k_mat_row = k_mat[timestep]
+    elif len(k_mat.shape) == 1:
+        k_mat_row = k_mat
+    else:
+        raise ValueError(
+            f"k_mat needs to be 1 or 2 dimensional but was given with {len(k_mat.shape)} dimensions"
+        )
+    var_ind = np.where(np.isnan(k_mat_row))[0]
+    fix_ind = np.where(~np.isnan(k_mat_row))[0]
+    return fix_ind, var_ind
+
+
+def _extract_weights_from_k_matrix(
+    k_mat: np.ndarray, timestep: int
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+    # Extracts the fixed and variable weights from the k_matrix matrix for the given timestep
+    if len(k_mat.shape) == 2:
+        k_mat_row = k_mat[timestep]
+    elif len(k_mat.shape) == 1:
+        k_mat_row = k_mat
+    else:
+        raise ValueError(
+            f"k_mat needs to be 1 or 2 dimensional but was given with {len(k_mat.shape)} dimensions"
+        )
+    k_var = k_mat_row[np.isnan(k_mat_row)]
+    k_fix = k_mat_row[~np.isnan(k_mat_row)]
+    return k_fix, k_var, k_mat_row
+
+
+def _extract_cumulative_density_values_variable_weights(
+    cdfs_ll: list[float],
+    cdfs_median: list[float],
+    cdfs_ul: list[float],
+    fix_ind: int,
+    var_ind: int,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+    # Extracts the cumulative density values of the distributions associated with the weights to be optimized.
+    cdfs_ll_var, cdfs_median_var, cdfs_ul_var = (
+        np.array(i)[var_ind] for i in [cdfs_ll, cdfs_median, cdfs_ul]
+    )
+    cdfs_ll_fix, cdfs_median_fix, cdfs_ul_fix = (
+        np.array(i)[fix_ind] for i in [cdfs_ll, cdfs_median, cdfs_ul]
+    )
+    return (
+        cdfs_ll_fix,
+        cdfs_ll_var,
+        cdfs_median_fix,
+        cdfs_median_var,
+        cdfs_ul_fix,
+        cdfs_ul_var,
+    )
+
+
+def _calculate_cumulative_density_values(
+    distributions: list[AbstractDistribution], h: float, ll: float, ul: float
+) -> tuple[list[float], list[float], list[float]]:
+    # Calculates the values of the cumulative density function of the distributions at the lower and upper limit of
+    # the tolerance interval as well as at the expected median of the mixture distribution.
+    cdfs_ll = [distribution.cdf(ll) for distribution in distributions]
+    cdfs_median = [distribution.cdf(h) for distribution in distributions]
+    cdfs_ul = [distribution.cdf(ul) for distribution in distributions]
+    return cdfs_ll, cdfs_median, cdfs_ul
+
+
+def _calculate_weighted_sums_for_fixed_weights(
+    cdfs_ll_fix: np.ndarray,
+    cdfs_median_fix: np.ndarray,
+    cdfs_ul_fix: np.ndarray,
+    k_fix: np.ndarray,
+) -> tuple[float, float, float, float]:
+    # Calculates the sum of the fixed weights as well as the sum of the values of the cumulative density functions
+    # of the distributions at the lower and upper limit of the tolerance interval as well as at the expected median
+    # of the mixture distribution weighted with the respective fixed weight.
+    sum_k_fix = sum(k_fix)
+    lower_fix = sum(k_fix * cdfs_ll_fix)
+    upper_fix = sum(k_fix * cdfs_ul_fix)
+    median_fix = sum(k_fix * cdfs_median_fix)
+    return lower_fix, median_fix, sum_k_fix, upper_fix
+
+
+def solve_kmat_timestep(
+    k_mat: np.ndarray,
+    timestep: int,
+    ll: float,
+    ul: float,
+    h: float,
+    sigma_level: float,
+    distributions: list[AbstractDistribution],
+) -> np.ndarray:
+    """Calculates the optimal weights of the distributions within the mixture distribution to generate.
+    Predefined initial weights are left unchanged. Constraints considered: sum of weights equals 1,
+    requested percentage of data between the lower and upper limit of the tolerance interval, median of the
+    mixture distribution equals the requested value.
+
+    Parameters
+    ----------
+    k_mat: np.ndarray
+        Weight matrix with the predefined initial weights
+    timestep: int
+        Timestep for which to calculate the optimal values
+    ll: float
+        Lower limit of the tolerance interval
+    ul: float
+        Upper limit of the tolerance interval
+    h: float
+        Median of the mixture distribution
+    sigma_level: float
+        Defines the minimum percentage of data within the tolerance limits expected for the mixture distribution
+    distributions: list[DistributionProtocol]
+        Distributions from which the mixture distribution should be built
+
+    Returns
+    -------
+    np.ndarray
+        Optimal weights of the probability distributions within the mixture distribution for the given timestep
+    """
+
+    problem_parameters = _compute_problem_params(
+        k_mat, timestep, ll, ul, h, sigma_level, distributions
+    )
+
+    # solve the optimization problem
+    model = Model()
+    var = _model_variables(model, problem_parameters)
+    model = _model_constraints(model, var, problem_parameters)
+    model = _model_objective(model, var, problem_parameters)
+    model.solve()
+    k_star_opt = np.array([i.solution_value for i in var], dtype=np.float32)
+    del model, var
+    gc.collect()
+
+    # concatenate weights
+    k_star = np.zeros(problem_parameters.n_fix + problem_parameters.n_var)
+    k_star[problem_parameters.var_ind] = k_star_opt
+    k_star[problem_parameters.fix_ind] = problem_parameters.k_fix
+    return k_star
+
+
+def _model_variables(model: Model, problem_parameters: ProblemParameters) -> list[Var]:
+    # add decision variables to optimization model
+    return model.continuous_var_list(
+        problem_parameters.n_var, name="k_matrix", lb=0, ub=1
+    )
+
+
+def _model_constraints(
+    model: Model, k_mod_var: list[Var], problem_parameters: ProblemParameters
+) -> Model:
+
+    (
+        n_var,
+        sum_k_fix,
+        cdfs_ll_var,
+        cdfs_median_var,
+        cdfs_ul_var,
+        lower_fix,
+        median_fix,
+        upper_fix,
+        sigma_level,
+    ) = (
+        problem_parameters.n_var,
+        problem_parameters.sum_k_fix,
+        problem_parameters.cdfs_ll_var,
+        problem_parameters.cdfs_median_var,
+        problem_parameters.cdfs_ul_var,
+        problem_parameters.lower_fix,
+        problem_parameters.median_fix,
+        problem_parameters.upper_fix,
+        problem_parameters.sigma_level,
+    )
+
+    # add constraints to optimization model
+    model = _sum_of_weights_constraint(k_mod_var, model, n_var, sum_k_fix)
+    model = _values_within_limits_constraint(
+        cdfs_ll_var,
+        cdfs_ul_var,
+        k_mod_var,
+        lower_fix,
+        model,
+        n_var,
+        sigma_level,
+        upper_fix,
+    )
+    model = _median_constraint(cdfs_median_var, k_mod_var, median_fix, model, n_var)
+    return model
+
+
+def _median_constraint(
+    cdfs_median_var: np.ndarray,
+    k_mod_var: list[Var],
+    median_fix: float,
+    model: Model,
+    n_var: int,
+    tolerance: float=0.005
+) -> Model:
+    # The median of the mixed distribution has to be h.
+    # h is considered in the calculation of the cdfs_median_var and median_fix.
+    # Small tolerance needed for solving
+    model.add_constraint(
+        sum(k_mod_var[i] * cdfs_median_var[i] for i in range(n_var)) + median_fix
+        <= 0.5 + tolerance,
+        ctname="c5",
+    )
+    model.add_constraint(
+        sum(k_mod_var[i] * cdfs_median_var[i] for i in range(n_var)) + median_fix
+        >= 0.5 - tolerance,
+        ctname="c6",
+    )
+    return model
+
+
+def _values_within_limits_constraint(
+    cdfs_ll_var: np.ndarray,
+    cdfs_ul_var: np.ndarray,
+    k_mod_var: list[Var],
+    lower_fix: float,
+    model: Model,
+    n_var: int,
+    sigma_level: float,
+    upper_fix: float,
+) -> Model:
+    # Less or equal than 0.135 % (depending on the sigma-level) of the distribution are below the lower limit.
+    # The lower limit is considered in the calculation of cdfs_ll_var and lower_fix.
+    model.add_constraint(
+        sum(k_mod_var[i] * cdfs_ll_var[i] for i in range(n_var)) + lower_fix
+        <= lower_quantile(sigma_level),
+        ctname="c3",
+    )
+    # More or equal than 99.865 % (depending on the sigma-level) of the distribution are above the upper limit.
+    # The upper limit is considered in the calculation of cdfs_ul_var and upper_fix.
+    model.add_constraint(
+        sum(k_mod_var[i] * cdfs_ul_var[i] for i in range(n_var)) + upper_fix
+        >= upper_quantile(sigma_level),
+        ctname="c4",
+    )
+    return model
+
+
+def _sum_of_weights_constraint(
+    k_mod_var: list[Var], model: Model, n_var: int, sum_k_fix: float
+) -> Model:
+    # The sum of all k_matrix values for the considered time step needs to be 1.
+    model.add_constraint(
+        sum(k_mod_var[i] for i in range(n_var)) + sum_k_fix <= 1, ctname="c1"
+    )
+    model.add_constraint(
+        sum(k_mod_var[i] for i in range(n_var)) + sum_k_fix >= 1, ctname="c2"
+    )
+    return model
+
+
+def _model_objective(
+    model: Model, k_mod_var: list[Var], problem_parameters: ProblemParameters
+) -> Model:
+    # add objective function to optimization model
+    # objective function: squared pairwise distances between the k_matrix values.
+    obj_fn = sum(
+        (k_mod_var[i] - k_mod_var[j]) ** 2
+        for i in range(problem_parameters.n_var)
+        for j in range(problem_parameters.n_var)
+    ) + sum(
+        (k_mod_var[i] - problem_parameters.k_fix[j]) ** 2
+        for i in range(problem_parameters.n_var)
+        for j in range(problem_parameters.n_fix)
+    )
+    # minimize objective function to get k_matrix values that are as similar as possible
+    model.set_objective("min", obj_fn)
+    return model
diff --git a/G_SPC/nn/__init__.py b/G_SPC/nn/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/G_SPC/nn/layer.py b/G_SPC/nn/layer.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3aa5c5d1b59a577bcbe7db534be8029c0f39a97
--- /dev/null
+++ b/G_SPC/nn/layer.py
@@ -0,0 +1,242 @@
+import numpy as np
+import tensorflow as tf
+from tensorflow import keras
+
+
+class NoOpLayer(keras.layers.Layer):
+    def __init__(self):
+        super().__init__()
+
+    def call(self, inputs, *args, **kwargs):
+        return inputs
+
+
+class LogDiffLayer(keras.layers.Layer):
+    def __init__(self, eps: float = 1e-8):
+        """Custom layer to return log returns."""
+        super().__init__()
+
+        self.eps = tf.constant(eps, dtype=tf.float32, name="eps")
+
+    def _custom_call(self, inputs):
+        m = tf.math.reduce_min(inputs, axis=0)
+        sig = tf.math.sign(m)
+        # decision variable: only apply offset into positive-only values, when negative values are present
+        d = (sig - tf.ones_like(sig)) / tf.constant(
+            -3, dtype=tf.float32
+        )  # {0, 0.333, 0.666}
+        d = tf.math.round(d)
+        # avoid negative values, because else log is not defined
+        log_inputs = inputs + tf.math.abs(m * d)
+
+        # avoid 0: add eps
+        log_inputs += self.eps
+
+        log_inputs = tf.math.log(log_inputs)
+        diff = log_inputs[1:] - log_inputs[:-1]
+        zero_vec = (
+            tf.zeros(shape=(1, diff.shape[1]))
+            if len(diff.shape) > 1
+            else tf.zeros(shape=(1,))
+        )
+        return tf.concat([zero_vec, diff], axis=0)
+
+    def call(self, inputs, *args, **kwargs):
+        """Take log returns
+
+        Parameters
+        ----------
+        inputs: tf.Tensor
+            can either be 1D or nd: (None, n_steps) or (None, n_steps, n_features)
+
+        Returns
+        -------
+        tf.Tensor
+            log returns, same shape as `inputs`
+        """
+        return tf.map_fn(self._custom_call, inputs)
+
+    def get_config(self):
+        config = super().get_config()
+        config.update({"eps": self.eps.numpy()})
+        return config
+
+
+class CpkLogDiffLayer(LogDiffLayer):
+    def __init__(
+        self,
+        upper_limit: float | np.ndarray,
+        lower_limit: float | np.ndarray,
+        cpk: float = 1.33,
+        eps: float = 1e-8,
+    ):
+        super().__init__(eps)
+
+        self.upper_limit = tf.constant(
+            upper_limit, dtype=tf.float32, name="upper_limit"
+        )
+        self.lower_limit = tf.constant(
+            lower_limit, dtype=tf.float32, name="lower_limit"
+        )
+        self.cpk = cpk
+        self.c = tf.constant(cpk * 3, dtype=tf.float32, name="cpk")
+
+    def _custom_call(self, inputs):
+        std = tf.math.reduce_std(inputs, axis=0)
+        avg = tf.math.reduce_mean(inputs, axis=0)
+        d = tf.math.reduce_min([self.upper_limit - avg, avg - self.lower_limit], axis=0)
+
+        # scale to common std
+        numerator = d * inputs
+        denominator = self.c * std
+        scaled = numerator / denominator
+
+        # remove mean: center around 0
+        scaled -= tf.reduce_mean(scaled, axis=0)
+
+        return super()._custom_call(scaled)
+
+    def call(self, inputs, *args, **kwargs):
+        """Take log returns normalized by cpk sigma
+
+        Parameters
+        ----------
+        inputs: tf.Tensor
+            can either be 1D or nd: (None, n_steps) or (None, n_steps, n_features)
+
+        Returns
+        -------
+        tf.Tensor
+            log returns, same shape as `inputs`
+        """
+        return tf.map_fn(self._custom_call, inputs)
+
+    def get_config(self):
+        config = super().get_config()
+        config.update(
+            {
+                "upper_limit": self.upper_limit.numpy(),
+                "lower_limit": self.lower_limit.numpy(),
+                "cpk": self.cpk,
+                "c": self.c.numpy(),
+            }
+        )
+        return config
+
+
+class LimitScaling(keras.layers.Layer):
+    def __init__(
+        self, upper_limit: float | np.ndarray, lower_limit: float | np.ndarray
+    ):
+        super().__init__()
+
+        self.upper_limit = tf.constant(
+            upper_limit, dtype=tf.float32, name="upper_limit"
+        )
+        self.lower_limit = tf.constant(
+            lower_limit, dtype=tf.float32, name="lower_limit"
+        )
+
+    def _custom_call(self, inputs):
+        # center
+        avg = tf.math.reduce_mean(inputs, axis=0)
+        # scale by limit range
+        rg = self.upper_limit - self.lower_limit
+        return (inputs - avg) / rg
+
+    def call(self, inputs, *args, **kwargs):
+        return tf.map_fn(self._custom_call, inputs)
+
+    def get_config(self):
+        config = super().get_config()
+        config.update(
+            {
+                "upper_limit": self.upper_limit.numpy(),
+                "lower_limit": self.lower_limit.numpy(),
+            }
+        )
+        return config
+
+
+class MeanSubLayer(keras.layers.Layer):
+    def call(self, inputs, *args, **kwargs):
+        means = tf.expand_dims(tf.math.reduce_mean(inputs, axis=1), axis=1)
+        return inputs - means
+
+
+class ResNetIdentityBlock(keras.layers.Layer):
+    def __init__(self, filters: int, **kwargs) -> None:
+        super().__init__()
+
+        self.filters = filters
+
+        self.conv1 = keras.layers.Conv1D(filters, 3, padding="same")
+        self.batch1 = keras.layers.BatchNormalization()
+
+        self.conv2 = keras.layers.Conv1D(filters, 3, padding="same")
+        self.batch2 = keras.layers.BatchNormalization()
+
+    def call(self, inputs, *args, **kwargs):
+        x_skip = inputs
+
+        x = self.conv1(inputs)
+        x = self.batch1(x)
+        x = keras.activations.relu(x)
+
+        x = self.conv2(x)
+        x = self.batch2(x)
+
+        x = tf.add(x, x_skip)
+        x = keras.activations.relu(x)
+
+        return x
+
+    def get_config(self):
+        config = super().get_config()
+        config.update(
+            {
+                "filters": self.filters,
+            }
+        )
+        return config
+
+
+class ResNetConvBlock(keras.layers.Layer):
+    def __init__(self, filters: int, **kwargs):
+        super().__init__()
+
+        self.filters = filters
+
+        self.conv1 = keras.layers.Conv1D(filters, 3, 2, padding="same")
+        self.batch1 = keras.layers.BatchNormalization()
+
+        self.conv2 = keras.layers.Conv1D(filters, 3, padding="same")
+        self.batch2 = keras.layers.BatchNormalization()
+
+        self.conv_skip = keras.layers.Conv1D(filters, 1, 2)
+
+    def call(self, inputs, *args, **kwargs):
+        x_skip = inputs
+
+        x = self.conv1(inputs)
+        x = self.batch1(x)
+        x = keras.activations.relu(x)
+
+        x = self.conv2(x)
+        x = self.batch2(x)
+
+        x_skip = self.conv_skip(x_skip)
+
+        x = tf.add(x, x_skip)
+        x = keras.activations.relu(x)
+
+        return x
+
+    def get_config(self):
+        config = super().get_config()
+        config.update(
+            {
+                "filters": self.filters,
+            }
+        )
+        return config
diff --git a/G_SPC/nn/model.py b/G_SPC/nn/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..8edc0390041019542f4dcde66792368719eec32d
--- /dev/null
+++ b/G_SPC/nn/model.py
@@ -0,0 +1,87 @@
+"""
+1D-CNN model definition.
+"""
+from typing import Type
+
+import tensorflow as tf
+from tensorflow import keras
+
+from G_SPC.nn.layer import MeanSubLayer, ResNetConvBlock, ResNetIdentityBlock
+
+
+def rebalanced_nok_mse(y_true, y_pred):
+    factor = tf.pow(tf.ones_like(y_true) + y_true, 4)
+    return tf.math.reduce_mean(tf.square((y_true - y_pred) * factor), axis=-1)
+
+
+def conv_block(x, filters: int = 64) -> tf.Tensor:
+    # (None, 20, 1, 64)
+    conv1 = keras.layers.Conv1D(filters=filters, kernel_size=3, padding="same")(x)
+    conv1 = keras.layers.BatchNormalization()(conv1)
+    return keras.layers.ReLU()(conv1)
+
+
+def make_conv_spc(
+    input_shape: tuple[int, int],
+    preproc_layer_cls: Type[keras.layers.Layer] = MeanSubLayer,
+    filters: int = 64,
+):
+
+    input_layer = keras.Input(input_shape)  # (None, 20, 1)
+
+    preproc = preproc_layer_cls()(input_layer)  # (None, 20, 1)
+
+    conv1 = conv_block(preproc, filters)
+    conv2 = conv_block(conv1, filters)
+
+    max1 = keras.layers.MaxPool1D(pool_size=3)(conv2)  # (None, 6, 64)
+
+    conv3 = conv_block(max1, filters)  # (None, 6, 64)
+    conv4 = conv_block(conv3, filters)  # (None, 6, 64)
+
+    max2 = keras.layers.MaxPool1D(pool_size=3)(conv4)  # (None, 1, 64)
+
+    conv5 = conv_block(max2, filters)
+    conv6 = conv_block(conv5, filters)
+
+    flatten = keras.layers.Flatten()(conv6)
+
+    dense1 = keras.layers.Dense(input_shape[0], activation="relu")(
+        flatten
+    )  # (None, 20)
+    dense2 = keras.layers.Dense(int(input_shape[0] / 2), activation="relu")(dense1)
+
+    output_layer = keras.layers.Dense(1, activation="linear")(dense2)  # (None, 1)
+    return keras.models.Model(inputs=input_layer, outputs=output_layer)
+
+
+def make_resnet_spc(
+    input_shape: tuple[int, int],
+    preproc_layer_cls: Type[keras.layers.Layer] = MeanSubLayer,
+    filters_list: list[int] | tuple[list] = (32, 64, 128, 256),
+):
+    input_layer = keras.Input(input_shape)  # (None, 20, 1)
+
+    preproc = preproc_layer_cls()(input_layer)  # (None, 20, 1)
+
+    # 20 - 10 - 5 - 3
+    conv1 = ResNetIdentityBlock(filters_list[0])(preproc)  # (None, 20, f0)
+
+    ident2 = ResNetConvBlock(filters_list[1])(conv1)  # (None, 10, f1)
+    conv21 = ResNetIdentityBlock(filters_list[1])(ident2)
+    conv22 = ResNetIdentityBlock(filters_list[1])(conv21)
+
+    ident3 = ResNetConvBlock(filters_list[2])(conv22)  # (None, 5, f2)
+    conv31 = ResNetIdentityBlock(filters_list[2])(ident3)
+    conv32 = ResNetIdentityBlock(filters_list[2])(conv31)
+
+    ident4 = ResNetConvBlock(filters_list[3])(conv32)  # (None, 3, f3)
+    conv41 = ResNetIdentityBlock(filters_list[3])(ident4)
+    conv42 = ResNetIdentityBlock(filters_list[3])(conv41)
+
+    pool = keras.layers.AveragePooling1D(2, padding="same")(conv42)  # (None, 3, ...)
+    flatten = keras.layers.Flatten()(pool)
+    dense1 = keras.layers.Dense(3, activation="relu")(flatten)  # (None, 3)
+    dense2 = keras.layers.Dense(1, activation="linear")(dense1)  # (None, 1)
+
+    return keras.models.Model(inputs=input_layer, outputs=dense2)
diff --git a/G_SPC/nn/preproc.py b/G_SPC/nn/preproc.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a2686f83180f343ebef95c6f8389ec6688f69d4
--- /dev/null
+++ b/G_SPC/nn/preproc.py
@@ -0,0 +1,155 @@
+import numpy as np
+from numpy.typing import ArrayLike
+
+
+def ramp_1d_score(x: ArrayLike, ramp_length: int, scale: float = 1) -> np.ndarray:
+    """Ramp up anomaly score for training.
+
+    This function is only used, if mixture weights are not available (e.g., industrial data and not synthetically
+    generated data)
+
+    Parameters
+    ----------
+    x: ArrayLike
+        boolean 1D-array (target for anomaly score) with OK=True and NOK=False
+    ramp_length: int
+        Length of ramp-up, first value of ramp is 0, last value is of ramp is `scale`, linear ramp up
+    scale: float, default=1
+        Scale returned values. By default, return value ramps up from 0 (OK) to 1 (NOK) (scale==1).
+
+    Returns
+    -------
+    np.ndarray
+        1d anomaly score, OK->0, NOK->`scale`
+
+    Examples
+    --------
+    .. code-block:: python
+
+        x = np.array([True, True, True, True, True, True, True, True, True, True, False, True, True, True, False, True, True, True, True, True])
+
+        y = ramp_1d_score(x, 6, 3)
+
+        # >> [0., 0., 0., 0., 0., 0., 0.6, 1.2, 1.8000001, 2.4, 3., 1.2, 1.8000001, 2.4, 3., 0., 0., 0., 0., 0.]
+
+    """
+    if ramp_length < 2:
+        raise ValueError(f"ramp_length must be greater 2. Got: {ramp_length}")
+
+    x = np.array(x, dtype="float32")
+    if len(x.shape) > 1:
+        raise IndexError(f"`x` must be 1D. Got: {x.shape}")
+
+    # invert x: NOK==1, OK==0
+    x = 1 - x
+
+    ret = np.zeros(np.array(x).shape, dtype="float32")
+    saw_tooth = np.array(range(ramp_length)) / (ramp_length - 1)  # anomaly==1, okay==0
+
+    # get all NOK indices
+    nok_inds = np.where(x == 1)[0]
+
+    # apply ramp up
+    for nok in nok_inds:
+        if nok >= ramp_length - 1:
+            # nok-(ramp_length-1): -1 because we want to start with first value of the ramp
+            # nok+1: because last value is not included in slice
+            patch = x[nok - (ramp_length - 1) : nok + 1]
+
+            # ensures we're not overwriting stuff
+            patch[patch == 0] = saw_tooth[patch == 0]
+
+            # nok-(ramp_length-1): -1 because we want to start with first value of the ramp
+            # nok+1: because last value is not included in slice
+            ret[nok - (ramp_length - 1) : nok + 1] = patch[:]
+        else:
+            patch = x[: nok + 1]
+            patch[patch == 0] = saw_tooth[-len(patch) :][patch == 0]
+            ret[: nok + 1] = patch[:]
+
+    return ret * scale
+
+
+def mixture_to_score(k_mat: np.ndarray, cols_nok: list[int], scale: float = 1):
+    """Extract anomaly score from mixture weights for training.
+
+    Parameters
+    ----------
+    k_mat: np.ndarray
+        matrix of mixture weights, shape=(n_steps, n_distribs)
+    cols_nok: list[int]
+        indices of nok columns in `k_mat` corresponding to NOK distributions
+    scale: float, default=1
+        Scale returned values. By default, return value ramps up from 0 (OK) to 1 (NOK) (scale==1).
+
+    Returns
+    -------
+    np.ndarray
+        1d anomaly score, , OK->0, NOK->`scale`
+    """
+    weights = np.zeros((k_mat.shape[1], 1))
+    weights[cols_nok] = 1
+    return np.average(k_mat, weights=weights, axis=0) * scale
+
+
+def mixture_to_probavec(k_mat: np.ndarray, cols_nok: list[int]) -> np.ndarray:
+    """
+
+    Parameters
+    ----------
+    k_mat: np.ndarray
+    cols_nok: list[int]
+        indices of nok columns
+
+    Returns
+    -------
+    np.ndarray, shape=(len(k_mat), 2)
+        first column OK proba, second column NOK proba
+    """
+    cols_nok = [i if i != -1 else k_mat.shape[1] - 1 for i in cols_nok]
+
+    ok_cols = list(range(k_mat.shape[1]))
+    ok_cols = [i for i in ok_cols if i not in cols_nok]
+
+    oks = k_mat[:, ok_cols]
+    noks = k_mat[:, cols_nok]
+
+    oks_sum = np.expand_dims(np.sum(oks, axis=1), axis=1)
+    noks_sum = np.expand_dims(np.sum(noks, axis=1), axis=1)
+
+    return np.concatenate((oks_sum, noks_sum), axis=1)
+
+
+def blockify(
+    inp: np.ndarray, target: np.ndarray, window_length: int
+) -> tuple[np.ndarray, np.ndarray]:
+    # inp_ret --> observations: either 1D or multidimensional
+    # target --> anomaly scores: always 1D
+
+    if len(target.shape) > 1:
+        raise IndexError(f"`target` must be 1D. Got: {target.shape}")
+
+    inp_ret = []
+    target_ret = []
+
+    is_1d = len(inp.shape) == 1
+
+    for i in range(len(target) - window_length):
+
+        if is_1d:
+            inp_ret.append(inp[i : i + window_length])
+        else:
+            inp_ret.append(inp[i : i + window_length, :])
+
+        target_ret.append(target[i : i + window_length])
+
+    if is_1d:
+        inp_ret = np.expand_dims(inp_ret, axis=-1)
+
+    target_ret = np.array(target_ret)
+    if len(target_ret.shape) < 3:
+        target_ret = np.expand_dims(target_ret, -1)
+
+    # inp_ret: (blocks, window_length, 1) or (blocks, window_lengths, num_var)
+    # target_ret: (blocks, window_length, 1)
+    return np.array(inp_ret), target_ret
diff --git a/G_SPC/nn/preproc_pipe.py b/G_SPC/nn/preproc_pipe.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb2bae1842c6dabb90f941a537ce0678ca6c8085
--- /dev/null
+++ b/G_SPC/nn/preproc_pipe.py
@@ -0,0 +1,126 @@
+from pathlib import Path
+from typing import Callable
+
+import numpy as np
+from sklearn import model_selection
+
+from G_SPC.nn.preproc import blockify, ramp_1d_score, mixture_to_score
+
+
+def _pipe(
+    obs: np.ndarray,
+    target: np.ndarray,
+    observation_length: int,
+    shuffle: bool,
+    test_split: float,
+    random_state: int | None,
+):
+    block_data, block_target = blockify(obs, target, observation_length)
+
+    if np.isclose(test_split, 1.0, rtol=1e-09, atol=1e-09):
+        # Case for using the whole dataset for evaluation
+        data_train = np.empty(1)
+        data_test = block_data
+        target_train = np.empty(1)
+        target_test = block_target
+    else:
+        (
+            data_train,
+            data_test,
+            target_train,
+            target_test,
+        ) = model_selection.train_test_split(
+            block_data,
+            block_target,
+            test_size=test_split,
+            random_state=random_state,
+            shuffle=shuffle,
+        )
+
+    return data_train, data_test, target_train, target_test
+
+
+def prepare_pipe_ramp(
+    pth_observation: str | Path,
+    pth_inspect: str | Path,
+    load_fn: Callable[[str | Path], np.ndarray],
+    observation_length: int,
+    ramp_length: int,
+    anomaly_scale: float,
+    shuffle: bool,
+    test_split: float,
+    random_state: int | None = None,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+    """Prepare (industrial) data with actual QC-inspections results for training.
+
+    Parameters
+    ----------
+    pth_observation: str | Path,
+        path to observation data: real physical measurements, which shall be monitored/ controlled.
+        loaded data should have shape=(n_steps, n_vars)
+    pth_inspect: str | Path,
+        path to inspection data: real QC-Data, should be bool np.ndarray with shape=(n_steps,),
+        where OK=True and NOK=False
+    load_fn
+    observation_length
+    ramp_length
+    anomaly_scale: float
+        max anomaly score value (in case of NOK)
+    shuffle
+    test_split: float
+        between 0. and 1.
+    random_state
+
+    Returns
+    -------
+    tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
+        data_train, data_test, target_train, target_test
+    """
+    obs = load_fn(pth_observation)
+    insp = load_fn(pth_inspect)
+
+    target = ramp_1d_score(insp, ramp_length, anomaly_scale)
+
+    return _pipe(obs, target, observation_length, shuffle, test_split, random_state)
+
+
+def prepare_pipe_mixture(
+    pth_observation: str | Path,
+    load_fn: Callable[[str | Path], np.ndarray],
+    observation_length: int,
+    k_mat: np.ndarray,
+    cols_nok: list[int],
+    anomaly_scale: float,
+    shuffle: bool,
+    test_split: float,
+    random_state: int | None = None,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+    """Prepare synthetically generated data for training by using the mixture weights.
+
+    Parameters
+    ----------
+    pth_observation: str | Path,
+        path to observation data: real physical measurements, which shall be monitored/ controlled.
+        loaded data should have shape=(n_steps, n_vars)
+    load_fn
+    observation_length
+    k_mat: np.ndarray
+        weight matrix of mixture, shape=(n_steps, n_distribs)
+    cols_nok: list[int]
+        column indices in `k_mat` which resemble NOK distributions
+    anomaly_scale: float
+        Scale returned values. By default, return value ramps up from 0 (OK) to 1 (NOK) (scale==1).
+    shuffle
+    test_split
+    random_state
+
+    Returns
+    -------
+    tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
+        data_train, data_test, target_train, target_test
+    """
+    obs = load_fn(pth_observation)
+
+    target = mixture_to_score(k_mat=k_mat, cols_nok=cols_nok, scale=anomaly_scale)
+
+    return _pipe(obs, target, observation_length, shuffle, test_split, random_state)
diff --git a/G_SPC/normal_quantile.py b/G_SPC/normal_quantile.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca96f7dfa6ed0e905265e85617bebbf2541dd92f
--- /dev/null
+++ b/G_SPC/normal_quantile.py
@@ -0,0 +1,39 @@
+from tensorflow_probability import distributions as tfd
+
+
+def lower_quantile(sigma_level: float) -> float:
+    """Based on sigma level there is a defined interval within a certain percentage of values has to lie.
+    Returns the lower interval limit expressed as a quantile value taken from the standard normal distribution.
+
+    Parameters
+    ----------
+    sigma_level: float
+        Defines the percentage of the distributions values that has to lie within the tolerance interval
+
+    Returns
+    -------
+    float
+        Lower interval limit
+    """
+
+    p_inner = 2 * tfd.Normal(0, 1).cdf(sigma_level) - 1
+    return float((1 - p_inner) / 2)
+
+
+def upper_quantile(sigma_level: float) -> float:
+    """Based on sigma level there is a defined interval within a certain percentage of values has to lie.
+    Returns the upper interval limit expressed as a quantile value taken from the standard normal distribution.
+
+    Parameters
+    ----------
+    sigma_level: float
+        Defines the percentage of the distributions values that has to lie within the tolerance interval
+
+    Returns
+    -------
+    float
+        Upper interval limit
+    """
+
+    p_inner = 2 * tfd.Normal(0, 1).cdf(sigma_level) - 1
+    return float(0.5 + p_inner / 2)
diff --git a/G_SPC/spc.py b/G_SPC/spc.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d96f32c8d065636f7d4ebe5585604e43025ffd6
--- /dev/null
+++ b/G_SPC/spc.py
@@ -0,0 +1,1248 @@
+from __future__ import absolute_import
+
+import json
+import random
+import statistics
+from abc import ABC, abstractmethod
+from typing import Tuple, Union, Any
+
+import matplotlib.lines as mlines
+import matplotlib.pylab as plt
+import numpy as np
+import seaborn as sns
+from scipy import stats
+
+from G_SPC.exception import GSPCError
+
+
+class SPC(ABC):
+    """Class for calculating stability and plotting SPC charts
+
+    """
+
+    def __init__(
+        self,
+        data_phase_i: list[float] = None,
+        data_phase_ii: list[float] = None,
+        estimated: bool = False,
+    ):
+        """Initialize SPC
+
+        Parameters
+        ----------
+        data_phase_i: list[float]
+            Data for phase I of SPC. If estimated = True data is set to data_phase_ii
+        data_phase_ii: list[float]
+            Data for phase_ii of SPC
+        estimated: bool
+            Whether to use the data of phase_ii to calculate the limits
+            or to use separate data from phase I. Defaults to False.
+
+        """
+
+        assert data_phase_ii is not None
+        self.data_phase_ii = data_phase_ii.copy()
+        self.estimated = estimated
+        if not estimated:
+            assert data_phase_i is not None
+            self.data_phase_i = data_phase_i.copy()
+        else:
+            self.data_phase_i = data_phase_ii.copy()
+        self.datapoints_phase_i = None
+        self.datapoints_phase_ii = None
+        self.location = None
+        self.scale = None
+        self.lcl = None
+        self.ucl = None
+        self.type = "SPC"
+        self.criteria = []
+        self.sample_size = None
+        self.batch_size = None
+
+    def calculate_spc(self):
+        """Calculates the location, scale and limits as well as the datapoints for both phases.
+        Calculates, prints and returns the stability violations.
+
+        Returns
+        -------
+        dict
+            Indices of datapoints violating the stability criteria
+        """
+
+        (
+            self.location,
+            self.lcl,
+            self.ucl,
+            self.scale,
+            self.datapoints_phase_i,
+        ) = self._phase_i()
+        self.datapoints_phase_ii = self._phase_ii()
+        return self.stability_info()
+
+    def update_spc(self, data: list[float]) -> list[int]:
+        """Updates the SPC with additional phase_ii data and returns the indices of the resulting datapoints.
+        Afterwards the updated chart can be plotted with plot_spc. The information about stability violations
+        are given with calling stability_info.
+
+        Parameters
+        ----------
+        data: list[float]
+            Data to update the SPC chart
+
+        Returns
+        -------
+        list[int]
+            The indices of the datapoints generated from the additional data
+        """
+        if self.batch_size:
+            if len(data) % self.batch_size != 0:
+                raise GSPCError(
+                    "The number of values provided has to be a multiply of the batch size SPC methods "
+                    "that do not use individual values"
+                )
+            datapoint_indices = list(
+                range(
+                    len(self.datapoints_phase_ii),
+                    len(self.datapoints_phase_ii) + int(len(data) / self.batch_size),
+                )
+            )
+
+        else:
+            datapoint_indices = list(
+                range(
+                    len(self.datapoints_phase_ii),
+                    len(self.datapoints_phase_ii) + len(data),
+                )
+            )
+
+        datapoints_update = self._phase_ii(data)
+        self.data_phase_ii.extend(data)
+        self.datapoints_phase_ii.extend(datapoints_update)
+
+        return datapoint_indices
+
+    @abstractmethod
+    def _phase_i(self) -> Tuple[float, float, float, float, list[float]]:
+        return ...
+
+    @abstractmethod
+    def _phase_ii(self, data_phase_ii: list[float] = None) -> list[float]:
+        return ...
+
+    @staticmethod
+    def get_table_factor(factor: str, sample_size: int) -> float:
+        """Returns the value for the considered norm factor in dependence of the sample size
+
+        Parameters
+        ----------
+        factor: str
+            Norm factor which value is asked for
+        sample_size: int
+            Sample Size of the chart
+
+        Returns
+        -------
+        float
+            Value of the norm factor for calculation of limits/location
+        """
+        assert 1 < sample_size <= 25
+
+        # load table factors for limit calculation defined in DIN ISO 7870-2:2013
+        f = open("../data/table_factors.json")
+        factors = json.load(f)
+
+        assert factor in {"A", "A2", "A3", "B3", "B4", "B5", "B6", "C4"}
+        return factors[factor][sample_size - 2]
+
+    def _get_stability_info(self) -> dict:
+        """Returns a dictionary with information for which indices
+        of the datapoints_phase_ii the considered stability criteria are violated
+
+        Returns
+        -------
+        dict
+            Indices of datapoints violating the stability criteria
+        """
+
+        stability_info = dict()
+        if "out_of_limits" in self.criteria:
+            stability_info["out_of_limits"] = self._out_of_limit_indices(
+                self.datapoints_phase_ii
+            )
+        if "run" in self.criteria:
+            stability_info["run"] = self._run_indices(self.datapoints_phase_ii)
+        if "trend" in self.criteria:
+            stability_info["trend"] = self._trend_indices(self.datapoints_phase_ii)
+        if "two_out_of_three" in self.criteria:
+            stability_info["two_out_of_three"] = self._two_of_three_outside_two_sigma(
+                self.datapoints_phase_ii
+            )
+        if "four_out_of_five" in self.criteria:
+            stability_info["four_out_of_five"] = self._four_of_five_outside_one_sigma(
+                self.datapoints_phase_ii
+            )
+        if "middle_third" in self.criteria:
+            stability_info["middle_third"] = self._check_middle_third_violated(
+                self.datapoints_phase_ii
+            )
+        return stability_info
+
+    @staticmethod
+    def print_stability_info(stability_info: dict[str, Any]):
+        """Print out information about the violation of the stability criteria.
+
+        Parameters
+        ----------
+        stability_info: dict[str, Any]
+            Indices of datapoints violating the stability criteria
+
+        Returns
+        -------
+        None
+        """
+
+        if "out_of_limits" in stability_info:
+            print(
+                "Indices of data-points violating limits: "
+                + str(stability_info["out_of_limits"])
+            )
+        if "run" in stability_info:
+            print(
+                "Indices of data-points that are subject to a run: "
+                + str(stability_info["run"])
+            )
+        if "trend" in stability_info:
+            print(
+                "Indices of data-points that are subject to a trend: "
+                + str(stability_info["trend"])
+            )
+        if "two_out_of_three" in stability_info:
+            print(
+                "Indices of data-points violating the two out of three values within two sigma criterion: "
+                + str(stability_info["two_out_of_three"])
+            )
+        if "four_out_of_five" in stability_info:
+            print(
+                "Indices of data-points violating the four out of five values within one sigma criterion: "
+                + str(stability_info["four_out_of_five"])
+            )
+        if "middle_third" in stability_info:
+            is_violated, share = stability_info["middle_third"]
+            if is_violated:
+                print(
+                    "The middle third criterion is violated. "
+                    + f"{share*100}"
+                    + "% of the data are within the middle third."
+                )
+
+    def stability_info(self) -> dict[str, Any]:
+        """Prints and returns the stability info with the criteria relevant for this chart type
+
+        Returns
+        -------
+        dict[str, Any]
+            Indices of datapoints violating the stability criteria
+        """
+
+        stability_info = self._get_stability_info()
+        self.print_stability_info(stability_info)
+        return stability_info
+
+    def _out_of_limit_indices(self, datapoints: list[float]) -> list[int]:
+        """Returns indices of data-points with values outside the tolerance limits.
+
+        Parameters
+        ----------
+        datapoints: list[float]
+            List of the datapoints that are checked whether they are outside the tolerance limits
+
+        Returns
+        -------
+        list[int]
+            Indices of the datapoints outside the control limits
+        """
+
+        return [
+            i
+            for i in range(len(datapoints))
+            if (datapoints[i] < self.lcl) or (datapoints[i] > self.ucl)
+        ]
+
+    def _run_indices(self, datapoints: list[float]) -> list[int]:
+        """Returns indices of data-points where at least the values of the six previous data-points
+        were on the same side of the location parameter.
+
+        Parameters
+        ----------
+        datapoints:  list[float]
+            List of the datapoints that are checked whether they are subject to a run
+
+        Returns
+        -------
+        list[int]
+            Indices of the datapoints that are subject to a run
+        """
+
+        violating_indices = []
+        count_upper = 0
+        count_lower = 0
+        for i in range(len(datapoints)):
+            if datapoints[i] > self.location:
+                count_upper += 1
+                count_lower = 0
+            elif datapoints[i] < self.location:
+                count_upper = 0
+                count_lower += 1
+            else:
+                count_upper = 0
+                count_lower = 0
+            if (count_lower >= 7) or (count_upper >= 7):
+                violating_indices.append(i)
+        return violating_indices
+
+    @staticmethod
+    def _trend_indices(datapoints: list[float]) -> list[int]:
+        """Returns indices of data-points where at least the values of the six previous
+        data-points were continuously rising or falling.
+
+        Parameters
+        ----------
+        datapoints: list[float]
+            List of the datapoints that are checked whether they are subject to a trend
+
+        Returns
+        -------
+        list[int]
+            Indices of the datapoints that are subject to a trend
+        """
+
+        violating_indices = []
+        count_raising = 0
+        count_falling = 0
+        for i in range(1, len(datapoints)):
+            if datapoints[i] > datapoints[i - 1]:
+                count_raising += 1
+                count_falling = 0
+            elif datapoints[i] < datapoints[i - 1]:
+                count_raising = 0
+                count_falling += 1
+            else:
+                count_raising = 0
+                count_falling = 0
+            if (count_raising >= 6) or (count_falling >= 6):
+                violating_indices.append(i)
+        return violating_indices
+
+    def _two_of_three_outside_two_sigma(self, datapoints: list[float]) -> list[int]:
+        """Returns indices of data-points where at least the values of two of the last three datapoints
+               (including the current one) are outside a two sigma area above/below the location of the chart
+
+        Parameters
+        ----------
+        datapoints: list[float]
+            List of the datapoints that are checked whether they are subject to the
+            two out of three outside two sigma criterion
+
+        Returns
+        -------
+        list[int]
+            Indices of the datapoints that are subject to the two out of three outside two sigma criterion
+        """
+
+        violating_indices = []
+        count_lower = 0
+        count_upper = 0
+        for i in range(len(datapoints) - 2):
+            for j in range(i, i + 3):
+                if datapoints[j] > self.location + 2 * self.scale:
+                    count_upper += 1
+                if datapoints[j] < self.location - 2 * self.scale:
+                    count_lower += 1
+            if (count_lower >= 2) or (count_upper >= 2):
+                violating_indices.append(i + 2)
+            count_lower = 0
+            count_upper = 0
+        return violating_indices
+
+    def _four_of_five_outside_one_sigma(self, datapoints: list[float]) -> list[int]:
+        """Returns indices of data-points where at least the values of four of the last five datapoints
+        (including the current one) are outside a one sigma area above/below the location of the chart
+
+        Parameters
+        ----------
+        datapoints: list[float]
+            List of the datapoints that are checked whether they are subject to the
+            four out of five outside one sigma criterion
+
+        Returns
+        -------
+        list[int]
+            Indices of the datapoints that are subject to the four out of five outside one sigma criterion
+        """
+
+        violating_indices = []
+        count_lower = 0
+        count_upper = 0
+        for i in range(len(datapoints) - 4):
+            for j in range(i, i + 5):
+                if datapoints[j] > self.location + self.scale:
+                    count_upper += 1
+                if datapoints[j] < self.location - self.scale:
+                    count_lower += 1
+            if (count_lower >= 4) or (count_upper >= 4):
+                violating_indices.append(i + 4)
+            count_lower = 0
+            count_upper = 0
+        return violating_indices
+
+    def _check_middle_third_violated(
+        self, datapoints: list[float]
+    ) -> Tuple[bool, float]:
+        """Returns whether less than 40 % or more than 90 % of the data are within
+        the middle third of the tolerance interval
+
+        Parameters
+        ----------
+        datapoints: list[float]
+            List of the datapoints that are checked whether there are less than 40%
+            or more than 90% of them within the middle third of the tolerance limits
+
+        Returns
+        -------
+        is_violated: bool
+            Whether the criterion is violated
+        share: float
+            The share of the datapoints within the middle third
+        """
+
+        data_middle_third = [
+            x
+            for x in datapoints
+            if (x > self.location - 1 / 3 * (self.location - self.lcl))
+            and (x < self.location + 1 / 3 * (self.ucl - self.location))
+        ]
+        share = len(data_middle_third) / len(datapoints)
+
+        is_violated = share <= 0.4 or share > 0.9
+        return is_violated, share
+
+    @property
+    def get_datapoints_phase_ii(self) -> list[float]:
+        return self.datapoints_phase_ii
+
+    def plot_chart_phase_i(
+        self, chart_type: str, n_datapoints: Union[int, None] = None
+    ) -> None:
+        """Plotting the data considered in phase i of the SPC.
+
+        Parameters
+        ----------
+        chart_type: str
+            Type of the control chart
+        n_datapoints: Union[int, None]
+            Number of datapoints to plot. If None: all datapoints are plotted.
+
+        Returns
+        -------
+        None
+        """
+
+        sns.set_theme()
+        fig = plt.figure()
+        ax1 = fig.add_subplot(111)
+        if n_datapoints:
+            ax1.set_title(
+                f"{chart_type} (phase I) - last {n_datapoints} datapoints",
+                fontdict={"fontsize": 20, "fontweight": "bold"},
+            )
+        else:
+            ax1.set_title(
+                f"{chart_type} (phase I)",
+                fontdict={"fontsize": 20, "fontweight": "bold"},
+            )
+
+        if not n_datapoints:
+            n_datapoints = len(self.datapoints_phase_i)
+
+        self.plot_data_and_limits(
+            ax1,
+            self.datapoints_phase_i[-n_datapoints:],
+            self.lcl,
+            self.location,
+            self.ucl,
+        )
+        plt.show()
+
+    def plot_chart_phase_ii(
+        self,
+        chart_type: str,
+        criteria: list[str],
+        n_datapoints: Union[int, None] = None,
+    ) -> None:
+        """Plotting the SPC chart (phase_ii) with highlighting of stability criteria violations
+
+        Parameters
+        ----------
+        chart_type: str
+            Type of the control chart
+        criteria: list[str]
+            Defines which stability criteria are tested.
+            Supports 'out_of_limits', 'run', 'trend', 'two_out_of_three', 'four_out_of_five' and 'middle_third'.
+        n_datapoints: Union[int, None]
+            Number of datapoints to plot. If None: all datapoints are plotted.
+
+        Returns
+        -------
+        None
+        """
+
+        sns.set_theme()
+        fig = plt.figure()
+        ax1 = fig.add_subplot(111)
+
+        if n_datapoints:
+            ax1.set_title(
+                f"{chart_type} - last {n_datapoints} datapoints",
+                fontdict={"fontsize": 20, "fontweight": "bold"},
+            )
+        else:
+            ax1.set_title(chart_type, fontdict={"fontsize": 20, "fontweight": "bold"})
+
+        if not n_datapoints:
+            n_datapoints = len(self.datapoints_phase_ii)
+
+        self.plot_data_and_limits(
+            ax1,
+            self.datapoints_phase_ii[-n_datapoints:],
+            self.lcl,
+            self.location,
+            self.ucl,
+        )
+        chart_legend = ax1.legend(["Mean", "Limits", "Data"], loc="upper left")
+        ax1.add_artist(chart_legend)
+
+        # add violations of stability criteria to plot
+        self._plot_stability_criteria(
+            ax1, criteria, self.datapoints_phase_ii[-n_datapoints:]
+        )
+        stability_legend = self._stability_criteria_legend(criteria)
+        ax1.legend(handles=stability_legend, loc="upper right")
+
+        plt.show()
+
+    @staticmethod
+    def plot_data_and_limits(
+        ax1: plt.axis, datapoints: list[float], lcl: float, location: float, ucl: float
+    ):
+        # add the datapoints as well as control limits and location to chart
+        ax1.plot([location for _ in datapoints], color="red")
+        ax1.plot([lcl for _ in datapoints], color="orange", linestyle="--")
+        ax1.plot(
+            [ucl for _ in datapoints],
+            color="orange",
+            linestyle="--",
+            label="_nolegend_",
+        )
+        ax1.plot(datapoints, color="blue")
+
+    @staticmethod
+    def _stability_criteria_legend(criteria: list[str]):
+        # define legend to explain the plotted stability violations
+        stability_legend = []
+        if "out_of_limits" in criteria:
+            pink_triangle = mlines.Line2D(
+                [],
+                [],
+                color="pink",
+                marker="^",
+                linestyle="None",
+                markersize=10,
+                label="Out of limits",
+            )
+            stability_legend.append(pink_triangle)
+        if "run" in criteria:
+            red_cross = mlines.Line2D(
+                [],
+                [],
+                color="red",
+                marker="x",
+                linestyle="None",
+                markersize=10,
+                label="Run",
+            )
+            stability_legend.append(red_cross)
+        if "trend" in criteria:
+            grey_circle = mlines.Line2D(
+                [],
+                [],
+                color="grey",
+                marker="o",
+                linestyle="None",
+                markersize=10,
+                label="Trend",
+            )
+            stability_legend.append(grey_circle)
+        if "two_out_of_three" in criteria:
+            green_square = mlines.Line2D(
+                [],
+                [],
+                color="green",
+                marker="s",
+                linestyle="None",
+                markersize=10,
+                label="2 sigma",
+            )
+            stability_legend.append(green_square)
+        if "four_out_of_five" in criteria:
+            orange_diamond = mlines.Line2D(
+                [],
+                [],
+                color="orange",
+                marker="d",
+                linestyle="None",
+                markersize=10,
+                label="1 sigma",
+            )
+            stability_legend.append(orange_diamond)
+        return stability_legend
+
+    def _plot_middle_third_criterion(self, ax1: plt.axis):
+        # plots whether the middle third criterion is violated
+        is_violated, share_middle_third = self._check_middle_third_violated(
+            self.datapoints_phase_ii
+        )
+        if is_violated:
+            ax1.text(
+                0.5,
+                0.03,
+                "The middle third criterion is violated!\n "
+                + f"{share_middle_third * 100}"
+                + " % of the data are within the middle third.",
+                horizontalalignment="center",
+                verticalalignment="center",
+                transform=ax1.transAxes,
+                color="red",
+                backgroundcolor="white",
+            )
+
+    def _plot_stability_criteria(
+        self, ax1: plt.axis, criteria: list[str], datapoints: list[float]
+    ):
+        # display violations of stability criteria in charts
+        if "middle_third" in criteria:
+            self._plot_middle_third_criterion(ax1)
+        for i in range(len(self.datapoints_phase_ii)):
+            if "out_of_limits" in criteria and i in self._out_of_limit_indices(
+                datapoints
+            ):
+                ax1.plot(
+                    i, datapoints[i], color="pink", marker="^", label="_nolegend_",
+                )
+            if "run" in criteria and i in self._run_indices(datapoints):
+                ax1.plot(
+                    i, datapoints[i], color="red", marker="x", label="_nolegend_",
+                )
+            if "trend" in criteria and i in self._trend_indices(datapoints):
+                ax1.plot(
+                    i, datapoints[i], color="grey", marker="o", label="_nolegend_",
+                )
+            if (
+                "two_out_of_three" in criteria
+                and i in self._two_of_three_outside_two_sigma(datapoints)
+            ):
+                ax1.plot(
+                    i, datapoints[i], color="green", marker="s", label="_nolegend_",
+                )
+            if (
+                "four_out_of_five" in criteria
+                and i in self._four_of_five_outside_one_sigma(datapoints)
+            ):
+                ax1.plot(
+                    i, datapoints[i], color="orange", marker="d", label="_nolegend_",
+                )
+
+    def plot_spc(
+        self, n_datapoints: Union[int, None] = None, plot_phase_i: bool = True
+    ) -> None:
+        """Plots the control chart (phase_ii) as well as the datapoints generated in phase i
+
+        Parameters
+        ----------
+        n_datapoints: Union[int, None]
+            Number of datapoints to plot. The last n_datapoints are plottet. If None: all datapoints are plotted.
+        plot_phase_i: bool
+            Whether to plot phase one of SPC. Defaults to true.
+        Returns
+        -------
+        None
+        """
+        if plot_phase_i:
+            self.plot_chart_phase_i(self.type, n_datapoints)
+        self.plot_chart_phase_ii(self.type, self.criteria, n_datapoints)
+
+
+class XBarChart(SPC):
+    """Perform SPC based on x bar approach
+
+    """
+
+    def __init__(
+        self,
+        data_phase_i: list[float] = None,
+        data_phase_ii: list[float] = None,
+        sample_size: int = 5,
+        batch_size: int = 5,
+        estimated: Union[bool, None, int] = None,
+        criteria: list[str] = None,
+    ) -> None:
+        """Initialize SPC based on x bar approach
+
+        Parameters
+        ----------
+        data_phase_i: list[float]
+            Data for phase I of SPC. If estimated = True data is set to data_phase_ii
+        data_phase_ii: list[float]
+            Data for phase_ii of SPC
+        estimated: bool
+            Whether to use the data of phase_ii to calculate the limits or to use separate data from phase I.
+            Defaults to False.
+        sample_size: int
+            Number of data-points for average calculation
+        batch_size: int
+            Size of data slices from which the samples are drawn
+        criteria: list[str]
+            Stability criteria considered to consider. Defaults to violation of limits, trend, run and the middle
+            third criterion
+
+        Returns
+        -------
+        None
+
+        """
+
+        if criteria is None:
+            criteria = ["out_of_limits", "trend", "run", "middle_third"]
+        assert batch_size >= sample_size
+
+        super().__init__(data_phase_i, data_phase_ii, estimated)
+        self.sample_size = sample_size
+        self.batch_size = batch_size
+        self.type = r"$\widebar{X}$ chart"
+        self.criteria = criteria
+
+    def _phase_i(self) -> Tuple[float, float, float, float, list[float]]:
+        """Calculates and resulting datapoints, limits, location and scale from data
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        scale: float
+            The scale parameter
+        generated_datapoints: list[float]
+            The datapoints representing the sample averages
+        """
+
+        data_phase_i = self.data_phase_i.copy()
+
+        data_samples = self.sample_data(data_phase_i)
+
+        location, scale, lcl, ucl = self._calculate_chart_parameters(data_samples)
+
+        data_generated = [float(np.mean(data_sample)) for data_sample in data_samples]
+        return float(location), float(lcl), float(ucl), float(scale), data_generated
+
+    def _calculate_chart_parameters(
+        self, data_samples: list[list[float]]
+    ) -> Tuple[float, float, float, float]:
+        """Calculates the chart parameters
+
+        Parameters
+        ----------
+        data_samples: list[list[float]]
+            The sampled data to calculate the statistics from
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        scale: float
+            The scale parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        """
+        stdev_samples = [statistics.stdev(data_sample) for data_sample in data_samples]
+        scale = np.mean(stdev_samples).item()
+        location = np.mean(data_samples).item()
+        lcl, ucl = self._calculate_limits(location, scale)
+        return location, scale, lcl, ucl
+
+    def _calculate_limits(self, location: float, scale: float) -> Tuple[float, float]:
+        """Calculates the control limits for the XBarChart
+
+        Parameters
+        ----------
+        location: float
+            The location parameter
+        scale: float
+            The scale parameter
+
+        Returns
+        -------
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+
+        """
+        if not self.estimated:
+            lcl = location - self.get_table_factor("A", self.sample_size) * scale
+            ucl = location + self.get_table_factor("A", self.sample_size) * scale
+        elif self.estimated == 2:
+            lcl = location - self.get_table_factor("A2", self.sample_size) * scale
+            ucl = location + self.get_table_factor("A2", self.sample_size) * scale
+        elif self.estimated == 3:
+            lcl = location - self.get_table_factor("A3", self.sample_size) * scale
+            ucl = location + self.get_table_factor("A3", self.sample_size) * scale
+        else:
+            raise ValueError("Only None, 2 or 3 are valid values.")
+        return lcl, ucl
+
+    def sample_data(self, data):
+        """Splits data into slices of batch size and takes a sample of sample size of each slice.
+
+        Parameters
+        ----------
+        data: list[float]
+            The data to sample from
+
+        Returns
+        -------
+        list[list[float]]
+            Sampled data
+
+        """
+        data.reverse()
+        data = [
+            data[i * self.batch_size : (i + 1) * self.batch_size]
+            for i in range(int(len(data) // self.batch_size))
+        ]
+        data.reverse()
+        return [random.sample(values, self.sample_size) for values in data]
+
+    def _phase_ii(self, data_phase_ii: list[float] = None) -> list[float]:
+        """Calculates the datapoints for calculation and plotting of phase_ii
+
+        Parameters
+        ----------
+        data_phase_ii: list[float]
+            If provided the data for the calculation of datapoints of phase_ii. Else self.data_phase_ii is used.
+
+        Returns
+        -------
+        list[float]
+            Calculated datapoints for phase_ii
+
+        """
+        if not data_phase_ii:
+            data_phase_ii = self.data_phase_ii.copy()
+        data_samples = self.sample_data(data_phase_ii)
+        return [float(np.mean(sample)) for sample in data_samples]
+
+
+class ShewartIndividualsChart(SPC):
+    """Perform SPC based on Shewart individual approach
+
+    """
+
+    def __init__(
+        self,
+        data_phase_i: list[float] = None,
+        data_phase_ii: list[float] = None,
+        estimated: bool = False,
+        alpha: float = None,
+        criteria: list[str] = None,
+    ):
+        """Initialize SPC based on Shewart individual approach
+
+        Parameters
+        ----------
+        data_phase_i: list[float]
+            Data for phase I of SPC. If estimated = True data is set to data_phase_ii
+        data_phase_ii: list[float]
+            Data for phase_ii of SPC
+        estimated: bool
+            Whether to use the data of phase_ii to calculate the limits
+            or to use separate data from phase I. Defaults to False.
+        alpha: float
+            Accepted error rate. Ignored when estimated = True
+        criteria: list[str]
+            Stability criteria considered to consider. Defaults to violation of limits, trend, run and the middle
+            third criterion
+
+        Returns
+        -------
+        None
+        """
+
+        if criteria is None:
+            criteria = ["out_of_limits", "trend", "run", "middle_third"]
+        super().__init__(data_phase_i, data_phase_ii, estimated)
+        if not self.estimated:
+            self.z_score = stats.norm.ppf(1 - alpha / 2)
+        self.type = "Shewart individuals chart"
+        self.criteria = criteria
+
+    def _phase_i(self) -> Tuple[float, float, float, float, list[float]]:
+        """Calculates and resulting datapoints, limits, location and scale from data
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        scale: float
+            The scale parameter
+        generated_datapoints: list[float]
+            The datapoints phase I as there is no aggregation involved in individual charts
+        """
+
+        moving_range = [
+            np.absolute(
+                np.array(self.data_phase_i[1:]) - np.array(self.data_phase_i[:-1])
+            )
+        ]
+
+        location, scale, lcl, ucl = self._calculate_chart_parameters(moving_range)
+
+        return float(location), float(lcl), float(ucl), float(scale), self.data_phase_i
+
+    def _calculate_chart_parameters(
+        self, moving_range: list[float]
+    ) -> Tuple[float, float, float, float]:
+        """Calculates the chart parameters
+
+        Parameters
+        ----------
+        moving_range: list[float]
+            The data to calculate the chart parameters from
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        scale: float
+            The scale parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        """
+        location = np.mean(self.data_phase_i).item()
+        if self.estimated:
+            scale = np.mean(moving_range)
+            lcl = location - 2.66 * scale
+            ucl = location + 2.66 * scale
+
+        else:
+            scale = statistics.stdev(self.data_phase_i)
+            lcl = location - self.z_score * scale
+            ucl = location + self.z_score * scale
+        return location, scale, lcl, ucl
+
+    def _phase_ii(self, data_phase_ii: list[float] = None) -> list[float]:
+        """Calculates the datapoints for calculation and plotting of phase_ii
+
+        Parameters
+        ----------
+        data_phase_ii: list[float]
+            If provided the data for the calculation of datapoints of phase_ii. Else self.data_phase_ii is used.
+
+        Returns
+        -------
+        list[float]
+            Calculated datapoints for phase_ii
+        """
+        if not data_phase_ii:
+            data_phase_ii = self.data_phase_ii.copy()
+        return data_phase_ii
+
+
+class MovingRangeChart(SPC):
+    """Perform SPC based on moving range approach
+
+    """
+
+    def __init__(
+        self,
+        data_phase_i: list[float] = None,
+        data_phase_ii: list[float] = None,
+        estimated: bool = False,
+        criteria: list[str] = None,
+    ) -> None:
+        """Initialize SPC based on moving range approach
+
+        Parameters
+        ----------
+        data_phase_i: list[float]
+            Data for phase I of SPC. If estimated = True data is set to data_phase_ii
+        data_phase_ii: list[float]
+            Data for phase_ii of SPC
+        estimated: bool
+            Whether to use the data of phase_ii to calculate the limits
+            or to use separate data from phase I. Defaults to False.
+        criteria: list[str]
+            Stability criteria considered to consider. Defaults to violation of limits, trend, run and the middle
+            third criterion
+
+        Returns
+        -------
+        None
+        """
+
+        if criteria is None:
+            criteria = ["out_of_limits", "trend", "run", "middle_third"]
+        super().__init__(data_phase_i, data_phase_ii, estimated)
+        self.type = "Moving range chart"
+        self.criteria = criteria
+
+    def _phase_i(self) -> Tuple[float, float, float, float, list[float]]:
+        """Calculates and resulting datapoints, limits, location and scale from data
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        scale: float
+            The scale parameter
+        generated_datapoints: list[float]
+            The datapoints phase I as there is no aggregation involved
+        """
+
+        datapoints_phase_i = np.absolute(
+            np.array(self.data_phase_i[1:]) - np.array(self.data_phase_i[:-1])
+        )
+
+        location, scale, lcl, ucl = self._calculate_chart_parameters(datapoints_phase_i)
+
+        return location, lcl, ucl, scale, datapoints_phase_i.tolist()
+
+    def _calculate_chart_parameters(
+        self, datapoints_phase_i: list[float]
+    ) -> Tuple[float, float, float, float]:
+        """Calculates the chart parameters
+
+        Parameters
+        ----------
+        datapoints_phase_i: list[float]
+            The data to calculate the chart parameters from
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        scale: float
+            The scale parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        """
+        if self.estimated:
+            scale = np.mean(datapoints_phase_i)
+            location = scale
+            ucl = 3.267 * np.mean(datapoints_phase_i)
+        else:
+            scale = statistics.stdev(self.data_phase_i)
+            location = 1.128 * scale
+            ucl = 3.686 * scale
+        lcl = 0
+        return location, scale, lcl, ucl
+
+    def _phase_ii(self, data_phase_ii: list[float] = None) -> list[float]:
+        """Calculates the datapoints for calculation and plotting of phase_ii
+
+        Parameters
+        ----------
+        data_phase_ii: list[float]
+            If provided the data for the calculation of datapoints of phase_ii. Else self.data_phase_ii is used.
+
+        Returns
+        -------
+        list[float]
+            Calculated datapoints for phase_ii
+        """
+
+        if not data_phase_ii:
+            data_phase_ii = self.data_phase_ii.copy()
+
+        datapoints_phase_ii = np.absolute(
+            np.array(data_phase_ii[1:]) - np.array(data_phase_ii[:-1])
+        )
+        return datapoints_phase_ii.tolist()
+
+
+class SChart(SPC):
+    """Perform SPC based on standard deviation approach
+
+    """
+
+    def __init__(
+        self,
+        data_phase_i: list[float] = None,
+        data_phase_ii: list[float] = None,
+        sample_size: int = 5,
+        batch_size: int = 5,
+        estimated: bool = False,
+        criteria: list[str] = None,
+    ) -> None:
+        """Initialize SPC based on standard deviation approach
+
+        Parameters
+        ----------
+        data_phase_i: list[float]
+            Data for phase I of SPC. If estimated = True data is set to data_phase_ii
+        data_phase_ii: list[float]
+            Data for phase_ii of SPC
+        estimated: bool
+            Whether to use the data of phase_ii to calculate the limits
+            or to use separate data from phase I. Defaults to False.
+        sample_size: int
+            Number of data-points for average calculation
+        batch_size: int
+            Size of data slices from which the samples are drawn
+        criteria: list[str]
+            Stability criteria considered to consider. Defaults to violation of limits, trend, run and the middle
+            third criterion
+
+        Returns
+        -------
+        None
+        """
+
+        if criteria is None:
+            criteria = ["out_of_limits", "trend", "run", "middle_third"]
+        assert batch_size >= sample_size
+        super().__init__(data_phase_i, data_phase_ii, estimated)
+        self.sample_size = sample_size
+        self.batch_size = batch_size
+        self.type = "S chart"
+        self.criteria = criteria
+
+    def _phase_i(self) -> Tuple[float, float, float, float, list[float]]:
+        """Calculates and resulting datapoints, limits, location and scale from data
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        scale: float
+            The scale parameter
+        generated_datapoints: list[float]
+            The datapoints representing the sample averages
+        """
+
+        data_phase_i = self.data_phase_i.copy()
+        data_samples = self.sample_data(data_phase_i)
+        stdev_samples = [statistics.stdev(data_sample) for data_sample in data_samples]
+
+        location, scale, lcl, ucl = self._calculate_chart_parameters(stdev_samples)
+        datapoints_phase_i = [
+            float(statistics.stdev(sample)) for sample in data_samples
+        ]
+        return float(location), float(lcl), float(ucl), float(scale), datapoints_phase_i
+
+    def _calculate_chart_parameters(
+        self, stdev_samples: list[float]
+    ) -> Tuple[float, float, float, float]:
+        """Calculates the chart parameters
+
+        Parameters
+        ----------
+        stdev_samples: list[float]
+            The data to calculate the chart parameters from
+
+        Returns
+        -------
+        location: float
+            The location parameter
+        scale: float
+            The scale parameter
+        lcl: float
+            The lower control limit
+        ucl: float
+            The upper control limit
+        """
+        scale = np.mean(stdev_samples).item()
+        if not self.estimated:
+            location = self.get_table_factor("C4", self.sample_size) * scale
+            if self.sample_size > 5:
+                lcl = self.get_table_factor("B5", self.sample_size) * scale
+            else:
+                lcl = 0
+            ucl = self.get_table_factor("B6", self.sample_size) * scale
+        else:
+            location = scale
+            if self.sample_size > 5:
+                lcl = self.get_table_factor("B3", self.sample_size) * scale
+            else:
+                lcl = 0
+            ucl = self.get_table_factor("B4", self.sample_size) * scale
+        return location, scale, lcl, ucl
+
+    def _phase_ii(self, data_phase_ii: list[float] = None) -> list[float]:
+        """Calculates the datapoints for calculation and plotting of phase_ii
+
+        Parameters
+        ----------
+        data_phase_ii: list[float]
+            If provided the data for the calculation of datapoints of phase_ii. Else self.data_phase_ii is used.
+
+        Returns
+        -------
+        list[float]
+            Calculated datapoints for phase_ii
+        """
+        if not data_phase_ii:
+            data_phase_ii = self.data_phase_ii.copy()
+        data_samples = self.sample_data(data_phase_ii)
+        return [statistics.stdev(sample) for sample in data_samples]
+
+    def sample_data(self, data):
+        """Splits data into slices of batch size and takes a sample of sample size of each slice.
+
+        Parameters
+        ----------
+        data: list[float]
+            The data to sample from
+
+        Returns
+        -------
+        list[list[float]]
+            Sampled data
+
+        """
+        data.reverse()
+        data = [
+            data[i * self.batch_size : (i + 1) * self.batch_size]
+            for i in range(int(len(data) // self.batch_size))
+        ]
+        data.reverse()
+        return [random.sample(values, self.sample_size) for values in data]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..f0fbf139f81019dd6aef79c562473a3b8700b217
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Chair for Intelligence in Quality Sensing | RWTH Aachen University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index f31f0db4148357a364c1da03d45d6e5e8928b71a..6c1676fe3b878b80e69c0af4db719ae38f6f38ef 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,15 @@
 # Generalized Statistical Process Control
 
+Source code to the publication "Generalized Statistical Process Control via 1D-ResNet Pretraining" by Tobias Schulze, Louis Huebser, Sebastian Beckschulte and Robert H. Schmitt (Laboratory for Machine Tools and Production Engineering, WZL of RWTH Aachen University).
 
 
-## Getting started
 
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
+## Structure
 
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
-
-## Add your files
-
-- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
-- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
-
-```
-cd existing_repo
-git remote add origin https://git-ce.rwth-aachen.de/wzl-iqs3/quality-insights/publications/generalized-statistical-process-control.git
-git branch -M main
-git push -uf origin main
-```
-
-## Integrate with your tools
-
-- [ ] [Set up project integrations](https://git-ce.rwth-aachen.de/wzl-iqs3/quality-insights/publications/generalized-statistical-process-control/-/settings/integrations)
-
-## Collaborate with your team
-
-- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
-- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
-- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
-- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
-- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
-
-## Test and Deploy
-
-Use the built-in continuous integration in GitLab.
-
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
-
-***
-
-# Editing this README
-
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
-
-## Suggestions for a good README
-
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
-
-## Name
-Choose a self-explaining name for your project.
-
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
-
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
-
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
-
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
-
-## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
-
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
-
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
-
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
-
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
-
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
-
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
+- data: Directory for data generated with G-SPC (data used in the publication are available at: https://zenodo.org/records/8246621)
+- G-SPC: Implementation of Generalized Statistical Process Control
+- scripts: Scripts used for the publication (actual use case is not included due to data privacy)
 
 ## License
-For open source projects, say how it is licensed.
+The code is subject to MIT license.
 
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
diff --git a/data/DATA.md b/data/DATA.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/data/table_factors.json b/data/table_factors.json
new file mode 100644
index 0000000000000000000000000000000000000000..02b5d344f58a8dcbdc9172f38be55b5a8b112a17
--- /dev/null
+++ b/data/table_factors.json
@@ -0,0 +1,209 @@
+{
+            "A": [
+                2.121,
+                1.732,
+                1.5,
+                1.342,
+                1.225,
+                1.134,
+                1.061,
+                1,
+                0.949,
+                0.905,
+                0.866,
+                0.832,
+                0.802,
+                0.775,
+                0.75,
+                0.728,
+                0.707,
+                0.688,
+                0.671,
+                0.655,
+                0.64,
+                0.626,
+                0.612,
+                0.6
+            ],
+            "A2": [
+                1.88,
+                1.023,
+                0.729,
+                0.577,
+                0.483,
+                0.419,
+                0.373,
+                0.337,
+                0.308,
+                0.285,
+                0.266,
+                0.249,
+                0.235,
+                0.223,
+                0.212,
+                0.203,
+                0.194,
+                0.187,
+                0.18,
+                0.173,
+                0.167,
+                0.162,
+                0.157,
+                0.153
+            ],
+            "A3": [
+                2.659,
+                1.954,
+                1.624,
+                1.427,
+                1.287,
+                1.182,
+                1.099,
+                1.032,
+                0.975,
+                0.927,
+                0.886,
+                0.85,
+                0.817,
+                0.789,
+                0.763,
+                0.739,
+                0.718,
+                0.698,
+                0.68,
+                0.663,
+                0.647,
+                0.633,
+                0.619,
+                0.606
+            ],
+            "B3": [
+                "None",
+                "None",
+                "None",
+                "None",
+                0.03,
+                0.118,
+                0.185,
+                0.239,
+                0.284,
+                0.321,
+                0.354,
+                0.382,
+                0.406,
+                0.428,
+                0.448,
+                0.466,
+                0.482,
+                0.497,
+                0.510,
+                0.523,
+                0.534,
+                0.545,
+                0.555,
+                0.565
+            ],
+            "B4": [
+                3.267,
+                2.568,
+                2.266,
+                2.089,
+                1.97,
+                1.882,
+                1.815,
+                1.761,
+                1.716,
+                1.679,
+                1.646,
+                1.618,
+                1.594,
+                1.572,
+                1.552,
+                1.534,
+                1.518,
+                1.503,
+                1.490,
+                1.477,
+                1.466,
+                1.455,
+                1.445,
+                1.435
+            ],
+            "B5": [
+                "None",
+                "None",
+                "None",
+                "None",
+                0.029,
+                0.113,
+                0.179,
+                0.232,
+                0.276,
+                0.313,
+                0.346,
+                0.374,
+                0.399,
+                0.421,
+                0.44,
+                0.458,
+                0.475,
+                0.49,
+                0.504,
+                0.516,
+                0.528,
+                0.539,
+                0.549,
+                0.559
+            ],
+            "B6": [
+                2.606,
+                2.276,
+                2.088,
+                1.964,
+                1.874,
+                1.806,
+                1.751,
+                1.707,
+                1.669,
+                1.637,
+                1.610,
+                1.585,
+                1.563,
+                1.544,
+                1.526,
+                1.511,
+                1.496,
+                1.483,
+                1.470,
+                1.459,
+                1.448,
+                1.438,
+                1.429,
+                1.420
+            ],
+            "C4": [
+                0.7979,
+                0.8862,
+                0.9213,
+                0.9400,
+                0.9515,
+                0.9594,
+                0.9650,
+                0.9693,
+                0.9727,
+                0.9754,
+                0.9776,
+                0.9794,
+                0.9810,
+                0.9823,
+                0.9835,
+                0.9845,
+                0.9854,
+                0.9862,
+                0.9876,
+                0.9882,
+                0.9887,
+                0.9892,
+                0.9896
+            ]
+        }
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0ad3ad9277773edc67e64e08a115683b814e8b21
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,13 @@
+numpy>=1.22.4
+pandas>=1.4.3
+scipy>=1.8.1
+tensorflow>=2.10.0
+docplex~=2.23.221
+tensorflow-probability~=0.18.0
+attrs>=21.4.0
+matplotlib>=3.5.2
+seaborn>=0.11.2
+tqdm~=4.64.0
+joblib~=1.2.0
+scikit-learn~=1.2.1
+setuptools~=62.1.0
\ No newline at end of file
diff --git a/scripts/benchmark/benchmark_cnn.ipynb b/scripts/benchmark/benchmark_cnn.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..528a66bf9257e8ca3f272fc7b682516e651979f2
--- /dev/null
+++ b/scripts/benchmark/benchmark_cnn.ipynb
@@ -0,0 +1,594 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "WARNING:tensorflow:From C:\\Users\\adam-ix592mtcb219gzd\\Anaconda3\\envs\\spc2ml_new\\lib\\site-packages\\keras\\src\\losses.py:2976: The name tf.losses.sparse_softmax_cross_entropy is deprecated. Please use tf.compat.v1.losses.sparse_softmax_cross_entropy instead.\n"
+     ]
+    }
+   ],
+   "source": [
+    "import warnings\n",
+    "\n",
+    "from scripts.benchmark.benchmark_utils import prepare_benchmark_data\n",
+    "\n",
+    "warnings.simplefilter(action='ignore', category=FutureWarning)\n",
+    "\n",
+    "import pandas\n",
+    "import matplotlib.pyplot as plt\n",
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from tensorflow import keras\n",
+    "\n",
+    "\n",
+    "from G_SPC.spc import ShewartIndividualsChart\n",
+    "from sklearn.metrics import confusion_matrix, recall_score, precision_score, ConfusionMatrixDisplay\n",
+    "\n",
+    "from G_SPC.nn.layer import MeanSubLayer\n",
+    "from G_SPC.nn.model import rebalanced_nok_mse\n",
+    "\n",
+    "%matplotlib notebook"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:08.971013Z",
+     "start_time": "2024-04-12T16:20:37.177784Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Benchmark SPC\n",
+    "### Load data"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "outputs": [],
+   "source": [
+    "x= pandas.read_csv(\n",
+    "        \"../../data/test/x.csv\",\n",
+    "        sep=\",\",\n",
+    "        decimal=\".\",\n",
+    "        dtype=float,\n",
+    "        header=None\n",
+    "    )\n",
+    "x = x[0].tolist()\n",
+    "y_true = pandas.read_csv(\n",
+    "        \"../../data/test/y.csv\",\n",
+    "        sep=\",\",\n",
+    "        decimal=\".\",\n",
+    "        dtype=float,\n",
+    "        header=None\n",
+    "    )\n",
+    "y_true = y_true[0].to_list()"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:09.016140Z",
+     "start_time": "2024-04-12T16:21:08.973046Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Perform SPC"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Indices of data-points violating limits: [31, 77, 108, 116, 133, 171, 187, 191, 196, 211, 219, 258, 264, 349, 367, 403, 457, 460, 485, 516, 522, 562, 571, 628, 654, 664, 675, 695, 699, 721, 731, 735, 737, 746, 752, 763, 782, 789, 798, 803, 805, 817, 832, 961, 964, 965, 978, 990, 992, 994, 995, 997, 998, 1019, 1021, 1051, 1059, 1060, 1061, 1066, 1068, 1070, 1084, 1087, 1094, 1097, 1099, 1100, 1107, 1117, 1130, 1140, 1145, 1164, 1174, 1198, 1207, 1209, 1217, 1218, 1224, 1229, 1234, 1245, 1252, 1260, 1261, 1263, 1269, 1272, 1273, 1278, 1283, 1285, 1287, 1291, 1294, 1300, 1302, 1303, 1306, 1307, 1318, 1319, 1323, 1334, 1336, 1337, 1340, 1343, 1344, 1348, 1349, 1352, 1355, 1358, 1365, 1370, 1371, 1372, 1378, 1379, 1381, 1383, 1384, 1387, 1388, 1389, 1390, 1391, 1395, 1396, 1399, 1401, 1402, 1412, 1418, 1421, 1423, 1425, 1428, 1429, 1431, 1441, 1442, 1444, 1445, 1452, 1454, 1459, 1464, 1466, 1470, 1473, 1474, 1476, 1477, 1478, 1480, 1485, 1487, 1492, 1493, 1497, 1498, 1500, 1501, 1504, 1505, 1507, 1508, 1509, 1510, 1511, 1512, 1515, 1516, 1518, 1521, 1522, 1524, 1525, 1526, 1529, 1532, 1535, 1538, 1539, 1541, 1543, 1544, 1545, 1548, 1552, 1553, 1554, 1555, 1557, 1561, 1564, 1565, 1566, 1569, 1570, 1572, 1574, 1575, 1576, 1580, 1581, 1582, 1585, 1586, 1587, 1588, 1589, 1590, 1592, 1593, 1595, 1597, 1598, 1602, 1603, 1604, 1607, 1608, 1614, 1618, 1619, 1622, 1624, 1625, 1631, 1632, 1633, 1635, 1637, 1639, 1641, 1642, 1644, 1649, 1651, 1652, 1653, 1655, 1656, 1657, 1659, 1662, 1664, 1666, 1667, 1668, 1669, 1672, 1674, 1675, 1677, 1678, 1679, 1680, 1682, 1683, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1694, 1695, 1698, 1699, 1700, 1703, 1704, 1705, 1706, 1708, 1709, 1710, 1711, 1712, 1714, 1716, 1717, 1718, 1719, 1720, 1722, 1724, 1726, 1728, 1729, 1730, 1731, 1732, 1734, 1738, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1751, 1753, 1754, 1755, 1756, 1758, 1759, 1761, 1762, 1763, 1764, 1765, 1767, 1769, 1772, 1773, 1774, 1775, 1776, 1777, 1779, 1781, 1782, 1783, 1784, 1785, 1786, 1789, 1790, 1791, 1793, 1794, 1796, 1800, 1801, 1802, 1804, 1805, 1806, 1807, 1808, 1810, 1812, 1813, 1815, 1816, 1817, 1818, 1820, 1821, 1822, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1832, 1833, 1835, 1837, 1838, 1840, 1841, 1842, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1852, 1853, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1868, 1870, 1871, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1903, 1906, 1907, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1918, 1919, 1921, 1922, 1923, 1924, 1925, 1926, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1969, 1971, 1972, 1973, 1974, 1976, 1977, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2001, 2009, 2071, 2117, 2139, 2199, 2215, 2222, 2245, 2250, 2267, 2293, 2317, 2333, 2346, 2356, 2376, 2380, 2392, 2425, 2438, 2445, 2453, 2459, 2465, 2490, 2517, 2522, 2534, 2575, 2601, 2620, 2653, 2669, 2683, 2686, 2688, 2695, 2701, 2729, 2752, 2755, 2761, 2779, 2894, 2914, 2926, 2941, 2956, 2999, 3037, 3055, 3056, 3059, 3065, 3067, 3068, 3087, 3100, 3116, 3124, 3125, 3128, 3137, 3139, 3140, 3146, 3147, 3148, 3150, 3163, 3166, 3172, 3179, 3181, 3189, 3201, 3207, 3213, 3216, 3223, 3224, 3225, 3229, 3235, 3240, 3244, 3249, 3251, 3254, 3259, 3261, 3265, 3267, 3269, 3272, 3274, 3277, 3286, 3288, 3291, 3293, 3294, 3295, 3296, 3300, 3301, 3311, 3313, 3320, 3322, 3325, 3326, 3330, 3331, 3334, 3338, 3339, 3342, 3346, 3347, 3348, 3354, 3358, 3364, 3365, 3366, 3367, 3371, 3372, 3373, 3376, 3380, 3384, 3387, 3388, 3391, 3395, 3397, 3399, 3400, 3407, 3410, 3411, 3412, 3413, 3415, 3418, 3419, 3421, 3426, 3428, 3429, 3434, 3436, 3437, 3438, 3442, 3443, 3447, 3448, 3449, 3451, 3452, 3454, 3455, 3460, 3461, 3466, 3468, 3471, 3472, 3474, 3479, 3480, 3482, 3488, 3489, 3490, 3496, 3497, 3503, 3504, 3505, 3507, 3510, 3512, 3515, 3519, 3521, 3522, 3526, 3533, 3537, 3539, 3540, 3541, 3542, 3544, 3547, 3550, 3552, 3553, 3555, 3561, 3562, 3564, 3566, 3567, 3568, 3569, 3570, 3573, 3578, 3580, 3581, 3582, 3584, 3586, 3587, 3593, 3594, 3596, 3599, 3602, 3606, 3607, 3608, 3609, 3611, 3612, 3615, 3616, 3617, 3619, 3621, 3622, 3625, 3626, 3629, 3632, 3633, 3635, 3637, 3638, 3640, 3643, 3645, 3647, 3649, 3650, 3652, 3653, 3654, 3655, 3656, 3660, 3661, 3663, 3666, 3670, 3672, 3675, 3676, 3677, 3678, 3681, 3682, 3683, 3685, 3688, 3689, 3690, 3691, 3700, 3701, 3702, 3703, 3704, 3706, 3707, 3709, 3711, 3713, 3714, 3717, 3718, 3720, 3721, 3722, 3723, 3724, 3726, 3728, 3730, 3732, 3733, 3735, 3736, 3737, 3738, 3739, 3742, 3744, 3745, 3746, 3748, 3749, 3750, 3751, 3752, 3755, 3758, 3759, 3760, 3761, 3763, 3764, 3766, 3767, 3768, 3770, 3771, 3773, 3775, 3776, 3778, 3779, 3780, 3781, 3783, 3784, 3785, 3789, 3791, 3792, 3793, 3795, 3796, 3797, 3798, 3799, 3800, 3801, 3807, 3808, 3810, 3811, 3812, 3813, 3815, 3816, 3819, 3820, 3821, 3822, 3823, 3824, 3825, 3826, 3828, 3829, 3830, 3832, 3833, 3835, 3836, 3837, 3838, 3840, 3843, 3844, 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3861, 3863, 3865, 3866, 3868, 3869, 3870, 3871, 3874, 3875, 3876, 3878, 3882, 3883, 3884, 3885, 3886, 3887, 3888, 3889, 3890, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3909, 3910, 3911, 3912, 3913, 3914, 3915, 3917, 3918, 3919, 3920, 3921, 3922, 3923, 3924, 3925, 3926, 3927, 3928, 3929, 3930, 3931, 3932, 3933, 3934, 3935, 3936, 3937, 3938, 3940, 3941, 3942, 3943, 3944, 3945, 3946, 3947, 3948, 3949, 3950, 3951, 3953, 3954, 3955, 3956, 3957, 3959, 3960, 3962, 3963, 3964, 3965, 3966, 3967, 3968, 3969, 3970, 3971, 3973, 3974, 3976, 3979, 3980, 3982, 3983, 3984, 3985, 3986, 3987, 3988, 3989, 3990, 3992, 3993, 3994, 3995, 3996, 3997, 3998, 3999, 4007, 4013, 4031, 4036, 4054, 4064, 4074, 4123, 4124, 4125, 4127, 4170, 4186, 4187, 4219, 4247, 4269, 4279, 4337, 4342, 4357, 4360, 4374, 4403, 4410, 4480, 4488, 4504, 4509, 4527, 4540, 4541, 4549, 4560, 4626, 4695, 4707, 4736, 4745, 4772, 4802, 4808, 4819, 4854, 4910, 4931, 4937, 4979, 4994, 4995, 4996, 4997, 4998, 4999, 5025, 5028, 5029, 5079, 5083, 5092, 5095, 5102, 5103, 5104, 5108, 5113, 5128, 5140, 5141, 5144, 5145, 5148, 5156, 5157, 5161, 5163, 5165, 5166, 5167, 5175, 5186, 5201, 5205, 5209, 5210, 5220, 5223, 5231, 5235, 5241, 5242, 5245, 5255, 5256, 5257, 5266, 5271, 5274, 5293, 5301, 5304, 5306, 5308, 5309, 5311, 5313, 5315, 5316, 5324, 5325, 5330, 5335, 5339, 5342, 5345, 5347, 5363, 5365, 5381, 5383, 5384, 5386, 5391, 5392, 5396, 5400, 5401, 5404, 5409, 5411, 5413, 5414, 5418, 5419, 5420, 5423, 5427, 5433, 5434, 5435, 5436, 5437, 5438, 5440, 5441, 5444, 5447, 5450, 5453, 5456, 5461, 5462, 5465, 5468, 5474, 5475, 5480, 5482, 5484, 5485, 5488, 5490, 5495, 5496, 5501, 5502, 5504, 5507, 5508, 5511, 5514, 5518, 5522, 5524, 5528, 5529, 5531, 5533, 5535, 5539, 5543, 5547, 5548, 5550, 5551, 5554, 5555, 5556, 5559, 5564, 5566, 5567, 5569, 5572, 5575, 5577, 5579, 5581, 5582, 5584, 5587, 5588, 5589, 5590, 5591, 5593, 5594, 5595, 5597, 5598, 5599, 5602, 5607, 5608, 5609, 5610, 5612, 5615, 5619, 5620, 5624, 5626, 5632, 5633, 5635, 5637, 5639, 5643, 5644, 5645, 5647, 5648, 5649, 5650, 5651, 5653, 5655, 5656, 5660, 5662, 5663, 5664, 5665, 5667, 5669, 5670, 5671, 5674, 5675, 5677, 5678, 5680, 5685, 5686, 5687, 5691, 5692, 5693, 5695, 5696, 5697, 5699, 5700, 5703, 5704, 5706, 5707, 5709, 5710, 5712, 5713, 5714, 5715, 5717, 5718, 5720, 5722, 5724, 5726, 5728, 5733, 5735, 5736, 5737, 5741, 5742, 5743, 5744, 5745, 5747, 5748, 5749, 5751, 5753, 5757, 5758, 5760, 5763, 5765, 5768, 5770, 5771, 5772, 5773, 5774, 5775, 5776, 5777, 5779, 5780, 5783, 5785, 5786, 5787, 5788, 5789, 5790, 5791, 5792, 5793, 5795, 5796, 5798, 5799, 5800, 5801, 5802, 5803, 5804, 5807, 5809, 5813, 5814, 5816, 5817, 5818, 5821, 5822, 5823, 5825, 5827, 5830, 5831, 5832, 5833, 5834, 5836, 5838, 5839, 5840, 5841, 5843, 5844, 5845, 5846, 5847, 5848, 5852, 5855, 5857, 5858, 5859, 5861, 5862, 5863, 5864, 5865, 5866, 5868, 5870, 5871, 5874, 5875, 5877, 5878, 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, 5887, 5889, 5890, 5891, 5893, 5894, 5895, 5896, 5897, 5898, 5899, 5900, 5902, 5904, 5906, 5908, 5909, 5910, 5912, 5913, 5914, 5915, 5917, 5918, 5919, 5920, 5921, 5924, 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, 5933, 5935, 5936, 5937, 5938, 5939, 5941, 5943, 5946, 5948, 5949, 5951, 5953, 5954, 5955, 5956, 5957, 5958, 5959, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, 5970, 5971, 5972, 5974, 5975, 5976, 5979, 5980, 5982, 5983, 5984, 5985, 5987, 5988, 5990, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999, 6023, 6048, 6099, 6118, 6130, 6163, 6165, 6175, 6197, 6199, 6225, 6228, 6255, 6262, 6278, 6315, 6334, 6342, 6415, 6461, 6483, 6490, 6498, 6530, 6532, 6535, 6538, 6557, 6579, 6601, 6615, 6635, 6651, 6757, 6766, 6775, 6789, 6793, 6807, 6826, 6847, 6850, 6867, 6911, 6963, 6969, 6980, 6985, 6987, 6988, 6992, 6993, 6995, 6996, 6999, 7007, 7014, 7030, 7033, 7057, 7065, 7068, 7092, 7098, 7100, 7117, 7120, 7143, 7145, 7146, 7169, 7179, 7180, 7185, 7192, 7194, 7202, 7205, 7210, 7213, 7216, 7219, 7224, 7227, 7238, 7240, 7243, 7247, 7248, 7250, 7251, 7252, 7254, 7256, 7265, 7267, 7277, 7280, 7286, 7290, 7298, 7300, 7305, 7306, 7314, 7316, 7323, 7324, 7327, 7329, 7333, 7334, 7337, 7340, 7341, 7343, 7350, 7357, 7358, 7362, 7363, 7374, 7375, 7376, 7380, 7384, 7388, 7392, 7393, 7395, 7398, 7404, 7406, 7410, 7413, 7415, 7416, 7417, 7418, 7424, 7426, 7427, 7429, 7432, 7434, 7435, 7436, 7439, 7441, 7444, 7445, 7446, 7447, 7450, 7459, 7460, 7461, 7466, 7467, 7468, 7471, 7476, 7477, 7480, 7482, 7485, 7486, 7487, 7489, 7497, 7501, 7502, 7505, 7506, 7508, 7509, 7511, 7512, 7516, 7519, 7522, 7523, 7525, 7527, 7532, 7534, 7536, 7541, 7542, 7544, 7550, 7551, 7552, 7556, 7557, 7561, 7563, 7565, 7567, 7568, 7573, 7576, 7578, 7581, 7584, 7585, 7587, 7589, 7590, 7593, 7594, 7595, 7596, 7597, 7598, 7599, 7601, 7602, 7603, 7605, 7608, 7611, 7612, 7613, 7619, 7621, 7623, 7628, 7629, 7630, 7631, 7632, 7634, 7639, 7640, 7641, 7642, 7643, 7645, 7646, 7648, 7649, 7650, 7651, 7653, 7654, 7656, 7657, 7660, 7665, 7674, 7675, 7676, 7677, 7679, 7683, 7686, 7689, 7691, 7693, 7694, 7696, 7698, 7701, 7702, 7703, 7704, 7705, 7706, 7708, 7709, 7710, 7712, 7714, 7717, 7720, 7722, 7723, 7724, 7725, 7726, 7727, 7728, 7730, 7731, 7733, 7736, 7738, 7739, 7740, 7744, 7747, 7749, 7751, 7752, 7753, 7755, 7756, 7758, 7760, 7763, 7764, 7765, 7766, 7768, 7769, 7770, 7772, 7777, 7778, 7779, 7780, 7781, 7783, 7784, 7785, 7786, 7787, 7789, 7792, 7794, 7795, 7796, 7798, 7800, 7801, 7802, 7803, 7806, 7807, 7808, 7809, 7810, 7811, 7812, 7813, 7815, 7816, 7817, 7818, 7820, 7821, 7822, 7823, 7824, 7825, 7826, 7829, 7831, 7832, 7833, 7834, 7835, 7836, 7837, 7838, 7839, 7840, 7841, 7842, 7843, 7844, 7845, 7846, 7847, 7849, 7850, 7851, 7852, 7853, 7860, 7861, 7863, 7866, 7867, 7868, 7869, 7870, 7872, 7873, 7874, 7875, 7877, 7880, 7882, 7883, 7884, 7885, 7886, 7888, 7889, 7891, 7892, 7893, 7894, 7895, 7896, 7897, 7900, 7901, 7902, 7903, 7904, 7905, 7906, 7907, 7908, 7909, 7912, 7913, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7925, 7927, 7928, 7929, 7930, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7943, 7944, 7945, 7946, 7947, 7948, 7951, 7952, 7953, 7955, 7956, 7957, 7958, 7959, 7960, 7961, 7962, 7964, 7966, 7967, 7970, 7971, 7972, 7974, 7975, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7986, 7987, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8015, 8030, 8036, 8038, 8105, 8119, 8124, 8149, 8206, 8266, 8277, 8278, 8304, 8330, 8333, 8338, 8376, 8426, 8430, 8455, 8462, 8475, 8508, 8521, 8533, 8556, 8566, 8587, 8594, 8598, 8605, 8616, 8630, 8665, 8667, 8682, 8688, 8702, 8727, 8738, 8743, 8754, 8764, 8766, 8801, 8808, 8823, 8855, 8878, 8885, 8912, 8920, 8923, 8954, 8985, 8987, 8988, 8994, 8995, 8996, 8997, 8998, 8999, 9017, 9037, 9043, 9056, 9060, 9066, 9083, 9092, 9118, 9124, 9130, 9146, 9148, 9153, 9158, 9164, 9166, 9173, 9176, 9181, 9184, 9185, 9189, 9191, 9192, 9202, 9213, 9217, 9219, 9220, 9222, 9224, 9227, 9229, 9234, 9269, 9278, 9283, 9284, 9299, 9304, 9305, 9306, 9307, 9311, 9314, 9315, 9319, 9323, 9326, 9332, 9341, 9342, 9348, 9349, 9354, 9357, 9359, 9360, 9365, 9366, 9373, 9375, 9376, 9386, 9388, 9391, 9396, 9398, 9402, 9403, 9404, 9406, 9409, 9410, 9411, 9415, 9419, 9420, 9421, 9425, 9433, 9439, 9440, 9441, 9461, 9465, 9467, 9472, 9473, 9474, 9481, 9488, 9495, 9496, 9500, 9502, 9505, 9522, 9524, 9525, 9526, 9528, 9529, 9531, 9535, 9537, 9540, 9542, 9543, 9544, 9547, 9548, 9549, 9551, 9552, 9553, 9556, 9558, 9559, 9562, 9571, 9573, 9577, 9578, 9580, 9586, 9587, 9590, 9592, 9594, 9598, 9599, 9605, 9609, 9613, 9617, 9620, 9624, 9627, 9630, 9632, 9637, 9640, 9643, 9645, 9651, 9656, 9658, 9659, 9660, 9662, 9663, 9667, 9668, 9671, 9675, 9676, 9677, 9684, 9685, 9686, 9687, 9689, 9690, 9691, 9693, 9696, 9697, 9698, 9701, 9702, 9703, 9713, 9714, 9722, 9726, 9731, 9735, 9739, 9742, 9744, 9745, 9747, 9753, 9754, 9757, 9761, 9763, 9764, 9767, 9768, 9771, 9772, 9773, 9777, 9779, 9780, 9785, 9786, 9791, 9792, 9794, 9795, 9798, 9803, 9805, 9806, 9810, 9812, 9813, 9816, 9817, 9819, 9821, 9822, 9825, 9828, 9830, 9831, 9832, 9835, 9841, 9845, 9846, 9847, 9848, 9849, 9850, 9851, 9852, 9853, 9854, 9856, 9857, 9859, 9860, 9862, 9868, 9869, 9870, 9873, 9874, 9876, 9877, 9879, 9880, 9881, 9882, 9883, 9889, 9891, 9894, 9897, 9898, 9899, 9900, 9901, 9902, 9903, 9905, 9906, 9907, 9911, 9913, 9914, 9917, 9922, 9923, 9927, 9929, 9930, 9931, 9933, 9935, 9937, 9939, 9940, 9943, 9944, 9948, 9949, 9950, 9951, 9952, 9956, 9958, 9959, 9963, 9966, 9969, 9973, 9975, 9976, 9978, 9979, 9980, 9985, 9987, 9989, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9998, 9999, 10001, 10008, 10031, 10069, 10087, 10125, 10153, 10202, 10206, 10211, 10237, 10295, 10297, 10317, 10325, 10329, 10344, 10348, 10350, 10358, 10359, 10365, 10405, 10413, 10422, 10434, 10462, 10472, 10508, 10555, 10572, 10580, 10589, 10603, 10605, 10614, 10625, 10627, 10637, 10647, 10672, 10691, 10706, 10722, 10737, 10757, 10760, 10769, 10786, 10809, 10819, 10840, 10852, 10879, 10896, 10907, 10919, 10947, 10954, 10969, 10981, 10983, 10984, 10986, 10987, 10988, 10990, 10992, 10998, 10999, 11004, 11035, 11058, 11071, 11093, 11100, 11103, 11122, 11125, 11126, 11134, 11136, 11155, 11175, 11176, 11183, 11196, 11206, 11218, 11219, 11230, 11233, 11238, 11247, 11249, 11260, 11261, 11262, 11265, 11280, 11286, 11292, 11293, 11294, 11302, 11305, 11316, 11322, 11323, 11325, 11327, 11333, 11334, 11335, 11337, 11341, 11345, 11346, 11348, 11355, 11361, 11369, 11370, 11371, 11379, 11389, 11390, 11395, 11396, 11397, 11399, 11400, 11402, 11403, 11405, 11406, 11407, 11409, 11410, 11413, 11415, 11416, 11421, 11422, 11423, 11426, 11427, 11428, 11432, 11443, 11446, 11448, 11450, 11451, 11458, 11463, 11467, 11469, 11470, 11472, 11475, 11478, 11479, 11484, 11485, 11487, 11498, 11500, 11502, 11503, 11506, 11507, 11510, 11511, 11513, 11518, 11520, 11524, 11526, 11527, 11529, 11537, 11540, 11543, 11547, 11548, 11552, 11555, 11556, 11560, 11562, 11563, 11566, 11568, 11570, 11572, 11574, 11578, 11579, 11580, 11584, 11585, 11590, 11595, 11596, 11597, 11605, 11609, 11611, 11613, 11614, 11615, 11620, 11621, 11622, 11628, 11629, 11630, 11633, 11634, 11635, 11637, 11642, 11643, 11646, 11649, 11650, 11651, 11653, 11654, 11659, 11660, 11661, 11662, 11666, 11667, 11674, 11676, 11678, 11679, 11681, 11683, 11684, 11685, 11686, 11689, 11690, 11692, 11694, 11695, 11697, 11700, 11701, 11702, 11703, 11705, 11706, 11707, 11709, 11712, 11715, 11716, 11718, 11721, 11723, 11724, 11726, 11727, 11729, 11732, 11733, 11735, 11737, 11738, 11739, 11740, 11743, 11746, 11749, 11751, 11752, 11754, 11755, 11756, 11757, 11758, 11759, 11760, 11761, 11762, 11763, 11764, 11765, 11767, 11768, 11769, 11770, 11771, 11772, 11775, 11776, 11779, 11780, 11782, 11783, 11784, 11785, 11788, 11789, 11791, 11792, 11793, 11794, 11799, 11801, 11802, 11803, 11807, 11808, 11811, 11813, 11814, 11816, 11817, 11820, 11821, 11822, 11823, 11824, 11825, 11826, 11830, 11831, 11832, 11834, 11835, 11836, 11837, 11838, 11839, 11841, 11842, 11844, 11845, 11848, 11849, 11851, 11852, 11853, 11854, 11855, 11859, 11860, 11861, 11862, 11863, 11865, 11866, 11867, 11871, 11872, 11873, 11874, 11875, 11877, 11878, 11880, 11882, 11883, 11888, 11889, 11890, 11891, 11892, 11894, 11895, 11896, 11897, 11898, 11900, 11901, 11902, 11903, 11904, 11905, 11906, 11907, 11908, 11910, 11912, 11913, 11915, 11916, 11917, 11919, 11920, 11922, 11925, 11926, 11930, 11931, 11932, 11933, 11935, 11936, 11937, 11940, 11942, 11943, 11944, 11946, 11947, 11948, 11949, 11950, 11952, 11955, 11956, 11957, 11959, 11960, 11961, 11962, 11963, 11964, 11967, 11969, 11970, 11971, 11972, 11976, 11977, 11978, 11980, 11982, 11983, 11984, 11985, 11986, 11987, 11988, 11989, 11991, 11992, 11993, 11994, 11995, 11998, 12016, 12027, 12030, 12036, 12090, 12115, 12129, 12186, 12188, 12192, 12348, 12359, 12367, 12381, 12388, 12393, 12431, 12445, 12447, 12454, 12460, 12461, 12481, 12483, 12485, 12487, 12494, 12502, 12504, 12520, 12533, 12534, 12547, 12639, 12665, 12666, 12680, 12695, 12704, 12751, 12755, 12813, 12818, 12825, 12835, 12837, 12853, 12908, 12963, 12968, 12987, 12990, 12991, 12992, 12993, 12996, 12997, 12998, 12999, 13024, 13046, 13056, 13075, 13079, 13084, 13102, 13103, 13107, 13116, 13119, 13120, 13123, 13124, 13126, 13128, 13129, 13131, 13142, 13149, 13151, 13152, 13154, 13156, 13158, 13162, 13167, 13184, 13194, 13195, 13196, 13199, 13202, 13204, 13205, 13211, 13215, 13217, 13220, 13227, 13237, 13242, 13244, 13248, 13252, 13255, 13257, 13260, 13261, 13265, 13266, 13268, 13269, 13280, 13283, 13285, 13300, 13304, 13308, 13309, 13317, 13319, 13320, 13321, 13322, 13324, 13325, 13328, 13329, 13336, 13338, 13340, 13341, 13342, 13344, 13345, 13350, 13352, 13355, 13357, 13359, 13360, 13361, 13366, 13370, 13372, 13375, 13376, 13378, 13381, 13385, 13387, 13391, 13393, 13397, 13403, 13405, 13406, 13407, 13408, 13412, 13414, 13415, 13417, 13418, 13423, 13424, 13425, 13426, 13427, 13429, 13434, 13436, 13438, 13440, 13442, 13447, 13450, 13451, 13456, 13457, 13463, 13468, 13473, 13474, 13477, 13478, 13480, 13482, 13483, 13484, 13485, 13492, 13493, 13495, 13496, 13499, 13500, 13503, 13504, 13505, 13506, 13507, 13509, 13511, 13513, 13514, 13515, 13516, 13517, 13518, 13519, 13521, 13524, 13527, 13530, 13533, 13535, 13537, 13539, 13540, 13542, 13544, 13545, 13546, 13548, 13551, 13552, 13554, 13555, 13557, 13559, 13561, 13564, 13567, 13570, 13572, 13573, 13576, 13578, 13579, 13580, 13581, 13582, 13583, 13584, 13585, 13587, 13588, 13589, 13592, 13594, 13595, 13596, 13597, 13599, 13601, 13603, 13605, 13606, 13611, 13613, 13614, 13617, 13622, 13623, 13626, 13627, 13629, 13630, 13632, 13634, 13636, 13637, 13641, 13643, 13645, 13646, 13647, 13648, 13649, 13650, 13651, 13652, 13653, 13655, 13656, 13657, 13659, 13660, 13661, 13663, 13664, 13668, 13670, 13671, 13672, 13673, 13678, 13679, 13680, 13683, 13685, 13687, 13689, 13690, 13691, 13692, 13693, 13694, 13695, 13696, 13698, 13699, 13701, 13702, 13703, 13705, 13706, 13707, 13708, 13710, 13711, 13712, 13713, 13715, 13720, 13721, 13723, 13725, 13728, 13729, 13730, 13731, 13733, 13734, 13736, 13737, 13738, 13740, 13741, 13743, 13744, 13745, 13746, 13747, 13748, 13750, 13751, 13754, 13755, 13756, 13757, 13758, 13759, 13760, 13761, 13762, 13763, 13765, 13766, 13768, 13769, 13771, 13772, 13773, 13775, 13777, 13778, 13779, 13781, 13783, 13784, 13785, 13788, 13789, 13790, 13792, 13793, 13794, 13795, 13796, 13797, 13798, 13800, 13801, 13803, 13804, 13805, 13807, 13809, 13810, 13812, 13813, 13814, 13815, 13816, 13817, 13820, 13821, 13822, 13823, 13824, 13829, 13830, 13831, 13832, 13833, 13834, 13835, 13838, 13839, 13840, 13841, 13842, 13844, 13845, 13846, 13847, 13848, 13849, 13850, 13851, 13852, 13855, 13856, 13857, 13859, 13860, 13862, 13863, 13865, 13866, 13867, 13868, 13869, 13870, 13872, 13874, 13875, 13876, 13877, 13878, 13879, 13880, 13881, 13882, 13883, 13884, 13885, 13886, 13887, 13888, 13889, 13890, 13893, 13894, 13895, 13896, 13897, 13898, 13899, 13901, 13903, 13904, 13905, 13906, 13907, 13908, 13909, 13910, 13912, 13914, 13915, 13916, 13917, 13918, 13919, 13920, 13921, 13923, 13924, 13925, 13926, 13927, 13929, 13931, 13932, 13933, 13934, 13935, 13936, 13937, 13938, 13940, 13941, 13942, 13943, 13944, 13945, 13947, 13948, 13949, 13950, 13952, 13954, 13955, 13956, 13957, 13958, 13959, 13960, 13961, 13962, 13963, 13964, 13965, 13966, 13967, 13968, 13969, 13971, 13973, 13974, 13975, 13976, 13979, 13980, 13981, 13982, 13983, 13984, 13985, 13987, 13988, 13989, 13990, 13991, 13992, 13993, 13994, 13997, 13998, 13999, 14001, 14017, 14030, 14032, 14039, 14057, 14059, 14102, 14119, 14128, 14164, 14166, 14174, 14185, 14210, 14220, 14221, 14237, 14288, 14314, 14327, 14330, 14343, 14346, 14381, 14455, 14467, 14469, 14471, 14539, 14550, 14584, 14611, 14616, 14624, 14637, 14662, 14666, 14702, 14707, 14737, 14795, 14800, 14803, 14806, 14820, 14882, 14905, 14931, 14940, 14958, 14990, 14992, 14993, 14998, 14999, 15042, 15057, 15066, 15072, 15077, 15099, 15103, 15105, 15106, 15111, 15116, 15117, 15119, 15134, 15135, 15143, 15163, 15167, 15170, 15172, 15175, 15187, 15212, 15213, 15217, 15219, 15226, 15228, 15229, 15232, 15235, 15238, 15244, 15249, 15251, 15262, 15264, 15276, 15281, 15286, 15291, 15293, 15305, 15306, 15307, 15319, 15322, 15323, 15325, 15331, 15337, 15343, 15344, 15347, 15348, 15350, 15352, 15353, 15354, 15355, 15356, 15361, 15365, 15367, 15368, 15370, 15371, 15375, 15376, 15377, 15380, 15384, 15386, 15387, 15389, 15396, 15397, 15400, 15401, 15405, 15407, 15410, 15412, 15418, 15419, 15428, 15429, 15436, 15438, 15442, 15451, 15453, 15455, 15459, 15460, 15461, 15462, 15463, 15473, 15478, 15481, 15483, 15484, 15489, 15495, 15496, 15500, 15502, 15503, 15505, 15507, 15510, 15514, 15515, 15518, 15520, 15521, 15523, 15524, 15531, 15532, 15534, 15535, 15538, 15539, 15540, 15541, 15544, 15545, 15546, 15547, 15550, 15551, 15552, 15554, 15555, 15560, 15561, 15562, 15564, 15566, 15568, 15571, 15573, 15574, 15579, 15580, 15581, 15585, 15586, 15588, 15591, 15592, 15593, 15595, 15596, 15599, 15602, 15606, 15608, 15609, 15613, 15614, 15616, 15617, 15619, 15620, 15622, 15623, 15625, 15628, 15632, 15633, 15634, 15635, 15642, 15643, 15644, 15647, 15648, 15650, 15653, 15654, 15656, 15657, 15658, 15659, 15660, 15661, 15662, 15664, 15667, 15668, 15670, 15671, 15672, 15674, 15676, 15678, 15680, 15681, 15682, 15684, 15685, 15686, 15688, 15690, 15692, 15693, 15695, 15698, 15702, 15703, 15704, 15705, 15706, 15707, 15712, 15713, 15715, 15716, 15719, 15720, 15721, 15722, 15723, 15725, 15726, 15728, 15730, 15733, 15734, 15735, 15736, 15738, 15740, 15741, 15743, 15744, 15745, 15746, 15747, 15748, 15749, 15750, 15751, 15752, 15753, 15754, 15756, 15757, 15759, 15763, 15764, 15767, 15769, 15770, 15772, 15773, 15774, 15776, 15777, 15778, 15779, 15780, 15781, 15782, 15783, 15784, 15785, 15786, 15788, 15789, 15790, 15792, 15793, 15794, 15796, 15797, 15798, 15799, 15800, 15803, 15805, 15806, 15807, 15808, 15809, 15811, 15812, 15814, 15815, 15816, 15817, 15818, 15820, 15821, 15823, 15827, 15828, 15829, 15831, 15834, 15836, 15837, 15838, 15839, 15840, 15842, 15844, 15847, 15848, 15849, 15850, 15851, 15852, 15853, 15854, 15855, 15857, 15858, 15859, 15861, 15862, 15864, 15865, 15866, 15867, 15868, 15869, 15872, 15874, 15875, 15876, 15877, 15878, 15879, 15880, 15881, 15883, 15885, 15886, 15887, 15888, 15889, 15890, 15892, 15893, 15895, 15897, 15898, 15899, 15900, 15901, 15903, 15907, 15910, 15911, 15913, 15914, 15915, 15916, 15917, 15918, 15919, 15920, 15921, 15923, 15924, 15925, 15926, 15928, 15929, 15931, 15932, 15933, 15934, 15937, 15939, 15940, 15941, 15942, 15944, 15945, 15947, 15950, 15952, 15953, 15954, 15955, 15956, 15958, 15959, 15961, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15974, 15975, 15976, 15978, 15980, 15982, 15984, 15985, 15986, 15987, 15988, 15989, 15990, 15991, 15992, 15993, 15994, 15995, 15996, 15997, 15998, 16010, 16019, 16022, 16046, 16076, 16106, 16111, 16119, 16120, 16134, 16155, 16157, 16201, 16205, 16220, 16222, 16258, 16260, 16275, 16284, 16287, 16305, 16307, 16313, 16342, 16368, 16371, 16447, 16454, 16493, 16513, 16524, 16575, 16583, 16585, 16597, 16622, 16646, 16675, 16682, 16748, 16767, 16831, 16832, 16838, 16841, 16880, 16885, 16891, 16895, 16906, 16945, 16952, 16965, 16970, 16975, 16978, 16995, 16996, 16997, 17005, 17025, 17026, 17030, 17040, 17050, 17077, 17081, 17086, 17110, 17122, 17127, 17137, 17157, 17159, 17160, 17169, 17174, 17188, 17195, 17196, 17197, 17202, 17213, 17214, 17217, 17218, 17220, 17231, 17232, 17245, 17246, 17249, 17257, 17261, 17262, 17271, 17272, 17273, 17274, 17277, 17281, 17284, 17286, 17288, 17292, 17297, 17310, 17312, 17315, 17321, 17322, 17325, 17327, 17330, 17344, 17345, 17347, 17354, 17355, 17361, 17369, 17373, 17375, 17376, 17378, 17389, 17390, 17393, 17395, 17397, 17401, 17402, 17403, 17411, 17414, 17420, 17422, 17423, 17429, 17440, 17443, 17445, 17448, 17449, 17451, 17452, 17456, 17457, 17459, 17465, 17467, 17469, 17470, 17473, 17475, 17476, 17478, 17480, 17482, 17484, 17488, 17490, 17492, 17493, 17494, 17495, 17496, 17498, 17502, 17504, 17505, 17511, 17515, 17517, 17521, 17526, 17529, 17530, 17532, 17536, 17537, 17539, 17543, 17544, 17548, 17554, 17555, 17556, 17558, 17559, 17561, 17562, 17564, 17567, 17568, 17569, 17572, 17574, 17575, 17579, 17580, 17582, 17584, 17585, 17586, 17587, 17588, 17589, 17599, 17602, 17604, 17605, 17606, 17607, 17609, 17611, 17612, 17613, 17614, 17620, 17622, 17624, 17625, 17626, 17628, 17629, 17630, 17631, 17633, 17634, 17636, 17637, 17638, 17643, 17644, 17646, 17647, 17649, 17650, 17651, 17652, 17653, 17655, 17656, 17657, 17658, 17659, 17662, 17668, 17669, 17670, 17672, 17676, 17678, 17679, 17680, 17681, 17684, 17685, 17686, 17687, 17688, 17689, 17691, 17692, 17695, 17697, 17698, 17700, 17701, 17702, 17703, 17704, 17705, 17709, 17712, 17713, 17716, 17719, 17720, 17722, 17723, 17726, 17727, 17729, 17731, 17732, 17733, 17734, 17737, 17738, 17740, 17741, 17742, 17743, 17744, 17746, 17751, 17753, 17754, 17755, 17756, 17757, 17758, 17759, 17760, 17761, 17762, 17763, 17769, 17772, 17773, 17774, 17775, 17777, 17779, 17780, 17781, 17782, 17783, 17784, 17786, 17787, 17788, 17790, 17791, 17792, 17794, 17797, 17798, 17801, 17802, 17804, 17808, 17810, 17812, 17815, 17816, 17817, 17819, 17821, 17822, 17823, 17824, 17826, 17827, 17830, 17832, 17833, 17834, 17838, 17840, 17841, 17844, 17846, 17847, 17849, 17850, 17851, 17852, 17853, 17854, 17856, 17858, 17859, 17860, 17862, 17864, 17865, 17866, 17867, 17868, 17869, 17870, 17871, 17872, 17873, 17874, 17875, 17877, 17879, 17880, 17882, 17884, 17885, 17886, 17888, 17889, 17891, 17892, 17894, 17897, 17898, 17900, 17901, 17902, 17903, 17904, 17905, 17906, 17908, 17909, 17910, 17911, 17912, 17914, 17917, 17919, 17920, 17922, 17923, 17924, 17926, 17928, 17929, 17931, 17933, 17934, 17936, 17937, 17938, 17939, 17940, 17941, 17943, 17944, 17945, 17946, 17947, 17948, 17949, 17955, 17956, 17957, 17959, 17960, 17962, 17963, 17964, 17965, 17966, 17969, 17971, 17972, 17973, 17974, 17975, 17976, 17978, 17979, 17981, 17983, 17984, 17985, 17988, 17989, 17990, 17991, 17993, 17994, 17995, 17996, 17997, 17998, 18006, 18009, 18036, 18079, 18121, 18192, 18251, 18255, 18293, 18308, 18316, 18368, 18370, 18377, 18390, 18427, 18433, 18444, 18454, 18496, 18556, 18560, 18563, 18576, 18633, 18634, 18639, 18647, 18656, 18658, 18663, 18672, 18674, 18716, 18752, 18753, 18767, 18884, 18918, 18921, 18944, 18956, 18971, 18974, 18980, 18993, 18996, 18997, 18999, 19001, 19026, 19042, 19058, 19063, 19067, 19074, 19076, 19096, 19121, 19128, 19134, 19137, 19147, 19164, 19168, 19170, 19178, 19180, 19182, 19190, 19198, 19199, 19204, 19206, 19209, 19218, 19221, 19225, 19228, 19233, 19236, 19237, 19248, 19250, 19267, 19276, 19277, 19285, 19291, 19294, 19298, 19306, 19314, 19320, 19322, 19326, 19328, 19331, 19338, 19339, 19340, 19347, 19351, 19353, 19360, 19365, 19367, 19368, 19371, 19374, 19378, 19379, 19382, 19384, 19390, 19392, 19394, 19396, 19398, 19401, 19403, 19407, 19411, 19414, 19415, 19422, 19423, 19429, 19432, 19435, 19438, 19441, 19442, 19444, 19448, 19449, 19450, 19456, 19458, 19462, 19464, 19465, 19469, 19470, 19472, 19474, 19476, 19483, 19484, 19485, 19488, 19489, 19491, 19494, 19501, 19504, 19516, 19519, 19520, 19523, 19524, 19526, 19527, 19528, 19530, 19531, 19537, 19538, 19541, 19551, 19555, 19556, 19558, 19562, 19564, 19566, 19567, 19568, 19571, 19572, 19575, 19577, 19578, 19580, 19582, 19583, 19584, 19591, 19598, 19602, 19604, 19606, 19610, 19613, 19614, 19615, 19618, 19623, 19624, 19625, 19626, 19630, 19631, 19633, 19639, 19640, 19641, 19643, 19644, 19646, 19648, 19652, 19655, 19657, 19658, 19662, 19663, 19665, 19667, 19668, 19677, 19678, 19679, 19682, 19683, 19684, 19685, 19689, 19694, 19695, 19699, 19701, 19703, 19704, 19708, 19710, 19711, 19712, 19714, 19720, 19722, 19724, 19726, 19727, 19728, 19729, 19731, 19732, 19736, 19737, 19738, 19743, 19747, 19748, 19749, 19750, 19751, 19752, 19754, 19757, 19760, 19761, 19762, 19763, 19765, 19766, 19767, 19768, 19771, 19775, 19776, 19777, 19778, 19780, 19781, 19783, 19784, 19786, 19788, 19789, 19792, 19793, 19795, 19798, 19801, 19802, 19804, 19805, 19808, 19812, 19814, 19815, 19817, 19818, 19819, 19822, 19824, 19828, 19829, 19831, 19832, 19833, 19834, 19835, 19837, 19838, 19839, 19841, 19842, 19845, 19852, 19853, 19857, 19858, 19866, 19867, 19868, 19869, 19871, 19872, 19875, 19879, 19882, 19883, 19885, 19886, 19888, 19889, 19890, 19891, 19898, 19901, 19904, 19906, 19907, 19908, 19910, 19911, 19912, 19914, 19916, 19917, 19918, 19919, 19920, 19921, 19923, 19924, 19925, 19927, 19928, 19931, 19933, 19934, 19935, 19937, 19938, 19940, 19941, 19943, 19946, 19947, 19950, 19951, 19952, 19953, 19954, 19958, 19959, 19962, 19964, 19966, 19967, 19968, 19970, 19971, 19972, 19976, 19979, 19980, 19981, 19983, 19984, 19988, 19989, 19990, 19991, 19993, 19995, 19996, 19997, 19999]\n",
+      "Indices of data-points that are subject to a run: [26, 27, 142, 143, 144, 145, 146, 147, 162, 220, 221, 222, 223, 245, 290, 291, 299, 300, 301, 328, 329, 356, 357, 410, 411, 456, 592, 593, 594, 595, 660, 661, 709, 717, 718, 839, 840, 1004, 1005, 1064, 1065, 1066, 1098, 1099, 1147, 1148, 1149, 1150, 1207, 1215, 1216, 1217, 1218, 1319, 1361, 1362, 1461, 1462, 1463, 1583, 1697, 1698, 1699, 1763, 1880, 2246, 2247, 2262, 2476, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2593, 2696, 2697, 2738, 2739, 2740, 2819, 2820, 2821, 2822, 2823, 2855, 2856, 2857, 2858, 2866, 2867, 2908, 2933, 3028, 3029, 3051, 3262, 3263, 3264, 3307, 3308, 3336, 3337, 3421, 3422, 3423, 3505, 3599, 3635, 3817, 4173, 4174, 4328, 4329, 4508, 4547, 4548, 4587, 4659, 4660, 4661, 4662, 4663, 4741, 4799, 4800, 4815, 4816, 4881, 4882, 4883, 4884, 4918, 5058, 5059, 5060, 5061, 5109, 5139, 5183, 5237, 5618, 5676, 5704, 5705, 5828, 6127, 6128, 6129, 6196, 6316, 6368, 6406, 6407, 6670, 6683, 6720, 6855, 6856, 6857, 6858, 6951, 6952, 6953, 6954, 6985, 6986, 6987, 7208, 7209, 7210, 7416, 7417, 7418, 7436, 7474, 7616, 7617, 7738, 7912, 7984, 7985, 7986, 7987, 7988, 7989, 8081, 8326, 8327, 8328, 8329, 8396, 8397, 8433, 8434, 8452, 8453, 8454, 8455, 8456, 8457, 8555, 8556, 8601, 8932, 8933, 8934, 8951, 9001, 9105, 9137, 9138, 9139, 9180, 9521, 9570, 9571, 9590, 9591, 9592, 9593, 9594, 10023, 10024, 10025, 10026, 10202, 10203, 10412, 10497, 10657, 10658, 10659, 10743, 10744, 10830, 10831, 10967, 10968, 10989, 11066, 11067, 11068, 11147, 11148, 11149, 11162, 11263, 11264, 11313, 11314, 11322, 11376, 11377, 11435, 11455, 11456, 11457, 11458, 11459, 11460, 11490, 11491, 11492, 11493, 11494, 11512, 11513, 11528, 11571, 11572, 11624, 11625, 11634, 11699, 11700, 11701, 11702, 11822, 11823, 11824, 11825, 11826, 11834, 11881, 11898, 11899, 11900, 12057, 12058, 12059, 12060, 12061, 12077, 12201, 12326, 12417, 12443, 12444, 12445, 12487, 12488, 12489, 12591, 12592, 12593, 12594, 12602, 12603, 12604, 12656, 12673, 12674, 12754, 12755, 12779, 12780, 12927, 12928, 12936, 13045, 13046, 13047, 13048, 13049, 13050, 13051, 13052, 13177, 13178, 13179, 13219, 13220, 13221, 13222, 13223, 13224, 13275, 13276, 13277, 13278, 13305, 13402, 13437, 13438, 13439, 13653, 13654, 13655, 13656, 13706, 13746, 13747, 13748, 13749, 14029, 14137, 14233, 14234, 14235, 14296, 14385, 14392, 14520, 14521, 14522, 14789, 14790, 14791, 14932, 14933, 15342, 15343, 15344, 15345, 15425, 15426, 15427, 15428, 15527, 15588, 15589, 15590, 15711, 15893, 15943, 15944, 15945, 16267, 16268, 16471, 16472, 16506, 16507, 16508, 16509, 16510, 16523, 16524, 16525, 16563, 16643, 16644, 16690, 16691, 16757, 16758, 16759, 16843, 16902, 16903, 16904, 16935, 16964, 17045, 17244, 17257, 17292, 17410, 17418, 17419, 17500, 17534, 17535, 17585, 17586, 17629, 17687, 17688, 17689, 17690, 17691, 17692, 17899, 17900, 17901, 17956, 17957, 17958, 17959, 18057, 18103, 18104, 18105, 18106, 18107, 18197, 18198, 18284, 18285, 18376, 18392, 18393, 18435, 18646, 18687, 18696, 18697, 18801, 18847, 18944, 18945, 19118, 19205, 19300, 19301, 19487, 19513, 19530, 19531, 19532, 19533, 19549, 19599, 19600, 19601, 19602, 19756, 19774, 19775, 19776, 19821, 19822]\n",
+      "Indices of data-points that are subject to a trend: [4900, 5141, 9473, 19737]\n",
+      "Indices of data-points violating limits: [31, 77, 108, 116, 133, 171, 187, 191, 196, 211, 219, 258, 264, 349, 367, 403, 457, 460, 485, 516, 522, 562, 571, 628, 654, 664, 675, 695, 699, 721, 731, 735, 737, 746, 752, 763, 782, 789, 798, 803, 805, 817, 832, 961, 964, 965, 978, 990, 992, 994, 995, 997, 998, 1019, 1021, 1051, 1059, 1060, 1061, 1066, 1068, 1070, 1084, 1087, 1094, 1097, 1099, 1100, 1107, 1117, 1130, 1140, 1145, 1164, 1174, 1198, 1207, 1209, 1217, 1218, 1224, 1229, 1234, 1245, 1252, 1260, 1261, 1263, 1269, 1272, 1273, 1278, 1283, 1285, 1287, 1291, 1294, 1300, 1302, 1303, 1306, 1307, 1318, 1319, 1323, 1334, 1336, 1337, 1340, 1343, 1344, 1348, 1349, 1352, 1355, 1358, 1365, 1370, 1371, 1372, 1378, 1379, 1381, 1383, 1384, 1387, 1388, 1389, 1390, 1391, 1395, 1396, 1399, 1401, 1402, 1412, 1418, 1421, 1423, 1425, 1428, 1429, 1431, 1441, 1442, 1444, 1445, 1452, 1454, 1459, 1464, 1466, 1470, 1473, 1474, 1476, 1477, 1478, 1480, 1485, 1487, 1492, 1493, 1497, 1498, 1500, 1501, 1504, 1505, 1507, 1508, 1509, 1510, 1511, 1512, 1515, 1516, 1518, 1521, 1522, 1524, 1525, 1526, 1529, 1532, 1535, 1538, 1539, 1541, 1543, 1544, 1545, 1548, 1552, 1553, 1554, 1555, 1557, 1561, 1564, 1565, 1566, 1569, 1570, 1572, 1574, 1575, 1576, 1580, 1581, 1582, 1585, 1586, 1587, 1588, 1589, 1590, 1592, 1593, 1595, 1597, 1598, 1602, 1603, 1604, 1607, 1608, 1614, 1618, 1619, 1622, 1624, 1625, 1631, 1632, 1633, 1635, 1637, 1639, 1641, 1642, 1644, 1649, 1651, 1652, 1653, 1655, 1656, 1657, 1659, 1662, 1664, 1666, 1667, 1668, 1669, 1672, 1674, 1675, 1677, 1678, 1679, 1680, 1682, 1683, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1694, 1695, 1698, 1699, 1700, 1703, 1704, 1705, 1706, 1708, 1709, 1710, 1711, 1712, 1714, 1716, 1717, 1718, 1719, 1720, 1722, 1724, 1726, 1728, 1729, 1730, 1731, 1732, 1734, 1738, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1751, 1753, 1754, 1755, 1756, 1758, 1759, 1761, 1762, 1763, 1764, 1765, 1767, 1769, 1772, 1773, 1774, 1775, 1776, 1777, 1779, 1781, 1782, 1783, 1784, 1785, 1786, 1789, 1790, 1791, 1793, 1794, 1796, 1800, 1801, 1802, 1804, 1805, 1806, 1807, 1808, 1810, 1812, 1813, 1815, 1816, 1817, 1818, 1820, 1821, 1822, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1832, 1833, 1835, 1837, 1838, 1840, 1841, 1842, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1852, 1853, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1868, 1870, 1871, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1903, 1906, 1907, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1918, 1919, 1921, 1922, 1923, 1924, 1925, 1926, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1969, 1971, 1972, 1973, 1974, 1976, 1977, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2001, 2009, 2071, 2117, 2139, 2199, 2215, 2222, 2245, 2250, 2267, 2293, 2317, 2333, 2346, 2356, 2376, 2380, 2392, 2425, 2438, 2445, 2453, 2459, 2465, 2490, 2517, 2522, 2534, 2575, 2601, 2620, 2653, 2669, 2683, 2686, 2688, 2695, 2701, 2729, 2752, 2755, 2761, 2779, 2894, 2914, 2926, 2941, 2956, 2999, 3037, 3055, 3056, 3059, 3065, 3067, 3068, 3087, 3100, 3116, 3124, 3125, 3128, 3137, 3139, 3140, 3146, 3147, 3148, 3150, 3163, 3166, 3172, 3179, 3181, 3189, 3201, 3207, 3213, 3216, 3223, 3224, 3225, 3229, 3235, 3240, 3244, 3249, 3251, 3254, 3259, 3261, 3265, 3267, 3269, 3272, 3274, 3277, 3286, 3288, 3291, 3293, 3294, 3295, 3296, 3300, 3301, 3311, 3313, 3320, 3322, 3325, 3326, 3330, 3331, 3334, 3338, 3339, 3342, 3346, 3347, 3348, 3354, 3358, 3364, 3365, 3366, 3367, 3371, 3372, 3373, 3376, 3380, 3384, 3387, 3388, 3391, 3395, 3397, 3399, 3400, 3407, 3410, 3411, 3412, 3413, 3415, 3418, 3419, 3421, 3426, 3428, 3429, 3434, 3436, 3437, 3438, 3442, 3443, 3447, 3448, 3449, 3451, 3452, 3454, 3455, 3460, 3461, 3466, 3468, 3471, 3472, 3474, 3479, 3480, 3482, 3488, 3489, 3490, 3496, 3497, 3503, 3504, 3505, 3507, 3510, 3512, 3515, 3519, 3521, 3522, 3526, 3533, 3537, 3539, 3540, 3541, 3542, 3544, 3547, 3550, 3552, 3553, 3555, 3561, 3562, 3564, 3566, 3567, 3568, 3569, 3570, 3573, 3578, 3580, 3581, 3582, 3584, 3586, 3587, 3593, 3594, 3596, 3599, 3602, 3606, 3607, 3608, 3609, 3611, 3612, 3615, 3616, 3617, 3619, 3621, 3622, 3625, 3626, 3629, 3632, 3633, 3635, 3637, 3638, 3640, 3643, 3645, 3647, 3649, 3650, 3652, 3653, 3654, 3655, 3656, 3660, 3661, 3663, 3666, 3670, 3672, 3675, 3676, 3677, 3678, 3681, 3682, 3683, 3685, 3688, 3689, 3690, 3691, 3700, 3701, 3702, 3703, 3704, 3706, 3707, 3709, 3711, 3713, 3714, 3717, 3718, 3720, 3721, 3722, 3723, 3724, 3726, 3728, 3730, 3732, 3733, 3735, 3736, 3737, 3738, 3739, 3742, 3744, 3745, 3746, 3748, 3749, 3750, 3751, 3752, 3755, 3758, 3759, 3760, 3761, 3763, 3764, 3766, 3767, 3768, 3770, 3771, 3773, 3775, 3776, 3778, 3779, 3780, 3781, 3783, 3784, 3785, 3789, 3791, 3792, 3793, 3795, 3796, 3797, 3798, 3799, 3800, 3801, 3807, 3808, 3810, 3811, 3812, 3813, 3815, 3816, 3819, 3820, 3821, 3822, 3823, 3824, 3825, 3826, 3828, 3829, 3830, 3832, 3833, 3835, 3836, 3837, 3838, 3840, 3843, 3844, 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3861, 3863, 3865, 3866, 3868, 3869, 3870, 3871, 3874, 3875, 3876, 3878, 3882, 3883, 3884, 3885, 3886, 3887, 3888, 3889, 3890, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3909, 3910, 3911, 3912, 3913, 3914, 3915, 3917, 3918, 3919, 3920, 3921, 3922, 3923, 3924, 3925, 3926, 3927, 3928, 3929, 3930, 3931, 3932, 3933, 3934, 3935, 3936, 3937, 3938, 3940, 3941, 3942, 3943, 3944, 3945, 3946, 3947, 3948, 3949, 3950, 3951, 3953, 3954, 3955, 3956, 3957, 3959, 3960, 3962, 3963, 3964, 3965, 3966, 3967, 3968, 3969, 3970, 3971, 3973, 3974, 3976, 3979, 3980, 3982, 3983, 3984, 3985, 3986, 3987, 3988, 3989, 3990, 3992, 3993, 3994, 3995, 3996, 3997, 3998, 3999, 4007, 4013, 4031, 4036, 4054, 4064, 4074, 4123, 4124, 4125, 4127, 4170, 4186, 4187, 4219, 4247, 4269, 4279, 4337, 4342, 4357, 4360, 4374, 4403, 4410, 4480, 4488, 4504, 4509, 4527, 4540, 4541, 4549, 4560, 4626, 4695, 4707, 4736, 4745, 4772, 4802, 4808, 4819, 4854, 4910, 4931, 4937, 4979, 4994, 4995, 4996, 4997, 4998, 4999, 5025, 5028, 5029, 5079, 5083, 5092, 5095, 5102, 5103, 5104, 5108, 5113, 5128, 5140, 5141, 5144, 5145, 5148, 5156, 5157, 5161, 5163, 5165, 5166, 5167, 5175, 5186, 5201, 5205, 5209, 5210, 5220, 5223, 5231, 5235, 5241, 5242, 5245, 5255, 5256, 5257, 5266, 5271, 5274, 5293, 5301, 5304, 5306, 5308, 5309, 5311, 5313, 5315, 5316, 5324, 5325, 5330, 5335, 5339, 5342, 5345, 5347, 5363, 5365, 5381, 5383, 5384, 5386, 5391, 5392, 5396, 5400, 5401, 5404, 5409, 5411, 5413, 5414, 5418, 5419, 5420, 5423, 5427, 5433, 5434, 5435, 5436, 5437, 5438, 5440, 5441, 5444, 5447, 5450, 5453, 5456, 5461, 5462, 5465, 5468, 5474, 5475, 5480, 5482, 5484, 5485, 5488, 5490, 5495, 5496, 5501, 5502, 5504, 5507, 5508, 5511, 5514, 5518, 5522, 5524, 5528, 5529, 5531, 5533, 5535, 5539, 5543, 5547, 5548, 5550, 5551, 5554, 5555, 5556, 5559, 5564, 5566, 5567, 5569, 5572, 5575, 5577, 5579, 5581, 5582, 5584, 5587, 5588, 5589, 5590, 5591, 5593, 5594, 5595, 5597, 5598, 5599, 5602, 5607, 5608, 5609, 5610, 5612, 5615, 5619, 5620, 5624, 5626, 5632, 5633, 5635, 5637, 5639, 5643, 5644, 5645, 5647, 5648, 5649, 5650, 5651, 5653, 5655, 5656, 5660, 5662, 5663, 5664, 5665, 5667, 5669, 5670, 5671, 5674, 5675, 5677, 5678, 5680, 5685, 5686, 5687, 5691, 5692, 5693, 5695, 5696, 5697, 5699, 5700, 5703, 5704, 5706, 5707, 5709, 5710, 5712, 5713, 5714, 5715, 5717, 5718, 5720, 5722, 5724, 5726, 5728, 5733, 5735, 5736, 5737, 5741, 5742, 5743, 5744, 5745, 5747, 5748, 5749, 5751, 5753, 5757, 5758, 5760, 5763, 5765, 5768, 5770, 5771, 5772, 5773, 5774, 5775, 5776, 5777, 5779, 5780, 5783, 5785, 5786, 5787, 5788, 5789, 5790, 5791, 5792, 5793, 5795, 5796, 5798, 5799, 5800, 5801, 5802, 5803, 5804, 5807, 5809, 5813, 5814, 5816, 5817, 5818, 5821, 5822, 5823, 5825, 5827, 5830, 5831, 5832, 5833, 5834, 5836, 5838, 5839, 5840, 5841, 5843, 5844, 5845, 5846, 5847, 5848, 5852, 5855, 5857, 5858, 5859, 5861, 5862, 5863, 5864, 5865, 5866, 5868, 5870, 5871, 5874, 5875, 5877, 5878, 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, 5887, 5889, 5890, 5891, 5893, 5894, 5895, 5896, 5897, 5898, 5899, 5900, 5902, 5904, 5906, 5908, 5909, 5910, 5912, 5913, 5914, 5915, 5917, 5918, 5919, 5920, 5921, 5924, 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, 5933, 5935, 5936, 5937, 5938, 5939, 5941, 5943, 5946, 5948, 5949, 5951, 5953, 5954, 5955, 5956, 5957, 5958, 5959, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, 5970, 5971, 5972, 5974, 5975, 5976, 5979, 5980, 5982, 5983, 5984, 5985, 5987, 5988, 5990, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999, 6023, 6048, 6099, 6118, 6130, 6163, 6165, 6175, 6197, 6199, 6225, 6228, 6255, 6262, 6278, 6315, 6334, 6342, 6415, 6461, 6483, 6490, 6498, 6530, 6532, 6535, 6538, 6557, 6579, 6601, 6615, 6635, 6651, 6757, 6766, 6775, 6789, 6793, 6807, 6826, 6847, 6850, 6867, 6911, 6963, 6969, 6980, 6985, 6987, 6988, 6992, 6993, 6995, 6996, 6999, 7007, 7014, 7030, 7033, 7057, 7065, 7068, 7092, 7098, 7100, 7117, 7120, 7143, 7145, 7146, 7169, 7179, 7180, 7185, 7192, 7194, 7202, 7205, 7210, 7213, 7216, 7219, 7224, 7227, 7238, 7240, 7243, 7247, 7248, 7250, 7251, 7252, 7254, 7256, 7265, 7267, 7277, 7280, 7286, 7290, 7298, 7300, 7305, 7306, 7314, 7316, 7323, 7324, 7327, 7329, 7333, 7334, 7337, 7340, 7341, 7343, 7350, 7357, 7358, 7362, 7363, 7374, 7375, 7376, 7380, 7384, 7388, 7392, 7393, 7395, 7398, 7404, 7406, 7410, 7413, 7415, 7416, 7417, 7418, 7424, 7426, 7427, 7429, 7432, 7434, 7435, 7436, 7439, 7441, 7444, 7445, 7446, 7447, 7450, 7459, 7460, 7461, 7466, 7467, 7468, 7471, 7476, 7477, 7480, 7482, 7485, 7486, 7487, 7489, 7497, 7501, 7502, 7505, 7506, 7508, 7509, 7511, 7512, 7516, 7519, 7522, 7523, 7525, 7527, 7532, 7534, 7536, 7541, 7542, 7544, 7550, 7551, 7552, 7556, 7557, 7561, 7563, 7565, 7567, 7568, 7573, 7576, 7578, 7581, 7584, 7585, 7587, 7589, 7590, 7593, 7594, 7595, 7596, 7597, 7598, 7599, 7601, 7602, 7603, 7605, 7608, 7611, 7612, 7613, 7619, 7621, 7623, 7628, 7629, 7630, 7631, 7632, 7634, 7639, 7640, 7641, 7642, 7643, 7645, 7646, 7648, 7649, 7650, 7651, 7653, 7654, 7656, 7657, 7660, 7665, 7674, 7675, 7676, 7677, 7679, 7683, 7686, 7689, 7691, 7693, 7694, 7696, 7698, 7701, 7702, 7703, 7704, 7705, 7706, 7708, 7709, 7710, 7712, 7714, 7717, 7720, 7722, 7723, 7724, 7725, 7726, 7727, 7728, 7730, 7731, 7733, 7736, 7738, 7739, 7740, 7744, 7747, 7749, 7751, 7752, 7753, 7755, 7756, 7758, 7760, 7763, 7764, 7765, 7766, 7768, 7769, 7770, 7772, 7777, 7778, 7779, 7780, 7781, 7783, 7784, 7785, 7786, 7787, 7789, 7792, 7794, 7795, 7796, 7798, 7800, 7801, 7802, 7803, 7806, 7807, 7808, 7809, 7810, 7811, 7812, 7813, 7815, 7816, 7817, 7818, 7820, 7821, 7822, 7823, 7824, 7825, 7826, 7829, 7831, 7832, 7833, 7834, 7835, 7836, 7837, 7838, 7839, 7840, 7841, 7842, 7843, 7844, 7845, 7846, 7847, 7849, 7850, 7851, 7852, 7853, 7860, 7861, 7863, 7866, 7867, 7868, 7869, 7870, 7872, 7873, 7874, 7875, 7877, 7880, 7882, 7883, 7884, 7885, 7886, 7888, 7889, 7891, 7892, 7893, 7894, 7895, 7896, 7897, 7900, 7901, 7902, 7903, 7904, 7905, 7906, 7907, 7908, 7909, 7912, 7913, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7925, 7927, 7928, 7929, 7930, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7943, 7944, 7945, 7946, 7947, 7948, 7951, 7952, 7953, 7955, 7956, 7957, 7958, 7959, 7960, 7961, 7962, 7964, 7966, 7967, 7970, 7971, 7972, 7974, 7975, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7986, 7987, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8015, 8030, 8036, 8038, 8105, 8119, 8124, 8149, 8206, 8266, 8277, 8278, 8304, 8330, 8333, 8338, 8376, 8426, 8430, 8455, 8462, 8475, 8508, 8521, 8533, 8556, 8566, 8587, 8594, 8598, 8605, 8616, 8630, 8665, 8667, 8682, 8688, 8702, 8727, 8738, 8743, 8754, 8764, 8766, 8801, 8808, 8823, 8855, 8878, 8885, 8912, 8920, 8923, 8954, 8985, 8987, 8988, 8994, 8995, 8996, 8997, 8998, 8999, 9017, 9037, 9043, 9056, 9060, 9066, 9083, 9092, 9118, 9124, 9130, 9146, 9148, 9153, 9158, 9164, 9166, 9173, 9176, 9181, 9184, 9185, 9189, 9191, 9192, 9202, 9213, 9217, 9219, 9220, 9222, 9224, 9227, 9229, 9234, 9269, 9278, 9283, 9284, 9299, 9304, 9305, 9306, 9307, 9311, 9314, 9315, 9319, 9323, 9326, 9332, 9341, 9342, 9348, 9349, 9354, 9357, 9359, 9360, 9365, 9366, 9373, 9375, 9376, 9386, 9388, 9391, 9396, 9398, 9402, 9403, 9404, 9406, 9409, 9410, 9411, 9415, 9419, 9420, 9421, 9425, 9433, 9439, 9440, 9441, 9461, 9465, 9467, 9472, 9473, 9474, 9481, 9488, 9495, 9496, 9500, 9502, 9505, 9522, 9524, 9525, 9526, 9528, 9529, 9531, 9535, 9537, 9540, 9542, 9543, 9544, 9547, 9548, 9549, 9551, 9552, 9553, 9556, 9558, 9559, 9562, 9571, 9573, 9577, 9578, 9580, 9586, 9587, 9590, 9592, 9594, 9598, 9599, 9605, 9609, 9613, 9617, 9620, 9624, 9627, 9630, 9632, 9637, 9640, 9643, 9645, 9651, 9656, 9658, 9659, 9660, 9662, 9663, 9667, 9668, 9671, 9675, 9676, 9677, 9684, 9685, 9686, 9687, 9689, 9690, 9691, 9693, 9696, 9697, 9698, 9701, 9702, 9703, 9713, 9714, 9722, 9726, 9731, 9735, 9739, 9742, 9744, 9745, 9747, 9753, 9754, 9757, 9761, 9763, 9764, 9767, 9768, 9771, 9772, 9773, 9777, 9779, 9780, 9785, 9786, 9791, 9792, 9794, 9795, 9798, 9803, 9805, 9806, 9810, 9812, 9813, 9816, 9817, 9819, 9821, 9822, 9825, 9828, 9830, 9831, 9832, 9835, 9841, 9845, 9846, 9847, 9848, 9849, 9850, 9851, 9852, 9853, 9854, 9856, 9857, 9859, 9860, 9862, 9868, 9869, 9870, 9873, 9874, 9876, 9877, 9879, 9880, 9881, 9882, 9883, 9889, 9891, 9894, 9897, 9898, 9899, 9900, 9901, 9902, 9903, 9905, 9906, 9907, 9911, 9913, 9914, 9917, 9922, 9923, 9927, 9929, 9930, 9931, 9933, 9935, 9937, 9939, 9940, 9943, 9944, 9948, 9949, 9950, 9951, 9952, 9956, 9958, 9959, 9963, 9966, 9969, 9973, 9975, 9976, 9978, 9979, 9980, 9985, 9987, 9989, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9998, 9999, 10001, 10008, 10031, 10069, 10087, 10125, 10153, 10202, 10206, 10211, 10237, 10295, 10297, 10317, 10325, 10329, 10344, 10348, 10350, 10358, 10359, 10365, 10405, 10413, 10422, 10434, 10462, 10472, 10508, 10555, 10572, 10580, 10589, 10603, 10605, 10614, 10625, 10627, 10637, 10647, 10672, 10691, 10706, 10722, 10737, 10757, 10760, 10769, 10786, 10809, 10819, 10840, 10852, 10879, 10896, 10907, 10919, 10947, 10954, 10969, 10981, 10983, 10984, 10986, 10987, 10988, 10990, 10992, 10998, 10999, 11004, 11035, 11058, 11071, 11093, 11100, 11103, 11122, 11125, 11126, 11134, 11136, 11155, 11175, 11176, 11183, 11196, 11206, 11218, 11219, 11230, 11233, 11238, 11247, 11249, 11260, 11261, 11262, 11265, 11280, 11286, 11292, 11293, 11294, 11302, 11305, 11316, 11322, 11323, 11325, 11327, 11333, 11334, 11335, 11337, 11341, 11345, 11346, 11348, 11355, 11361, 11369, 11370, 11371, 11379, 11389, 11390, 11395, 11396, 11397, 11399, 11400, 11402, 11403, 11405, 11406, 11407, 11409, 11410, 11413, 11415, 11416, 11421, 11422, 11423, 11426, 11427, 11428, 11432, 11443, 11446, 11448, 11450, 11451, 11458, 11463, 11467, 11469, 11470, 11472, 11475, 11478, 11479, 11484, 11485, 11487, 11498, 11500, 11502, 11503, 11506, 11507, 11510, 11511, 11513, 11518, 11520, 11524, 11526, 11527, 11529, 11537, 11540, 11543, 11547, 11548, 11552, 11555, 11556, 11560, 11562, 11563, 11566, 11568, 11570, 11572, 11574, 11578, 11579, 11580, 11584, 11585, 11590, 11595, 11596, 11597, 11605, 11609, 11611, 11613, 11614, 11615, 11620, 11621, 11622, 11628, 11629, 11630, 11633, 11634, 11635, 11637, 11642, 11643, 11646, 11649, 11650, 11651, 11653, 11654, 11659, 11660, 11661, 11662, 11666, 11667, 11674, 11676, 11678, 11679, 11681, 11683, 11684, 11685, 11686, 11689, 11690, 11692, 11694, 11695, 11697, 11700, 11701, 11702, 11703, 11705, 11706, 11707, 11709, 11712, 11715, 11716, 11718, 11721, 11723, 11724, 11726, 11727, 11729, 11732, 11733, 11735, 11737, 11738, 11739, 11740, 11743, 11746, 11749, 11751, 11752, 11754, 11755, 11756, 11757, 11758, 11759, 11760, 11761, 11762, 11763, 11764, 11765, 11767, 11768, 11769, 11770, 11771, 11772, 11775, 11776, 11779, 11780, 11782, 11783, 11784, 11785, 11788, 11789, 11791, 11792, 11793, 11794, 11799, 11801, 11802, 11803, 11807, 11808, 11811, 11813, 11814, 11816, 11817, 11820, 11821, 11822, 11823, 11824, 11825, 11826, 11830, 11831, 11832, 11834, 11835, 11836, 11837, 11838, 11839, 11841, 11842, 11844, 11845, 11848, 11849, 11851, 11852, 11853, 11854, 11855, 11859, 11860, 11861, 11862, 11863, 11865, 11866, 11867, 11871, 11872, 11873, 11874, 11875, 11877, 11878, 11880, 11882, 11883, 11888, 11889, 11890, 11891, 11892, 11894, 11895, 11896, 11897, 11898, 11900, 11901, 11902, 11903, 11904, 11905, 11906, 11907, 11908, 11910, 11912, 11913, 11915, 11916, 11917, 11919, 11920, 11922, 11925, 11926, 11930, 11931, 11932, 11933, 11935, 11936, 11937, 11940, 11942, 11943, 11944, 11946, 11947, 11948, 11949, 11950, 11952, 11955, 11956, 11957, 11959, 11960, 11961, 11962, 11963, 11964, 11967, 11969, 11970, 11971, 11972, 11976, 11977, 11978, 11980, 11982, 11983, 11984, 11985, 11986, 11987, 11988, 11989, 11991, 11992, 11993, 11994, 11995, 11998, 12016, 12027, 12030, 12036, 12090, 12115, 12129, 12186, 12188, 12192, 12348, 12359, 12367, 12381, 12388, 12393, 12431, 12445, 12447, 12454, 12460, 12461, 12481, 12483, 12485, 12487, 12494, 12502, 12504, 12520, 12533, 12534, 12547, 12639, 12665, 12666, 12680, 12695, 12704, 12751, 12755, 12813, 12818, 12825, 12835, 12837, 12853, 12908, 12963, 12968, 12987, 12990, 12991, 12992, 12993, 12996, 12997, 12998, 12999, 13024, 13046, 13056, 13075, 13079, 13084, 13102, 13103, 13107, 13116, 13119, 13120, 13123, 13124, 13126, 13128, 13129, 13131, 13142, 13149, 13151, 13152, 13154, 13156, 13158, 13162, 13167, 13184, 13194, 13195, 13196, 13199, 13202, 13204, 13205, 13211, 13215, 13217, 13220, 13227, 13237, 13242, 13244, 13248, 13252, 13255, 13257, 13260, 13261, 13265, 13266, 13268, 13269, 13280, 13283, 13285, 13300, 13304, 13308, 13309, 13317, 13319, 13320, 13321, 13322, 13324, 13325, 13328, 13329, 13336, 13338, 13340, 13341, 13342, 13344, 13345, 13350, 13352, 13355, 13357, 13359, 13360, 13361, 13366, 13370, 13372, 13375, 13376, 13378, 13381, 13385, 13387, 13391, 13393, 13397, 13403, 13405, 13406, 13407, 13408, 13412, 13414, 13415, 13417, 13418, 13423, 13424, 13425, 13426, 13427, 13429, 13434, 13436, 13438, 13440, 13442, 13447, 13450, 13451, 13456, 13457, 13463, 13468, 13473, 13474, 13477, 13478, 13480, 13482, 13483, 13484, 13485, 13492, 13493, 13495, 13496, 13499, 13500, 13503, 13504, 13505, 13506, 13507, 13509, 13511, 13513, 13514, 13515, 13516, 13517, 13518, 13519, 13521, 13524, 13527, 13530, 13533, 13535, 13537, 13539, 13540, 13542, 13544, 13545, 13546, 13548, 13551, 13552, 13554, 13555, 13557, 13559, 13561, 13564, 13567, 13570, 13572, 13573, 13576, 13578, 13579, 13580, 13581, 13582, 13583, 13584, 13585, 13587, 13588, 13589, 13592, 13594, 13595, 13596, 13597, 13599, 13601, 13603, 13605, 13606, 13611, 13613, 13614, 13617, 13622, 13623, 13626, 13627, 13629, 13630, 13632, 13634, 13636, 13637, 13641, 13643, 13645, 13646, 13647, 13648, 13649, 13650, 13651, 13652, 13653, 13655, 13656, 13657, 13659, 13660, 13661, 13663, 13664, 13668, 13670, 13671, 13672, 13673, 13678, 13679, 13680, 13683, 13685, 13687, 13689, 13690, 13691, 13692, 13693, 13694, 13695, 13696, 13698, 13699, 13701, 13702, 13703, 13705, 13706, 13707, 13708, 13710, 13711, 13712, 13713, 13715, 13720, 13721, 13723, 13725, 13728, 13729, 13730, 13731, 13733, 13734, 13736, 13737, 13738, 13740, 13741, 13743, 13744, 13745, 13746, 13747, 13748, 13750, 13751, 13754, 13755, 13756, 13757, 13758, 13759, 13760, 13761, 13762, 13763, 13765, 13766, 13768, 13769, 13771, 13772, 13773, 13775, 13777, 13778, 13779, 13781, 13783, 13784, 13785, 13788, 13789, 13790, 13792, 13793, 13794, 13795, 13796, 13797, 13798, 13800, 13801, 13803, 13804, 13805, 13807, 13809, 13810, 13812, 13813, 13814, 13815, 13816, 13817, 13820, 13821, 13822, 13823, 13824, 13829, 13830, 13831, 13832, 13833, 13834, 13835, 13838, 13839, 13840, 13841, 13842, 13844, 13845, 13846, 13847, 13848, 13849, 13850, 13851, 13852, 13855, 13856, 13857, 13859, 13860, 13862, 13863, 13865, 13866, 13867, 13868, 13869, 13870, 13872, 13874, 13875, 13876, 13877, 13878, 13879, 13880, 13881, 13882, 13883, 13884, 13885, 13886, 13887, 13888, 13889, 13890, 13893, 13894, 13895, 13896, 13897, 13898, 13899, 13901, 13903, 13904, 13905, 13906, 13907, 13908, 13909, 13910, 13912, 13914, 13915, 13916, 13917, 13918, 13919, 13920, 13921, 13923, 13924, 13925, 13926, 13927, 13929, 13931, 13932, 13933, 13934, 13935, 13936, 13937, 13938, 13940, 13941, 13942, 13943, 13944, 13945, 13947, 13948, 13949, 13950, 13952, 13954, 13955, 13956, 13957, 13958, 13959, 13960, 13961, 13962, 13963, 13964, 13965, 13966, 13967, 13968, 13969, 13971, 13973, 13974, 13975, 13976, 13979, 13980, 13981, 13982, 13983, 13984, 13985, 13987, 13988, 13989, 13990, 13991, 13992, 13993, 13994, 13997, 13998, 13999, 14001, 14017, 14030, 14032, 14039, 14057, 14059, 14102, 14119, 14128, 14164, 14166, 14174, 14185, 14210, 14220, 14221, 14237, 14288, 14314, 14327, 14330, 14343, 14346, 14381, 14455, 14467, 14469, 14471, 14539, 14550, 14584, 14611, 14616, 14624, 14637, 14662, 14666, 14702, 14707, 14737, 14795, 14800, 14803, 14806, 14820, 14882, 14905, 14931, 14940, 14958, 14990, 14992, 14993, 14998, 14999, 15042, 15057, 15066, 15072, 15077, 15099, 15103, 15105, 15106, 15111, 15116, 15117, 15119, 15134, 15135, 15143, 15163, 15167, 15170, 15172, 15175, 15187, 15212, 15213, 15217, 15219, 15226, 15228, 15229, 15232, 15235, 15238, 15244, 15249, 15251, 15262, 15264, 15276, 15281, 15286, 15291, 15293, 15305, 15306, 15307, 15319, 15322, 15323, 15325, 15331, 15337, 15343, 15344, 15347, 15348, 15350, 15352, 15353, 15354, 15355, 15356, 15361, 15365, 15367, 15368, 15370, 15371, 15375, 15376, 15377, 15380, 15384, 15386, 15387, 15389, 15396, 15397, 15400, 15401, 15405, 15407, 15410, 15412, 15418, 15419, 15428, 15429, 15436, 15438, 15442, 15451, 15453, 15455, 15459, 15460, 15461, 15462, 15463, 15473, 15478, 15481, 15483, 15484, 15489, 15495, 15496, 15500, 15502, 15503, 15505, 15507, 15510, 15514, 15515, 15518, 15520, 15521, 15523, 15524, 15531, 15532, 15534, 15535, 15538, 15539, 15540, 15541, 15544, 15545, 15546, 15547, 15550, 15551, 15552, 15554, 15555, 15560, 15561, 15562, 15564, 15566, 15568, 15571, 15573, 15574, 15579, 15580, 15581, 15585, 15586, 15588, 15591, 15592, 15593, 15595, 15596, 15599, 15602, 15606, 15608, 15609, 15613, 15614, 15616, 15617, 15619, 15620, 15622, 15623, 15625, 15628, 15632, 15633, 15634, 15635, 15642, 15643, 15644, 15647, 15648, 15650, 15653, 15654, 15656, 15657, 15658, 15659, 15660, 15661, 15662, 15664, 15667, 15668, 15670, 15671, 15672, 15674, 15676, 15678, 15680, 15681, 15682, 15684, 15685, 15686, 15688, 15690, 15692, 15693, 15695, 15698, 15702, 15703, 15704, 15705, 15706, 15707, 15712, 15713, 15715, 15716, 15719, 15720, 15721, 15722, 15723, 15725, 15726, 15728, 15730, 15733, 15734, 15735, 15736, 15738, 15740, 15741, 15743, 15744, 15745, 15746, 15747, 15748, 15749, 15750, 15751, 15752, 15753, 15754, 15756, 15757, 15759, 15763, 15764, 15767, 15769, 15770, 15772, 15773, 15774, 15776, 15777, 15778, 15779, 15780, 15781, 15782, 15783, 15784, 15785, 15786, 15788, 15789, 15790, 15792, 15793, 15794, 15796, 15797, 15798, 15799, 15800, 15803, 15805, 15806, 15807, 15808, 15809, 15811, 15812, 15814, 15815, 15816, 15817, 15818, 15820, 15821, 15823, 15827, 15828, 15829, 15831, 15834, 15836, 15837, 15838, 15839, 15840, 15842, 15844, 15847, 15848, 15849, 15850, 15851, 15852, 15853, 15854, 15855, 15857, 15858, 15859, 15861, 15862, 15864, 15865, 15866, 15867, 15868, 15869, 15872, 15874, 15875, 15876, 15877, 15878, 15879, 15880, 15881, 15883, 15885, 15886, 15887, 15888, 15889, 15890, 15892, 15893, 15895, 15897, 15898, 15899, 15900, 15901, 15903, 15907, 15910, 15911, 15913, 15914, 15915, 15916, 15917, 15918, 15919, 15920, 15921, 15923, 15924, 15925, 15926, 15928, 15929, 15931, 15932, 15933, 15934, 15937, 15939, 15940, 15941, 15942, 15944, 15945, 15947, 15950, 15952, 15953, 15954, 15955, 15956, 15958, 15959, 15961, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15974, 15975, 15976, 15978, 15980, 15982, 15984, 15985, 15986, 15987, 15988, 15989, 15990, 15991, 15992, 15993, 15994, 15995, 15996, 15997, 15998, 16010, 16019, 16022, 16046, 16076, 16106, 16111, 16119, 16120, 16134, 16155, 16157, 16201, 16205, 16220, 16222, 16258, 16260, 16275, 16284, 16287, 16305, 16307, 16313, 16342, 16368, 16371, 16447, 16454, 16493, 16513, 16524, 16575, 16583, 16585, 16597, 16622, 16646, 16675, 16682, 16748, 16767, 16831, 16832, 16838, 16841, 16880, 16885, 16891, 16895, 16906, 16945, 16952, 16965, 16970, 16975, 16978, 16995, 16996, 16997, 17005, 17025, 17026, 17030, 17040, 17050, 17077, 17081, 17086, 17110, 17122, 17127, 17137, 17157, 17159, 17160, 17169, 17174, 17188, 17195, 17196, 17197, 17202, 17213, 17214, 17217, 17218, 17220, 17231, 17232, 17245, 17246, 17249, 17257, 17261, 17262, 17271, 17272, 17273, 17274, 17277, 17281, 17284, 17286, 17288, 17292, 17297, 17310, 17312, 17315, 17321, 17322, 17325, 17327, 17330, 17344, 17345, 17347, 17354, 17355, 17361, 17369, 17373, 17375, 17376, 17378, 17389, 17390, 17393, 17395, 17397, 17401, 17402, 17403, 17411, 17414, 17420, 17422, 17423, 17429, 17440, 17443, 17445, 17448, 17449, 17451, 17452, 17456, 17457, 17459, 17465, 17467, 17469, 17470, 17473, 17475, 17476, 17478, 17480, 17482, 17484, 17488, 17490, 17492, 17493, 17494, 17495, 17496, 17498, 17502, 17504, 17505, 17511, 17515, 17517, 17521, 17526, 17529, 17530, 17532, 17536, 17537, 17539, 17543, 17544, 17548, 17554, 17555, 17556, 17558, 17559, 17561, 17562, 17564, 17567, 17568, 17569, 17572, 17574, 17575, 17579, 17580, 17582, 17584, 17585, 17586, 17587, 17588, 17589, 17599, 17602, 17604, 17605, 17606, 17607, 17609, 17611, 17612, 17613, 17614, 17620, 17622, 17624, 17625, 17626, 17628, 17629, 17630, 17631, 17633, 17634, 17636, 17637, 17638, 17643, 17644, 17646, 17647, 17649, 17650, 17651, 17652, 17653, 17655, 17656, 17657, 17658, 17659, 17662, 17668, 17669, 17670, 17672, 17676, 17678, 17679, 17680, 17681, 17684, 17685, 17686, 17687, 17688, 17689, 17691, 17692, 17695, 17697, 17698, 17700, 17701, 17702, 17703, 17704, 17705, 17709, 17712, 17713, 17716, 17719, 17720, 17722, 17723, 17726, 17727, 17729, 17731, 17732, 17733, 17734, 17737, 17738, 17740, 17741, 17742, 17743, 17744, 17746, 17751, 17753, 17754, 17755, 17756, 17757, 17758, 17759, 17760, 17761, 17762, 17763, 17769, 17772, 17773, 17774, 17775, 17777, 17779, 17780, 17781, 17782, 17783, 17784, 17786, 17787, 17788, 17790, 17791, 17792, 17794, 17797, 17798, 17801, 17802, 17804, 17808, 17810, 17812, 17815, 17816, 17817, 17819, 17821, 17822, 17823, 17824, 17826, 17827, 17830, 17832, 17833, 17834, 17838, 17840, 17841, 17844, 17846, 17847, 17849, 17850, 17851, 17852, 17853, 17854, 17856, 17858, 17859, 17860, 17862, 17864, 17865, 17866, 17867, 17868, 17869, 17870, 17871, 17872, 17873, 17874, 17875, 17877, 17879, 17880, 17882, 17884, 17885, 17886, 17888, 17889, 17891, 17892, 17894, 17897, 17898, 17900, 17901, 17902, 17903, 17904, 17905, 17906, 17908, 17909, 17910, 17911, 17912, 17914, 17917, 17919, 17920, 17922, 17923, 17924, 17926, 17928, 17929, 17931, 17933, 17934, 17936, 17937, 17938, 17939, 17940, 17941, 17943, 17944, 17945, 17946, 17947, 17948, 17949, 17955, 17956, 17957, 17959, 17960, 17962, 17963, 17964, 17965, 17966, 17969, 17971, 17972, 17973, 17974, 17975, 17976, 17978, 17979, 17981, 17983, 17984, 17985, 17988, 17989, 17990, 17991, 17993, 17994, 17995, 17996, 17997, 17998, 18006, 18009, 18036, 18079, 18121, 18192, 18251, 18255, 18293, 18308, 18316, 18368, 18370, 18377, 18390, 18427, 18433, 18444, 18454, 18496, 18556, 18560, 18563, 18576, 18633, 18634, 18639, 18647, 18656, 18658, 18663, 18672, 18674, 18716, 18752, 18753, 18767, 18884, 18918, 18921, 18944, 18956, 18971, 18974, 18980, 18993, 18996, 18997, 18999, 19001, 19026, 19042, 19058, 19063, 19067, 19074, 19076, 19096, 19121, 19128, 19134, 19137, 19147, 19164, 19168, 19170, 19178, 19180, 19182, 19190, 19198, 19199, 19204, 19206, 19209, 19218, 19221, 19225, 19228, 19233, 19236, 19237, 19248, 19250, 19267, 19276, 19277, 19285, 19291, 19294, 19298, 19306, 19314, 19320, 19322, 19326, 19328, 19331, 19338, 19339, 19340, 19347, 19351, 19353, 19360, 19365, 19367, 19368, 19371, 19374, 19378, 19379, 19382, 19384, 19390, 19392, 19394, 19396, 19398, 19401, 19403, 19407, 19411, 19414, 19415, 19422, 19423, 19429, 19432, 19435, 19438, 19441, 19442, 19444, 19448, 19449, 19450, 19456, 19458, 19462, 19464, 19465, 19469, 19470, 19472, 19474, 19476, 19483, 19484, 19485, 19488, 19489, 19491, 19494, 19501, 19504, 19516, 19519, 19520, 19523, 19524, 19526, 19527, 19528, 19530, 19531, 19537, 19538, 19541, 19551, 19555, 19556, 19558, 19562, 19564, 19566, 19567, 19568, 19571, 19572, 19575, 19577, 19578, 19580, 19582, 19583, 19584, 19591, 19598, 19602, 19604, 19606, 19610, 19613, 19614, 19615, 19618, 19623, 19624, 19625, 19626, 19630, 19631, 19633, 19639, 19640, 19641, 19643, 19644, 19646, 19648, 19652, 19655, 19657, 19658, 19662, 19663, 19665, 19667, 19668, 19677, 19678, 19679, 19682, 19683, 19684, 19685, 19689, 19694, 19695, 19699, 19701, 19703, 19704, 19708, 19710, 19711, 19712, 19714, 19720, 19722, 19724, 19726, 19727, 19728, 19729, 19731, 19732, 19736, 19737, 19738, 19743, 19747, 19748, 19749, 19750, 19751, 19752, 19754, 19757, 19760, 19761, 19762, 19763, 19765, 19766, 19767, 19768, 19771, 19775, 19776, 19777, 19778, 19780, 19781, 19783, 19784, 19786, 19788, 19789, 19792, 19793, 19795, 19798, 19801, 19802, 19804, 19805, 19808, 19812, 19814, 19815, 19817, 19818, 19819, 19822, 19824, 19828, 19829, 19831, 19832, 19833, 19834, 19835, 19837, 19838, 19839, 19841, 19842, 19845, 19852, 19853, 19857, 19858, 19866, 19867, 19868, 19869, 19871, 19872, 19875, 19879, 19882, 19883, 19885, 19886, 19888, 19889, 19890, 19891, 19898, 19901, 19904, 19906, 19907, 19908, 19910, 19911, 19912, 19914, 19916, 19917, 19918, 19919, 19920, 19921, 19923, 19924, 19925, 19927, 19928, 19931, 19933, 19934, 19935, 19937, 19938, 19940, 19941, 19943, 19946, 19947, 19950, 19951, 19952, 19953, 19954, 19958, 19959, 19962, 19964, 19966, 19967, 19968, 19970, 19971, 19972, 19976, 19979, 19980, 19981, 19983, 19984, 19988, 19989, 19990, 19991, 19993, 19995, 19996, 19997, 19999]\n",
+      "Indices of data-points that are subject to a run: [26, 27, 142, 143, 144, 145, 146, 147, 162, 220, 221, 222, 223, 245, 290, 291, 299, 300, 301, 328, 329, 356, 357, 410, 411, 456, 592, 593, 594, 595, 660, 661, 709, 717, 718, 839, 840, 1004, 1005, 1064, 1065, 1066, 1098, 1099, 1147, 1148, 1149, 1150, 1207, 1215, 1216, 1217, 1218, 1319, 1361, 1362, 1461, 1462, 1463, 1583, 1697, 1698, 1699, 1763, 1880, 2246, 2247, 2262, 2476, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2593, 2696, 2697, 2738, 2739, 2740, 2819, 2820, 2821, 2822, 2823, 2855, 2856, 2857, 2858, 2866, 2867, 2908, 2933, 3028, 3029, 3051, 3262, 3263, 3264, 3307, 3308, 3336, 3337, 3421, 3422, 3423, 3505, 3599, 3635, 3817, 4173, 4174, 4328, 4329, 4508, 4547, 4548, 4587, 4659, 4660, 4661, 4662, 4663, 4741, 4799, 4800, 4815, 4816, 4881, 4882, 4883, 4884, 4918, 5058, 5059, 5060, 5061, 5109, 5139, 5183, 5237, 5618, 5676, 5704, 5705, 5828, 6127, 6128, 6129, 6196, 6316, 6368, 6406, 6407, 6670, 6683, 6720, 6855, 6856, 6857, 6858, 6951, 6952, 6953, 6954, 6985, 6986, 6987, 7208, 7209, 7210, 7416, 7417, 7418, 7436, 7474, 7616, 7617, 7738, 7912, 7984, 7985, 7986, 7987, 7988, 7989, 8081, 8326, 8327, 8328, 8329, 8396, 8397, 8433, 8434, 8452, 8453, 8454, 8455, 8456, 8457, 8555, 8556, 8601, 8932, 8933, 8934, 8951, 9001, 9105, 9137, 9138, 9139, 9180, 9521, 9570, 9571, 9590, 9591, 9592, 9593, 9594, 10023, 10024, 10025, 10026, 10202, 10203, 10412, 10497, 10657, 10658, 10659, 10743, 10744, 10830, 10831, 10967, 10968, 10989, 11066, 11067, 11068, 11147, 11148, 11149, 11162, 11263, 11264, 11313, 11314, 11322, 11376, 11377, 11435, 11455, 11456, 11457, 11458, 11459, 11460, 11490, 11491, 11492, 11493, 11494, 11512, 11513, 11528, 11571, 11572, 11624, 11625, 11634, 11699, 11700, 11701, 11702, 11822, 11823, 11824, 11825, 11826, 11834, 11881, 11898, 11899, 11900, 12057, 12058, 12059, 12060, 12061, 12077, 12201, 12326, 12417, 12443, 12444, 12445, 12487, 12488, 12489, 12591, 12592, 12593, 12594, 12602, 12603, 12604, 12656, 12673, 12674, 12754, 12755, 12779, 12780, 12927, 12928, 12936, 13045, 13046, 13047, 13048, 13049, 13050, 13051, 13052, 13177, 13178, 13179, 13219, 13220, 13221, 13222, 13223, 13224, 13275, 13276, 13277, 13278, 13305, 13402, 13437, 13438, 13439, 13653, 13654, 13655, 13656, 13706, 13746, 13747, 13748, 13749, 14029, 14137, 14233, 14234, 14235, 14296, 14385, 14392, 14520, 14521, 14522, 14789, 14790, 14791, 14932, 14933, 15342, 15343, 15344, 15345, 15425, 15426, 15427, 15428, 15527, 15588, 15589, 15590, 15711, 15893, 15943, 15944, 15945, 16267, 16268, 16471, 16472, 16506, 16507, 16508, 16509, 16510, 16523, 16524, 16525, 16563, 16643, 16644, 16690, 16691, 16757, 16758, 16759, 16843, 16902, 16903, 16904, 16935, 16964, 17045, 17244, 17257, 17292, 17410, 17418, 17419, 17500, 17534, 17535, 17585, 17586, 17629, 17687, 17688, 17689, 17690, 17691, 17692, 17899, 17900, 17901, 17956, 17957, 17958, 17959, 18057, 18103, 18104, 18105, 18106, 18107, 18197, 18198, 18284, 18285, 18376, 18392, 18393, 18435, 18646, 18687, 18696, 18697, 18801, 18847, 18944, 18945, 19118, 19205, 19300, 19301, 19487, 19513, 19530, 19531, 19532, 19533, 19549, 19599, 19600, 19601, 19602, 19756, 19774, 19775, 19776, 19821, 19822]\n",
+      "Indices of data-points that are subject to a trend: [4900, 5141, 9473, 19737]\n"
+     ]
+    }
+   ],
+   "source": [
+    "# calculate SPC using Shewart Individual Chart with limits based on the values of the first OK phase\n",
+    "chart = ShewartIndividualsChart(x[:950], x, estimated=False, alpha=0.05)\n",
+    "chart.calculate_spc()\n",
+    "info = chart.stability_info()"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:09.078799Z",
+     "start_time": "2024-04-12T16:21:09.018301Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Calculate Benchmark\n",
+    "Predicted label: Consider violations of charts as positive and all other datapoints as negative\n",
+    "\n",
+    "\n",
+    "True label: Consider violations of limits as positive and all datapoints within tolerance limits as negative"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "outputs": [],
+   "source": [
+    "violations = list(set().union(*[info['out_of_limits'], info['run'], info['trend']]))\n",
+    "replacements = [0]*len(violations)\n",
+    "y_pred = [1]*len(y_true)\n",
+    "for (index, replacement) in zip(violations, replacements):\n",
+    "        y_pred[index] = replacement\n",
+    "tp = [1 if t == 0 and p == 0 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "fn = [1 if t == 0 and p == 1 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "fp = [1 if t == 1 and p == 0 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "tn = 0\n",
+    "conf_mat = confusion_matrix(y_true, y_pred, normalize='all')\n",
+    "recall = recall_score(y_true, y_pred, pos_label=0)\n",
+    "precision = precision_score(y_true, y_pred, pos_label=0)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:09.140542Z",
+     "start_time": "2024-04-12T16:21:09.079803Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Display SPC benchmark results\n",
+    "#### Confusion matrix"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "outputs": [
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.Javascript object>",
+      "application/javascript": "/* Put everything inside the global mpl namespace */\n/* global mpl */\nwindow.mpl = {};\n\nmpl.get_websocket_type = function () {\n    if (typeof WebSocket !== 'undefined') {\n        return WebSocket;\n    } else if (typeof MozWebSocket !== 'undefined') {\n        return MozWebSocket;\n    } else {\n        alert(\n            'Your browser does not have WebSocket support. ' +\n                'Please try Chrome, Safari or Firefox ≥ 6. ' +\n                'Firefox 4 and 5 are also supported but you ' +\n                'have to enable WebSockets in about:config.'\n        );\n    }\n};\n\nmpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n    this.id = figure_id;\n\n    this.ws = websocket;\n\n    this.supports_binary = this.ws.binaryType !== undefined;\n\n    if (!this.supports_binary) {\n        var warnings = document.getElementById('mpl-warnings');\n        if (warnings) {\n            warnings.style.display = 'block';\n            warnings.textContent =\n                'This browser does not support binary websocket messages. ' +\n                'Performance may be slow.';\n        }\n    }\n\n    this.imageObj = new Image();\n\n    this.context = undefined;\n    this.message = undefined;\n    this.canvas = undefined;\n    this.rubberband_canvas = undefined;\n    this.rubberband_context = undefined;\n    this.format_dropdown = undefined;\n\n    this.image_mode = 'full';\n\n    this.root = document.createElement('div');\n    this.root.setAttribute('style', 'display: inline-block');\n    this._root_extra_style(this.root);\n\n    parent_element.appendChild(this.root);\n\n    this._init_header(this);\n    this._init_canvas(this);\n    this._init_toolbar(this);\n\n    var fig = this;\n\n    this.waiting = false;\n\n    this.ws.onopen = function () {\n        fig.send_message('supports_binary', { value: fig.supports_binary });\n        fig.send_message('send_image_mode', {});\n        if (fig.ratio !== 1) {\n            fig.send_message('set_device_pixel_ratio', {\n                device_pixel_ratio: fig.ratio,\n            });\n        }\n        fig.send_message('refresh', {});\n    };\n\n    this.imageObj.onload = function () {\n        if (fig.image_mode === 'full') {\n            // Full images could contain transparency (where diff images\n            // almost always do), so we need to clear the canvas so that\n            // there is no ghosting.\n            fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n        }\n        fig.context.drawImage(fig.imageObj, 0, 0);\n    };\n\n    this.imageObj.onunload = function () {\n        fig.ws.close();\n    };\n\n    this.ws.onmessage = this._make_on_message_function(this);\n\n    this.ondownload = ondownload;\n};\n\nmpl.figure.prototype._init_header = function () {\n    var titlebar = document.createElement('div');\n    titlebar.classList =\n        'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n    var titletext = document.createElement('div');\n    titletext.classList = 'ui-dialog-title';\n    titletext.setAttribute(\n        'style',\n        'width: 100%; text-align: center; padding: 3px;'\n    );\n    titlebar.appendChild(titletext);\n    this.root.appendChild(titlebar);\n    this.header = titletext;\n};\n\nmpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._init_canvas = function () {\n    var fig = this;\n\n    var canvas_div = (this.canvas_div = document.createElement('div'));\n    canvas_div.setAttribute('tabindex', '0');\n    canvas_div.setAttribute(\n        'style',\n        'border: 1px solid #ddd;' +\n            'box-sizing: content-box;' +\n            'clear: both;' +\n            'min-height: 1px;' +\n            'min-width: 1px;' +\n            'outline: 0;' +\n            'overflow: hidden;' +\n            'position: relative;' +\n            'resize: both;' +\n            'z-index: 2;'\n    );\n\n    function on_keyboard_event_closure(name) {\n        return function (event) {\n            return fig.key_event(event, name);\n        };\n    }\n\n    canvas_div.addEventListener(\n        'keydown',\n        on_keyboard_event_closure('key_press')\n    );\n    canvas_div.addEventListener(\n        'keyup',\n        on_keyboard_event_closure('key_release')\n    );\n\n    this._canvas_extra_style(canvas_div);\n    this.root.appendChild(canvas_div);\n\n    var canvas = (this.canvas = document.createElement('canvas'));\n    canvas.classList.add('mpl-canvas');\n    canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'pointer-events: none;' +\n            'position: relative;' +\n            'z-index: 0;'\n    );\n\n    this.context = canvas.getContext('2d');\n\n    var backingStore =\n        this.context.backingStorePixelRatio ||\n        this.context.webkitBackingStorePixelRatio ||\n        this.context.mozBackingStorePixelRatio ||\n        this.context.msBackingStorePixelRatio ||\n        this.context.oBackingStorePixelRatio ||\n        this.context.backingStorePixelRatio ||\n        1;\n\n    this.ratio = (window.devicePixelRatio || 1) / backingStore;\n\n    var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n        'canvas'\n    ));\n    rubberband_canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'left: 0;' +\n            'pointer-events: none;' +\n            'position: absolute;' +\n            'top: 0;' +\n            'z-index: 1;'\n    );\n\n    // Apply a ponyfill if ResizeObserver is not implemented by browser.\n    if (this.ResizeObserver === undefined) {\n        if (window.ResizeObserver !== undefined) {\n            this.ResizeObserver = window.ResizeObserver;\n        } else {\n            var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n            this.ResizeObserver = obs.ResizeObserver;\n        }\n    }\n\n    this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n        var nentries = entries.length;\n        for (var i = 0; i < nentries; i++) {\n            var entry = entries[i];\n            var width, height;\n            if (entry.contentBoxSize) {\n                if (entry.contentBoxSize instanceof Array) {\n                    // Chrome 84 implements new version of spec.\n                    width = entry.contentBoxSize[0].inlineSize;\n                    height = entry.contentBoxSize[0].blockSize;\n                } else {\n                    // Firefox implements old version of spec.\n                    width = entry.contentBoxSize.inlineSize;\n                    height = entry.contentBoxSize.blockSize;\n                }\n            } else {\n                // Chrome <84 implements even older version of spec.\n                width = entry.contentRect.width;\n                height = entry.contentRect.height;\n            }\n\n            // Keep the size of the canvas and rubber band canvas in sync with\n            // the canvas container.\n            if (entry.devicePixelContentBoxSize) {\n                // Chrome 84 implements new version of spec.\n                canvas.setAttribute(\n                    'width',\n                    entry.devicePixelContentBoxSize[0].inlineSize\n                );\n                canvas.setAttribute(\n                    'height',\n                    entry.devicePixelContentBoxSize[0].blockSize\n                );\n            } else {\n                canvas.setAttribute('width', width * fig.ratio);\n                canvas.setAttribute('height', height * fig.ratio);\n            }\n            /* This rescales the canvas back to display pixels, so that it\n             * appears correct on HiDPI screens. */\n            canvas.style.width = width + 'px';\n            canvas.style.height = height + 'px';\n\n            rubberband_canvas.setAttribute('width', width);\n            rubberband_canvas.setAttribute('height', height);\n\n            // And update the size in Python. We ignore the initial 0/0 size\n            // that occurs as the element is placed into the DOM, which should\n            // otherwise not happen due to the minimum size styling.\n            if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n                fig.request_resize(width, height);\n            }\n        }\n    });\n    this.resizeObserverInstance.observe(canvas_div);\n\n    function on_mouse_event_closure(name) {\n        /* User Agent sniffing is bad, but WebKit is busted:\n         * https://bugs.webkit.org/show_bug.cgi?id=144526\n         * https://bugs.webkit.org/show_bug.cgi?id=181818\n         * The worst that happens here is that they get an extra browser\n         * selection when dragging, if this check fails to catch them.\n         */\n        var UA = navigator.userAgent;\n        var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n        if(isWebKit) {\n            return function (event) {\n                /* This prevents the web browser from automatically changing to\n                 * the text insertion cursor when the button is pressed. We\n                 * want to control all of the cursor setting manually through\n                 * the 'cursor' event from matplotlib */\n                event.preventDefault()\n                return fig.mouse_event(event, name);\n            };\n        } else {\n            return function (event) {\n                return fig.mouse_event(event, name);\n            };\n        }\n    }\n\n    canvas_div.addEventListener(\n        'mousedown',\n        on_mouse_event_closure('button_press')\n    );\n    canvas_div.addEventListener(\n        'mouseup',\n        on_mouse_event_closure('button_release')\n    );\n    canvas_div.addEventListener(\n        'dblclick',\n        on_mouse_event_closure('dblclick')\n    );\n    // Throttle sequential mouse events to 1 every 20ms.\n    canvas_div.addEventListener(\n        'mousemove',\n        on_mouse_event_closure('motion_notify')\n    );\n\n    canvas_div.addEventListener(\n        'mouseenter',\n        on_mouse_event_closure('figure_enter')\n    );\n    canvas_div.addEventListener(\n        'mouseleave',\n        on_mouse_event_closure('figure_leave')\n    );\n\n    canvas_div.addEventListener('wheel', function (event) {\n        if (event.deltaY < 0) {\n            event.step = 1;\n        } else {\n            event.step = -1;\n        }\n        on_mouse_event_closure('scroll')(event);\n    });\n\n    canvas_div.appendChild(canvas);\n    canvas_div.appendChild(rubberband_canvas);\n\n    this.rubberband_context = rubberband_canvas.getContext('2d');\n    this.rubberband_context.strokeStyle = '#000000';\n\n    this._resize_canvas = function (width, height, forward) {\n        if (forward) {\n            canvas_div.style.width = width + 'px';\n            canvas_div.style.height = height + 'px';\n        }\n    };\n\n    // Disable right mouse context menu.\n    canvas_div.addEventListener('contextmenu', function (_e) {\n        event.preventDefault();\n        return false;\n    });\n\n    function set_focus() {\n        canvas.focus();\n        canvas_div.focus();\n    }\n\n    window.setTimeout(set_focus, 100);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'mpl-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'mpl-button-group';\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'mpl-button-group';\n            continue;\n        }\n\n        var button = (fig.buttons[name] = document.createElement('button'));\n        button.classList = 'mpl-widget';\n        button.setAttribute('role', 'button');\n        button.setAttribute('aria-disabled', 'false');\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n\n        var icon_img = document.createElement('img');\n        icon_img.src = '_images/' + image + '.png';\n        icon_img.srcset = '_images/' + image + '_large.png 2x';\n        icon_img.alt = tooltip;\n        button.appendChild(icon_img);\n\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    var fmt_picker = document.createElement('select');\n    fmt_picker.classList = 'mpl-widget';\n    toolbar.appendChild(fmt_picker);\n    this.format_dropdown = fmt_picker;\n\n    for (var ind in mpl.extensions) {\n        var fmt = mpl.extensions[ind];\n        var option = document.createElement('option');\n        option.selected = fmt === mpl.default_extension;\n        option.innerHTML = fmt;\n        fmt_picker.appendChild(option);\n    }\n\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n};\n\nmpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n    // which will in turn request a refresh of the image.\n    this.send_message('resize', { width: x_pixels, height: y_pixels });\n};\n\nmpl.figure.prototype.send_message = function (type, properties) {\n    properties['type'] = type;\n    properties['figure_id'] = this.id;\n    this.ws.send(JSON.stringify(properties));\n};\n\nmpl.figure.prototype.send_draw_message = function () {\n    if (!this.waiting) {\n        this.waiting = true;\n        this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    var format_dropdown = fig.format_dropdown;\n    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n    fig.ondownload(fig, format);\n};\n\nmpl.figure.prototype.handle_resize = function (fig, msg) {\n    var size = msg['size'];\n    if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n        fig._resize_canvas(size[0], size[1], msg['forward']);\n        fig.send_message('refresh', {});\n    }\n};\n\nmpl.figure.prototype.handle_rubberband = function (fig, msg) {\n    var x0 = msg['x0'] / fig.ratio;\n    var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n    var x1 = msg['x1'] / fig.ratio;\n    var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n    x0 = Math.floor(x0) + 0.5;\n    y0 = Math.floor(y0) + 0.5;\n    x1 = Math.floor(x1) + 0.5;\n    y1 = Math.floor(y1) + 0.5;\n    var min_x = Math.min(x0, x1);\n    var min_y = Math.min(y0, y1);\n    var width = Math.abs(x1 - x0);\n    var height = Math.abs(y1 - y0);\n\n    fig.rubberband_context.clearRect(\n        0,\n        0,\n        fig.canvas.width / fig.ratio,\n        fig.canvas.height / fig.ratio\n    );\n\n    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n};\n\nmpl.figure.prototype.handle_figure_label = function (fig, msg) {\n    // Updates the figure title.\n    fig.header.textContent = msg['label'];\n};\n\nmpl.figure.prototype.handle_cursor = function (fig, msg) {\n    fig.canvas_div.style.cursor = msg['cursor'];\n};\n\nmpl.figure.prototype.handle_message = function (fig, msg) {\n    fig.message.textContent = msg['message'];\n};\n\nmpl.figure.prototype.handle_draw = function (fig, _msg) {\n    // Request the server to send over a new figure.\n    fig.send_draw_message();\n};\n\nmpl.figure.prototype.handle_image_mode = function (fig, msg) {\n    fig.image_mode = msg['mode'];\n};\n\nmpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n    for (var key in msg) {\n        if (!(key in fig.buttons)) {\n            continue;\n        }\n        fig.buttons[key].disabled = !msg[key];\n        fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n    }\n};\n\nmpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n    if (msg['mode'] === 'PAN') {\n        fig.buttons['Pan'].classList.add('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    } else if (msg['mode'] === 'ZOOM') {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.add('active');\n    } else {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    }\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Called whenever the canvas gets updated.\n    this.send_message('ack', {});\n};\n\n// A function to construct a web socket function for onmessage handling.\n// Called in the figure constructor.\nmpl.figure.prototype._make_on_message_function = function (fig) {\n    return function socket_on_message(evt) {\n        if (evt.data instanceof Blob) {\n            var img = evt.data;\n            if (img.type !== 'image/png') {\n                /* FIXME: We get \"Resource interpreted as Image but\n                 * transferred with MIME type text/plain:\" errors on\n                 * Chrome.  But how to set the MIME type?  It doesn't seem\n                 * to be part of the websocket stream */\n                img.type = 'image/png';\n            }\n\n            /* Free the memory for the previous frames */\n            if (fig.imageObj.src) {\n                (window.URL || window.webkitURL).revokeObjectURL(\n                    fig.imageObj.src\n                );\n            }\n\n            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n                img\n            );\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        } else if (\n            typeof evt.data === 'string' &&\n            evt.data.slice(0, 21) === 'data:image/png;base64'\n        ) {\n            fig.imageObj.src = evt.data;\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        }\n\n        var msg = JSON.parse(evt.data);\n        var msg_type = msg['type'];\n\n        // Call the  \"handle_{type}\" callback, which takes\n        // the figure and JSON message as its only arguments.\n        try {\n            var callback = fig['handle_' + msg_type];\n        } catch (e) {\n            console.log(\n                \"No handler for the '\" + msg_type + \"' message type: \",\n                msg\n            );\n            return;\n        }\n\n        if (callback) {\n            try {\n                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n                callback(fig, msg);\n            } catch (e) {\n                console.log(\n                    \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n                    e,\n                    e.stack,\n                    msg\n                );\n            }\n        }\n    };\n};\n\nfunction getModifiers(event) {\n    var mods = [];\n    if (event.ctrlKey) {\n        mods.push('ctrl');\n    }\n    if (event.altKey) {\n        mods.push('alt');\n    }\n    if (event.shiftKey) {\n        mods.push('shift');\n    }\n    if (event.metaKey) {\n        mods.push('meta');\n    }\n    return mods;\n}\n\n/*\n * return a copy of an object with only non-object keys\n * we need this to avoid circular references\n * https://stackoverflow.com/a/24161582/3208463\n */\nfunction simpleKeys(original) {\n    return Object.keys(original).reduce(function (obj, key) {\n        if (typeof original[key] !== 'object') {\n            obj[key] = original[key];\n        }\n        return obj;\n    }, {});\n}\n\nmpl.figure.prototype.mouse_event = function (event, name) {\n    if (name === 'button_press') {\n        this.canvas.focus();\n        this.canvas_div.focus();\n    }\n\n    // from https://stackoverflow.com/q/1114465\n    var boundingRect = this.canvas.getBoundingClientRect();\n    var x = (event.clientX - boundingRect.left) * this.ratio;\n    var y = (event.clientY - boundingRect.top) * this.ratio;\n\n    this.send_message(name, {\n        x: x,\n        y: y,\n        button: event.button,\n        step: event.step,\n        modifiers: getModifiers(event),\n        guiEvent: simpleKeys(event),\n    });\n\n    return false;\n};\n\nmpl.figure.prototype._key_event_extra = function (_event, _name) {\n    // Handle any extra behaviour associated with a key event\n};\n\nmpl.figure.prototype.key_event = function (event, name) {\n    // Prevent repeat events\n    if (name === 'key_press') {\n        if (event.key === this._key) {\n            return;\n        } else {\n            this._key = event.key;\n        }\n    }\n    if (name === 'key_release') {\n        this._key = null;\n    }\n\n    var value = '';\n    if (event.ctrlKey && event.key !== 'Control') {\n        value += 'ctrl+';\n    }\n    else if (event.altKey && event.key !== 'Alt') {\n        value += 'alt+';\n    }\n    else if (event.shiftKey && event.key !== 'Shift') {\n        value += 'shift+';\n    }\n\n    value += 'k' + event.key;\n\n    this._key_event_extra(event, name);\n\n    this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n    return false;\n};\n\nmpl.figure.prototype.toolbar_button_onclick = function (name) {\n    if (name === 'download') {\n        this.handle_save(this, null);\n    } else {\n        this.send_message('toolbar_button', { name: name });\n    }\n};\n\nmpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n    this.message.textContent = tooltip;\n};\n\n///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n// prettier-ignore\nvar _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\nmpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n\nmpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n\nmpl.default_extension = \"png\";/* global mpl */\n\nvar comm_websocket_adapter = function (comm) {\n    // Create a \"websocket\"-like object which calls the given IPython comm\n    // object with the appropriate methods. Currently this is a non binary\n    // socket, so there is still some room for performance tuning.\n    var ws = {};\n\n    ws.binaryType = comm.kernel.ws.binaryType;\n    ws.readyState = comm.kernel.ws.readyState;\n    function updateReadyState(_event) {\n        if (comm.kernel.ws) {\n            ws.readyState = comm.kernel.ws.readyState;\n        } else {\n            ws.readyState = 3; // Closed state.\n        }\n    }\n    comm.kernel.ws.addEventListener('open', updateReadyState);\n    comm.kernel.ws.addEventListener('close', updateReadyState);\n    comm.kernel.ws.addEventListener('error', updateReadyState);\n\n    ws.close = function () {\n        comm.close();\n    };\n    ws.send = function (m) {\n        //console.log('sending', m);\n        comm.send(m);\n    };\n    // Register the callback with on_msg.\n    comm.on_msg(function (msg) {\n        //console.log('receiving', msg['content']['data'], msg);\n        var data = msg['content']['data'];\n        if (data['blob'] !== undefined) {\n            data = {\n                data: new Blob(msg['buffers'], { type: data['blob'] }),\n            };\n        }\n        // Pass the mpl event to the overridden (by mpl) onmessage function.\n        ws.onmessage(data);\n    });\n    return ws;\n};\n\nmpl.mpl_figure_comm = function (comm, msg) {\n    // This is the function which gets called when the mpl process\n    // starts-up an IPython Comm through the \"matplotlib\" channel.\n\n    var id = msg.content.data.id;\n    // Get hold of the div created by the display call when the Comm\n    // socket was opened in Python.\n    var element = document.getElementById(id);\n    var ws_proxy = comm_websocket_adapter(comm);\n\n    function ondownload(figure, _format) {\n        window.open(figure.canvas.toDataURL());\n    }\n\n    var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n\n    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n    // web socket which is closed, not our websocket->open comm proxy.\n    ws_proxy.onopen();\n\n    fig.parent_element = element;\n    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n    if (!fig.cell_info) {\n        console.error('Failed to find cell for figure', id, fig);\n        return;\n    }\n    fig.cell_info[0].output_area.element.on(\n        'cleared',\n        { fig: fig },\n        fig._remove_fig_handler\n    );\n};\n\nmpl.figure.prototype.handle_close = function (fig, msg) {\n    var width = fig.canvas.width / fig.ratio;\n    fig.cell_info[0].output_area.element.off(\n        'cleared',\n        fig._remove_fig_handler\n    );\n    fig.resizeObserverInstance.unobserve(fig.canvas_div);\n\n    // Update the output cell to use the data from the current canvas.\n    fig.push_to_output();\n    var dataURL = fig.canvas.toDataURL();\n    // Re-enable the keyboard manager in IPython - without this line, in FF,\n    // the notebook keyboard shortcuts fail.\n    IPython.keyboard_manager.enable();\n    fig.parent_element.innerHTML =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n    fig.close_ws(fig, msg);\n};\n\nmpl.figure.prototype.close_ws = function (fig, msg) {\n    fig.send_message('closing', msg);\n    // fig.ws.close()\n};\n\nmpl.figure.prototype.push_to_output = function (_remove_interactive) {\n    // Turn the data on the canvas into data in the output cell.\n    var width = this.canvas.width / this.ratio;\n    var dataURL = this.canvas.toDataURL();\n    this.cell_info[1]['text/html'] =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Tell IPython that the notebook contents must change.\n    IPython.notebook.set_dirty(true);\n    this.send_message('ack', {});\n    var fig = this;\n    // Wait a second, then push the new image to the DOM so\n    // that it is saved nicely (might be nice to debounce this).\n    setTimeout(function () {\n        fig.push_to_output();\n    }, 1000);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'btn-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'btn-group';\n    var button;\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'btn-group';\n            continue;\n        }\n\n        button = fig.buttons[name] = document.createElement('button');\n        button.classList = 'btn btn-default';\n        button.href = '#';\n        button.title = name;\n        button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    // Add the status bar.\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message pull-right';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n\n    // Add the close button to the window.\n    var buttongrp = document.createElement('div');\n    buttongrp.classList = 'btn-group inline pull-right';\n    button = document.createElement('button');\n    button.classList = 'btn btn-mini btn-primary';\n    button.href = '#';\n    button.title = 'Stop Interaction';\n    button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n    button.addEventListener('click', function (_evt) {\n        fig.handle_close(fig, {});\n    });\n    button.addEventListener(\n        'mouseover',\n        on_mouseover_closure('Stop Interaction')\n    );\n    buttongrp.appendChild(button);\n    var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n    titlebar.insertBefore(buttongrp, titlebar.firstChild);\n};\n\nmpl.figure.prototype._remove_fig_handler = function (event) {\n    var fig = event.data.fig;\n    if (event.target !== this) {\n        // Ignore bubbled events from children.\n        return;\n    }\n    fig.close_ws(fig, {});\n};\n\nmpl.figure.prototype._root_extra_style = function (el) {\n    el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n};\n\nmpl.figure.prototype._canvas_extra_style = function (el) {\n    // this is important to make the div 'focusable\n    el.setAttribute('tabindex', 0);\n    // reach out to IPython and tell the keyboard manager to turn it's self\n    // off when our div gets focus\n\n    // location in version 3\n    if (IPython.notebook.keyboard_manager) {\n        IPython.notebook.keyboard_manager.register_events(el);\n    } else {\n        // location in version 2\n        IPython.keyboard_manager.register_events(el);\n    }\n};\n\nmpl.figure.prototype._key_event_extra = function (event, _name) {\n    // Check for shift+enter\n    if (event.shiftKey && event.which === 13) {\n        this.canvas_div.blur();\n        // select the cell after this one\n        var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n        IPython.notebook.select(index + 1);\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    fig.ondownload(fig, null);\n};\n\nmpl.find_output_cell = function (html_output) {\n    // Return the cell and output element which can be found *uniquely* in the notebook.\n    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n    // IPython event is triggered only after the cells have been serialised, which for\n    // our purposes (turning an active figure into a static one), is too late.\n    var cells = IPython.notebook.get_cells();\n    var ncells = cells.length;\n    for (var i = 0; i < ncells; i++) {\n        var cell = cells[i];\n        if (cell.cell_type === 'code') {\n            for (var j = 0; j < cell.output_area.outputs.length; j++) {\n                var data = cell.output_area.outputs[j];\n                if (data.data) {\n                    // IPython >= 3 moved mimebundle to data attribute of output\n                    data = data.data;\n                }\n                if (data['text/html'] === html_output) {\n                    return [cell, data, j];\n                }\n            }\n        }\n    }\n};\n\n// Register the function which deals with the matplotlib target/channel.\n// The kernel may be null if the page has been refreshed.\nif (IPython.notebook.kernel !== null) {\n    IPython.notebook.kernel.comm_manager.register_target(\n        'matplotlib',\n        mpl.mpl_figure_comm\n    );\n}\n"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.HTML object>",
+      "text/html": "<div id='aea86a9b-e352-4733-aa14-bfdc1d8939dc'></div>"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True positive: 2887\n",
+      "False negative: 0\n",
+      "False positive: 2173\n",
+      "True negative: Not defined\n"
+     ]
+    }
+   ],
+   "source": [
+    "disp = ConfusionMatrixDisplay(conf_mat)\n",
+    "disp.plot()\n",
+    "plt.grid(False)\n",
+    "plt.show()\n",
+    "\n",
+    "print(f'True positive: {tp}')\n",
+    "print(f'False negative: {fn}')\n",
+    "print(f'False positive: {fp}')\n",
+    "print(f'True negative: Not defined')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:09.219330Z",
+     "start_time": "2024-04-12T16:21:09.142839Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "#### Precision, recall and F1-score"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recall: 1.0\n",
+      "Precision: 0.5705533596837945\n",
+      "F1-score: 0.7265634830753743\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f'Recall: {recall}')\n",
+    "print(f'Precision: {precision}')\n",
+    "print(f'F1-score: {2*precision*recall/(precision+recall)}')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:21:26.552023Z",
+     "start_time": "2024-04-12T16:21:26.544573Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Benchmark Convolutional Neural Network"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "True label: Positive (0) when there is a limit violation within the time window (In case of synthetic generated data: and if any value of the input sliding window stems from NOK phase)\n",
+    "Predicted: Positive (0) when there is a signal\n",
+    "\n",
+    "This results in the following scheme for the confusion matrix:\n",
+    "Synthetic data:\n",
+    "\n",
+    "| --- | --- | --- | --- |\n",
+    "| Case | A | B | C |\n",
+    "| True Positive | 0 | 0 | 0 |\n",
+    "| False Positive | 0 | 1 | 0/1 |\n",
+    "|  | 0 | 0 | 1 |\n",
+    "| False Negative | 1 | 0 | 0 |\n",
+    "\n",
+    "Real data (NOK phase unknown):\n",
+    "\n",
+    "| --- | --- | --- |\n",
+    "| Case | A | C |\n",
+    "| True Positive | 0 | 0 |\n",
+    "| False Positive | 0 | 1 |\n",
+    "| False Negative | 1 | 0 |\n",
+    "\n",
+    "\n",
+    "True Negative is not defined. The reason for this is provided in the paper.\n",
+    "\n",
+    "Process / Prediction is OK?\n",
+    "True = 1\n",
+    "False = 0\n",
+    "\n",
+    "Assumption: Actual NOK-Phase where sum of weights of NOK distributions >= 0.5\n",
+    "\n",
+    "A: Predicted (Signal from CNN?):\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | anomaly score meets or exceeds threshold |\n",
+    "| 1 | anomaly score below threshold|\n",
+    "\n",
+    "B: Datapoint from NOK phase?:\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | at least one data point in sliding window is from NOK phase |\n",
+    "| 1 | all datapoints in sliding window stem from OK phase |\n",
+    "\n",
+    "C: Violation of tolerance limits?:\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | at least one violation of tolerance limits within sliding window |\n",
+    "| 1 | no violation of tolerance limits within sliding window |"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "outputs": [],
+   "source": [
+    "PTH_OBS = \"../../data/test/x.csv\"\n",
+    "PTH_INSP = \"../../data/test/y.csv\"\n",
+    "PTH_KMAT = \"../../data/test/k_matrix.csv\"\n",
+    "PTH_PHASE = \"../../data/test/phase.csv\"\n",
+    "OBS_WIN = 18\n",
+    "RAMP_LEN = 3\n",
+    "ANOM_SCALE = 0.5\n",
+    "ANOM_THRESHOLD = 0.1\n",
+    "\n",
+    "n=18"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:05.251371Z",
+     "start_time": "2024-04-12T16:23:05.246372Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "outputs": [],
+   "source": [
+    "# Preparations and B (phase_info)\n",
+    "kmat = pd.read_csv(PTH_KMAT, header=None)\n",
+    "phase = (kmat.iloc[:, -1] < 0.00001).to_numpy()\n",
+    "np.savetxt(PTH_PHASE, phase, delimiter=',')\n",
+    "def load_fn(x):\n",
+    "    return pd.read_csv(x, header=None).to_numpy().flatten()\n",
+    "x, y, phase_info = prepare_benchmark_data(\n",
+    "        pth_observation=PTH_OBS,\n",
+    "        pth_inspect=PTH_INSP,\n",
+    "        pth_phase=PTH_KMAT,\n",
+    "        load_fn=load_fn,\n",
+    "        observation_length=OBS_WIN,\n",
+    "        ramp_length=RAMP_LEN,\n",
+    "        anomaly_scale=ANOM_SCALE,\n",
+    "    )"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:06.565170Z",
+     "start_time": "2024-04-12T16:23:05.812729Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "WARNING:tensorflow:From C:\\Users\\adam-ix592mtcb219gzd\\Anaconda3\\envs\\spc2ml_new\\lib\\site-packages\\keras\\src\\backend.py:1398: The name tf.executing_eagerly_outside_functions is deprecated. Please use tf.compat.v1.executing_eagerly_outside_functions instead.\n",
+      "WARNING:tensorflow:From C:\\Users\\adam-ix592mtcb219gzd\\Anaconda3\\envs\\spc2ml_new\\lib\\site-packages\\keras\\src\\backend.py:6642: The name tf.nn.max_pool is deprecated. Please use tf.nn.max_pool2d instead.\n"
+     ]
+    }
+   ],
+   "source": [
+    "# load model\n",
+    "custom_objects = {'MeanSubLayer': MeanSubLayer, 'rebalanced_nok_mse': rebalanced_nok_mse}\n",
+    "model = keras.models.load_model(\"../model_training/cnn/model/best_model.h5\", custom_objects=custom_objects)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:38.026592Z",
+     "start_time": "2024-04-12T16:23:36.822367Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "625/625 [==============================] - 7s 2ms/step\n"
+     ]
+    }
+   ],
+   "source": [
+    "# make predictions\n",
+    "ys = model.predict(x)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.164562Z",
+     "start_time": "2024-04-12T16:23:39.141025Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "outputs": [],
+   "source": [
+    "# A\n",
+    "y_pred = np.array(ys)\n",
+    "# Process OK? True->1, False->0\n",
+    "pred = np.where(y_pred < ANOM_THRESHOLD, 1, 0)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.180038Z",
+     "start_time": "2024-04-12T16:23:46.166757Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "outputs": [],
+   "source": [
+    "# C\n",
+    "limit_violation = []\n",
+    "\n",
+    "for window in y:\n",
+    "        # only the last n steps of time window considered\n",
+    "        limit_violation.append(not window[-n:].any())"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.226756Z",
+     "start_time": "2024-04-12T16:23:46.182061Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "outputs": [],
+   "source": [
+    "if len(phase_info.shape)== 1:\n",
+    "    tp = [1 if a == 0 and b == 0 and c == 0 else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "    fn = [1 if a == 1 and b == 0 and c == 0 else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "    fp = [1 if (a == 0 and b == 1 ) or (a == 0 and b == 0 and c == 1) else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "elif len(phase_info.shape)== 0:\n",
+    "    tp = [1 if a == 0 and c == 0 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "    fn = [1 if a == 1 and c == 0 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "    fp = [1 if a == 0 and c == 1 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "else:\n",
+    "    raise ValueError('The given format of phase_info is not supported. It has to be a 0- or 1-dimensional numpy array.')\n",
+    "\n",
+    "tn = 0\n",
+    "n_total = ys.shape[0]"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.352453Z",
+     "start_time": "2024-04-12T16:23:46.228903Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "outputs": [
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.Javascript object>",
+      "application/javascript": "/* Put everything inside the global mpl namespace */\n/* global mpl */\nwindow.mpl = {};\n\nmpl.get_websocket_type = function () {\n    if (typeof WebSocket !== 'undefined') {\n        return WebSocket;\n    } else if (typeof MozWebSocket !== 'undefined') {\n        return MozWebSocket;\n    } else {\n        alert(\n            'Your browser does not have WebSocket support. ' +\n                'Please try Chrome, Safari or Firefox ≥ 6. ' +\n                'Firefox 4 and 5 are also supported but you ' +\n                'have to enable WebSockets in about:config.'\n        );\n    }\n};\n\nmpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n    this.id = figure_id;\n\n    this.ws = websocket;\n\n    this.supports_binary = this.ws.binaryType !== undefined;\n\n    if (!this.supports_binary) {\n        var warnings = document.getElementById('mpl-warnings');\n        if (warnings) {\n            warnings.style.display = 'block';\n            warnings.textContent =\n                'This browser does not support binary websocket messages. ' +\n                'Performance may be slow.';\n        }\n    }\n\n    this.imageObj = new Image();\n\n    this.context = undefined;\n    this.message = undefined;\n    this.canvas = undefined;\n    this.rubberband_canvas = undefined;\n    this.rubberband_context = undefined;\n    this.format_dropdown = undefined;\n\n    this.image_mode = 'full';\n\n    this.root = document.createElement('div');\n    this.root.setAttribute('style', 'display: inline-block');\n    this._root_extra_style(this.root);\n\n    parent_element.appendChild(this.root);\n\n    this._init_header(this);\n    this._init_canvas(this);\n    this._init_toolbar(this);\n\n    var fig = this;\n\n    this.waiting = false;\n\n    this.ws.onopen = function () {\n        fig.send_message('supports_binary', { value: fig.supports_binary });\n        fig.send_message('send_image_mode', {});\n        if (fig.ratio !== 1) {\n            fig.send_message('set_device_pixel_ratio', {\n                device_pixel_ratio: fig.ratio,\n            });\n        }\n        fig.send_message('refresh', {});\n    };\n\n    this.imageObj.onload = function () {\n        if (fig.image_mode === 'full') {\n            // Full images could contain transparency (where diff images\n            // almost always do), so we need to clear the canvas so that\n            // there is no ghosting.\n            fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n        }\n        fig.context.drawImage(fig.imageObj, 0, 0);\n    };\n\n    this.imageObj.onunload = function () {\n        fig.ws.close();\n    };\n\n    this.ws.onmessage = this._make_on_message_function(this);\n\n    this.ondownload = ondownload;\n};\n\nmpl.figure.prototype._init_header = function () {\n    var titlebar = document.createElement('div');\n    titlebar.classList =\n        'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n    var titletext = document.createElement('div');\n    titletext.classList = 'ui-dialog-title';\n    titletext.setAttribute(\n        'style',\n        'width: 100%; text-align: center; padding: 3px;'\n    );\n    titlebar.appendChild(titletext);\n    this.root.appendChild(titlebar);\n    this.header = titletext;\n};\n\nmpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._init_canvas = function () {\n    var fig = this;\n\n    var canvas_div = (this.canvas_div = document.createElement('div'));\n    canvas_div.setAttribute('tabindex', '0');\n    canvas_div.setAttribute(\n        'style',\n        'border: 1px solid #ddd;' +\n            'box-sizing: content-box;' +\n            'clear: both;' +\n            'min-height: 1px;' +\n            'min-width: 1px;' +\n            'outline: 0;' +\n            'overflow: hidden;' +\n            'position: relative;' +\n            'resize: both;' +\n            'z-index: 2;'\n    );\n\n    function on_keyboard_event_closure(name) {\n        return function (event) {\n            return fig.key_event(event, name);\n        };\n    }\n\n    canvas_div.addEventListener(\n        'keydown',\n        on_keyboard_event_closure('key_press')\n    );\n    canvas_div.addEventListener(\n        'keyup',\n        on_keyboard_event_closure('key_release')\n    );\n\n    this._canvas_extra_style(canvas_div);\n    this.root.appendChild(canvas_div);\n\n    var canvas = (this.canvas = document.createElement('canvas'));\n    canvas.classList.add('mpl-canvas');\n    canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'pointer-events: none;' +\n            'position: relative;' +\n            'z-index: 0;'\n    );\n\n    this.context = canvas.getContext('2d');\n\n    var backingStore =\n        this.context.backingStorePixelRatio ||\n        this.context.webkitBackingStorePixelRatio ||\n        this.context.mozBackingStorePixelRatio ||\n        this.context.msBackingStorePixelRatio ||\n        this.context.oBackingStorePixelRatio ||\n        this.context.backingStorePixelRatio ||\n        1;\n\n    this.ratio = (window.devicePixelRatio || 1) / backingStore;\n\n    var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n        'canvas'\n    ));\n    rubberband_canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'left: 0;' +\n            'pointer-events: none;' +\n            'position: absolute;' +\n            'top: 0;' +\n            'z-index: 1;'\n    );\n\n    // Apply a ponyfill if ResizeObserver is not implemented by browser.\n    if (this.ResizeObserver === undefined) {\n        if (window.ResizeObserver !== undefined) {\n            this.ResizeObserver = window.ResizeObserver;\n        } else {\n            var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n            this.ResizeObserver = obs.ResizeObserver;\n        }\n    }\n\n    this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n        var nentries = entries.length;\n        for (var i = 0; i < nentries; i++) {\n            var entry = entries[i];\n            var width, height;\n            if (entry.contentBoxSize) {\n                if (entry.contentBoxSize instanceof Array) {\n                    // Chrome 84 implements new version of spec.\n                    width = entry.contentBoxSize[0].inlineSize;\n                    height = entry.contentBoxSize[0].blockSize;\n                } else {\n                    // Firefox implements old version of spec.\n                    width = entry.contentBoxSize.inlineSize;\n                    height = entry.contentBoxSize.blockSize;\n                }\n            } else {\n                // Chrome <84 implements even older version of spec.\n                width = entry.contentRect.width;\n                height = entry.contentRect.height;\n            }\n\n            // Keep the size of the canvas and rubber band canvas in sync with\n            // the canvas container.\n            if (entry.devicePixelContentBoxSize) {\n                // Chrome 84 implements new version of spec.\n                canvas.setAttribute(\n                    'width',\n                    entry.devicePixelContentBoxSize[0].inlineSize\n                );\n                canvas.setAttribute(\n                    'height',\n                    entry.devicePixelContentBoxSize[0].blockSize\n                );\n            } else {\n                canvas.setAttribute('width', width * fig.ratio);\n                canvas.setAttribute('height', height * fig.ratio);\n            }\n            /* This rescales the canvas back to display pixels, so that it\n             * appears correct on HiDPI screens. */\n            canvas.style.width = width + 'px';\n            canvas.style.height = height + 'px';\n\n            rubberband_canvas.setAttribute('width', width);\n            rubberband_canvas.setAttribute('height', height);\n\n            // And update the size in Python. We ignore the initial 0/0 size\n            // that occurs as the element is placed into the DOM, which should\n            // otherwise not happen due to the minimum size styling.\n            if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n                fig.request_resize(width, height);\n            }\n        }\n    });\n    this.resizeObserverInstance.observe(canvas_div);\n\n    function on_mouse_event_closure(name) {\n        /* User Agent sniffing is bad, but WebKit is busted:\n         * https://bugs.webkit.org/show_bug.cgi?id=144526\n         * https://bugs.webkit.org/show_bug.cgi?id=181818\n         * The worst that happens here is that they get an extra browser\n         * selection when dragging, if this check fails to catch them.\n         */\n        var UA = navigator.userAgent;\n        var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n        if(isWebKit) {\n            return function (event) {\n                /* This prevents the web browser from automatically changing to\n                 * the text insertion cursor when the button is pressed. We\n                 * want to control all of the cursor setting manually through\n                 * the 'cursor' event from matplotlib */\n                event.preventDefault()\n                return fig.mouse_event(event, name);\n            };\n        } else {\n            return function (event) {\n                return fig.mouse_event(event, name);\n            };\n        }\n    }\n\n    canvas_div.addEventListener(\n        'mousedown',\n        on_mouse_event_closure('button_press')\n    );\n    canvas_div.addEventListener(\n        'mouseup',\n        on_mouse_event_closure('button_release')\n    );\n    canvas_div.addEventListener(\n        'dblclick',\n        on_mouse_event_closure('dblclick')\n    );\n    // Throttle sequential mouse events to 1 every 20ms.\n    canvas_div.addEventListener(\n        'mousemove',\n        on_mouse_event_closure('motion_notify')\n    );\n\n    canvas_div.addEventListener(\n        'mouseenter',\n        on_mouse_event_closure('figure_enter')\n    );\n    canvas_div.addEventListener(\n        'mouseleave',\n        on_mouse_event_closure('figure_leave')\n    );\n\n    canvas_div.addEventListener('wheel', function (event) {\n        if (event.deltaY < 0) {\n            event.step = 1;\n        } else {\n            event.step = -1;\n        }\n        on_mouse_event_closure('scroll')(event);\n    });\n\n    canvas_div.appendChild(canvas);\n    canvas_div.appendChild(rubberband_canvas);\n\n    this.rubberband_context = rubberband_canvas.getContext('2d');\n    this.rubberband_context.strokeStyle = '#000000';\n\n    this._resize_canvas = function (width, height, forward) {\n        if (forward) {\n            canvas_div.style.width = width + 'px';\n            canvas_div.style.height = height + 'px';\n        }\n    };\n\n    // Disable right mouse context menu.\n    canvas_div.addEventListener('contextmenu', function (_e) {\n        event.preventDefault();\n        return false;\n    });\n\n    function set_focus() {\n        canvas.focus();\n        canvas_div.focus();\n    }\n\n    window.setTimeout(set_focus, 100);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'mpl-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'mpl-button-group';\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'mpl-button-group';\n            continue;\n        }\n\n        var button = (fig.buttons[name] = document.createElement('button'));\n        button.classList = 'mpl-widget';\n        button.setAttribute('role', 'button');\n        button.setAttribute('aria-disabled', 'false');\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n\n        var icon_img = document.createElement('img');\n        icon_img.src = '_images/' + image + '.png';\n        icon_img.srcset = '_images/' + image + '_large.png 2x';\n        icon_img.alt = tooltip;\n        button.appendChild(icon_img);\n\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    var fmt_picker = document.createElement('select');\n    fmt_picker.classList = 'mpl-widget';\n    toolbar.appendChild(fmt_picker);\n    this.format_dropdown = fmt_picker;\n\n    for (var ind in mpl.extensions) {\n        var fmt = mpl.extensions[ind];\n        var option = document.createElement('option');\n        option.selected = fmt === mpl.default_extension;\n        option.innerHTML = fmt;\n        fmt_picker.appendChild(option);\n    }\n\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n};\n\nmpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n    // which will in turn request a refresh of the image.\n    this.send_message('resize', { width: x_pixels, height: y_pixels });\n};\n\nmpl.figure.prototype.send_message = function (type, properties) {\n    properties['type'] = type;\n    properties['figure_id'] = this.id;\n    this.ws.send(JSON.stringify(properties));\n};\n\nmpl.figure.prototype.send_draw_message = function () {\n    if (!this.waiting) {\n        this.waiting = true;\n        this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    var format_dropdown = fig.format_dropdown;\n    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n    fig.ondownload(fig, format);\n};\n\nmpl.figure.prototype.handle_resize = function (fig, msg) {\n    var size = msg['size'];\n    if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n        fig._resize_canvas(size[0], size[1], msg['forward']);\n        fig.send_message('refresh', {});\n    }\n};\n\nmpl.figure.prototype.handle_rubberband = function (fig, msg) {\n    var x0 = msg['x0'] / fig.ratio;\n    var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n    var x1 = msg['x1'] / fig.ratio;\n    var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n    x0 = Math.floor(x0) + 0.5;\n    y0 = Math.floor(y0) + 0.5;\n    x1 = Math.floor(x1) + 0.5;\n    y1 = Math.floor(y1) + 0.5;\n    var min_x = Math.min(x0, x1);\n    var min_y = Math.min(y0, y1);\n    var width = Math.abs(x1 - x0);\n    var height = Math.abs(y1 - y0);\n\n    fig.rubberband_context.clearRect(\n        0,\n        0,\n        fig.canvas.width / fig.ratio,\n        fig.canvas.height / fig.ratio\n    );\n\n    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n};\n\nmpl.figure.prototype.handle_figure_label = function (fig, msg) {\n    // Updates the figure title.\n    fig.header.textContent = msg['label'];\n};\n\nmpl.figure.prototype.handle_cursor = function (fig, msg) {\n    fig.canvas_div.style.cursor = msg['cursor'];\n};\n\nmpl.figure.prototype.handle_message = function (fig, msg) {\n    fig.message.textContent = msg['message'];\n};\n\nmpl.figure.prototype.handle_draw = function (fig, _msg) {\n    // Request the server to send over a new figure.\n    fig.send_draw_message();\n};\n\nmpl.figure.prototype.handle_image_mode = function (fig, msg) {\n    fig.image_mode = msg['mode'];\n};\n\nmpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n    for (var key in msg) {\n        if (!(key in fig.buttons)) {\n            continue;\n        }\n        fig.buttons[key].disabled = !msg[key];\n        fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n    }\n};\n\nmpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n    if (msg['mode'] === 'PAN') {\n        fig.buttons['Pan'].classList.add('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    } else if (msg['mode'] === 'ZOOM') {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.add('active');\n    } else {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    }\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Called whenever the canvas gets updated.\n    this.send_message('ack', {});\n};\n\n// A function to construct a web socket function for onmessage handling.\n// Called in the figure constructor.\nmpl.figure.prototype._make_on_message_function = function (fig) {\n    return function socket_on_message(evt) {\n        if (evt.data instanceof Blob) {\n            var img = evt.data;\n            if (img.type !== 'image/png') {\n                /* FIXME: We get \"Resource interpreted as Image but\n                 * transferred with MIME type text/plain:\" errors on\n                 * Chrome.  But how to set the MIME type?  It doesn't seem\n                 * to be part of the websocket stream */\n                img.type = 'image/png';\n            }\n\n            /* Free the memory for the previous frames */\n            if (fig.imageObj.src) {\n                (window.URL || window.webkitURL).revokeObjectURL(\n                    fig.imageObj.src\n                );\n            }\n\n            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n                img\n            );\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        } else if (\n            typeof evt.data === 'string' &&\n            evt.data.slice(0, 21) === 'data:image/png;base64'\n        ) {\n            fig.imageObj.src = evt.data;\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        }\n\n        var msg = JSON.parse(evt.data);\n        var msg_type = msg['type'];\n\n        // Call the  \"handle_{type}\" callback, which takes\n        // the figure and JSON message as its only arguments.\n        try {\n            var callback = fig['handle_' + msg_type];\n        } catch (e) {\n            console.log(\n                \"No handler for the '\" + msg_type + \"' message type: \",\n                msg\n            );\n            return;\n        }\n\n        if (callback) {\n            try {\n                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n                callback(fig, msg);\n            } catch (e) {\n                console.log(\n                    \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n                    e,\n                    e.stack,\n                    msg\n                );\n            }\n        }\n    };\n};\n\nfunction getModifiers(event) {\n    var mods = [];\n    if (event.ctrlKey) {\n        mods.push('ctrl');\n    }\n    if (event.altKey) {\n        mods.push('alt');\n    }\n    if (event.shiftKey) {\n        mods.push('shift');\n    }\n    if (event.metaKey) {\n        mods.push('meta');\n    }\n    return mods;\n}\n\n/*\n * return a copy of an object with only non-object keys\n * we need this to avoid circular references\n * https://stackoverflow.com/a/24161582/3208463\n */\nfunction simpleKeys(original) {\n    return Object.keys(original).reduce(function (obj, key) {\n        if (typeof original[key] !== 'object') {\n            obj[key] = original[key];\n        }\n        return obj;\n    }, {});\n}\n\nmpl.figure.prototype.mouse_event = function (event, name) {\n    if (name === 'button_press') {\n        this.canvas.focus();\n        this.canvas_div.focus();\n    }\n\n    // from https://stackoverflow.com/q/1114465\n    var boundingRect = this.canvas.getBoundingClientRect();\n    var x = (event.clientX - boundingRect.left) * this.ratio;\n    var y = (event.clientY - boundingRect.top) * this.ratio;\n\n    this.send_message(name, {\n        x: x,\n        y: y,\n        button: event.button,\n        step: event.step,\n        modifiers: getModifiers(event),\n        guiEvent: simpleKeys(event),\n    });\n\n    return false;\n};\n\nmpl.figure.prototype._key_event_extra = function (_event, _name) {\n    // Handle any extra behaviour associated with a key event\n};\n\nmpl.figure.prototype.key_event = function (event, name) {\n    // Prevent repeat events\n    if (name === 'key_press') {\n        if (event.key === this._key) {\n            return;\n        } else {\n            this._key = event.key;\n        }\n    }\n    if (name === 'key_release') {\n        this._key = null;\n    }\n\n    var value = '';\n    if (event.ctrlKey && event.key !== 'Control') {\n        value += 'ctrl+';\n    }\n    else if (event.altKey && event.key !== 'Alt') {\n        value += 'alt+';\n    }\n    else if (event.shiftKey && event.key !== 'Shift') {\n        value += 'shift+';\n    }\n\n    value += 'k' + event.key;\n\n    this._key_event_extra(event, name);\n\n    this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n    return false;\n};\n\nmpl.figure.prototype.toolbar_button_onclick = function (name) {\n    if (name === 'download') {\n        this.handle_save(this, null);\n    } else {\n        this.send_message('toolbar_button', { name: name });\n    }\n};\n\nmpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n    this.message.textContent = tooltip;\n};\n\n///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n// prettier-ignore\nvar _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\nmpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n\nmpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n\nmpl.default_extension = \"png\";/* global mpl */\n\nvar comm_websocket_adapter = function (comm) {\n    // Create a \"websocket\"-like object which calls the given IPython comm\n    // object with the appropriate methods. Currently this is a non binary\n    // socket, so there is still some room for performance tuning.\n    var ws = {};\n\n    ws.binaryType = comm.kernel.ws.binaryType;\n    ws.readyState = comm.kernel.ws.readyState;\n    function updateReadyState(_event) {\n        if (comm.kernel.ws) {\n            ws.readyState = comm.kernel.ws.readyState;\n        } else {\n            ws.readyState = 3; // Closed state.\n        }\n    }\n    comm.kernel.ws.addEventListener('open', updateReadyState);\n    comm.kernel.ws.addEventListener('close', updateReadyState);\n    comm.kernel.ws.addEventListener('error', updateReadyState);\n\n    ws.close = function () {\n        comm.close();\n    };\n    ws.send = function (m) {\n        //console.log('sending', m);\n        comm.send(m);\n    };\n    // Register the callback with on_msg.\n    comm.on_msg(function (msg) {\n        //console.log('receiving', msg['content']['data'], msg);\n        var data = msg['content']['data'];\n        if (data['blob'] !== undefined) {\n            data = {\n                data: new Blob(msg['buffers'], { type: data['blob'] }),\n            };\n        }\n        // Pass the mpl event to the overridden (by mpl) onmessage function.\n        ws.onmessage(data);\n    });\n    return ws;\n};\n\nmpl.mpl_figure_comm = function (comm, msg) {\n    // This is the function which gets called when the mpl process\n    // starts-up an IPython Comm through the \"matplotlib\" channel.\n\n    var id = msg.content.data.id;\n    // Get hold of the div created by the display call when the Comm\n    // socket was opened in Python.\n    var element = document.getElementById(id);\n    var ws_proxy = comm_websocket_adapter(comm);\n\n    function ondownload(figure, _format) {\n        window.open(figure.canvas.toDataURL());\n    }\n\n    var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n\n    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n    // web socket which is closed, not our websocket->open comm proxy.\n    ws_proxy.onopen();\n\n    fig.parent_element = element;\n    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n    if (!fig.cell_info) {\n        console.error('Failed to find cell for figure', id, fig);\n        return;\n    }\n    fig.cell_info[0].output_area.element.on(\n        'cleared',\n        { fig: fig },\n        fig._remove_fig_handler\n    );\n};\n\nmpl.figure.prototype.handle_close = function (fig, msg) {\n    var width = fig.canvas.width / fig.ratio;\n    fig.cell_info[0].output_area.element.off(\n        'cleared',\n        fig._remove_fig_handler\n    );\n    fig.resizeObserverInstance.unobserve(fig.canvas_div);\n\n    // Update the output cell to use the data from the current canvas.\n    fig.push_to_output();\n    var dataURL = fig.canvas.toDataURL();\n    // Re-enable the keyboard manager in IPython - without this line, in FF,\n    // the notebook keyboard shortcuts fail.\n    IPython.keyboard_manager.enable();\n    fig.parent_element.innerHTML =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n    fig.close_ws(fig, msg);\n};\n\nmpl.figure.prototype.close_ws = function (fig, msg) {\n    fig.send_message('closing', msg);\n    // fig.ws.close()\n};\n\nmpl.figure.prototype.push_to_output = function (_remove_interactive) {\n    // Turn the data on the canvas into data in the output cell.\n    var width = this.canvas.width / this.ratio;\n    var dataURL = this.canvas.toDataURL();\n    this.cell_info[1]['text/html'] =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Tell IPython that the notebook contents must change.\n    IPython.notebook.set_dirty(true);\n    this.send_message('ack', {});\n    var fig = this;\n    // Wait a second, then push the new image to the DOM so\n    // that it is saved nicely (might be nice to debounce this).\n    setTimeout(function () {\n        fig.push_to_output();\n    }, 1000);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'btn-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'btn-group';\n    var button;\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'btn-group';\n            continue;\n        }\n\n        button = fig.buttons[name] = document.createElement('button');\n        button.classList = 'btn btn-default';\n        button.href = '#';\n        button.title = name;\n        button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    // Add the status bar.\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message pull-right';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n\n    // Add the close button to the window.\n    var buttongrp = document.createElement('div');\n    buttongrp.classList = 'btn-group inline pull-right';\n    button = document.createElement('button');\n    button.classList = 'btn btn-mini btn-primary';\n    button.href = '#';\n    button.title = 'Stop Interaction';\n    button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n    button.addEventListener('click', function (_evt) {\n        fig.handle_close(fig, {});\n    });\n    button.addEventListener(\n        'mouseover',\n        on_mouseover_closure('Stop Interaction')\n    );\n    buttongrp.appendChild(button);\n    var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n    titlebar.insertBefore(buttongrp, titlebar.firstChild);\n};\n\nmpl.figure.prototype._remove_fig_handler = function (event) {\n    var fig = event.data.fig;\n    if (event.target !== this) {\n        // Ignore bubbled events from children.\n        return;\n    }\n    fig.close_ws(fig, {});\n};\n\nmpl.figure.prototype._root_extra_style = function (el) {\n    el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n};\n\nmpl.figure.prototype._canvas_extra_style = function (el) {\n    // this is important to make the div 'focusable\n    el.setAttribute('tabindex', 0);\n    // reach out to IPython and tell the keyboard manager to turn it's self\n    // off when our div gets focus\n\n    // location in version 3\n    if (IPython.notebook.keyboard_manager) {\n        IPython.notebook.keyboard_manager.register_events(el);\n    } else {\n        // location in version 2\n        IPython.keyboard_manager.register_events(el);\n    }\n};\n\nmpl.figure.prototype._key_event_extra = function (event, _name) {\n    // Check for shift+enter\n    if (event.shiftKey && event.which === 13) {\n        this.canvas_div.blur();\n        // select the cell after this one\n        var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n        IPython.notebook.select(index + 1);\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    fig.ondownload(fig, null);\n};\n\nmpl.find_output_cell = function (html_output) {\n    // Return the cell and output element which can be found *uniquely* in the notebook.\n    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n    // IPython event is triggered only after the cells have been serialised, which for\n    // our purposes (turning an active figure into a static one), is too late.\n    var cells = IPython.notebook.get_cells();\n    var ncells = cells.length;\n    for (var i = 0; i < ncells; i++) {\n        var cell = cells[i];\n        if (cell.cell_type === 'code') {\n            for (var j = 0; j < cell.output_area.outputs.length; j++) {\n                var data = cell.output_area.outputs[j];\n                if (data.data) {\n                    // IPython >= 3 moved mimebundle to data attribute of output\n                    data = data.data;\n                }\n                if (data['text/html'] === html_output) {\n                    return [cell, data, j];\n                }\n            }\n        }\n    }\n};\n\n// Register the function which deals with the matplotlib target/channel.\n// The kernel may be null if the page has been refreshed.\nif (IPython.notebook.kernel !== null) {\n    IPython.notebook.kernel.comm_manager.register_target(\n        'matplotlib',\n        mpl.mpl_figure_comm\n    );\n}\n"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.HTML object>",
+      "text/html": "<div id='4e94fd79-b720-469f-96cb-9039dc33a852'></div>"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True positive: 2912\n",
+      "False negative: 5180\n",
+      "False positive: 339\n",
+      "True negative: Not defined\n"
+     ]
+    }
+   ],
+   "source": [
+    "conf_mat_cnn = np.array([[tp/n_total, fn/n_total], [fp/n_total, tn/n_total]])\n",
+    "disp = ConfusionMatrixDisplay(conf_mat_cnn)\n",
+    "disp.plot()\n",
+    "plt.grid(False)\n",
+    "plt.show()\n",
+    "\n",
+    "print(f'True positive: {tp}')\n",
+    "print(f'False negative: {fn}')\n",
+    "print(f'False positive: {fp}')\n",
+    "print(f'True negative: Not defined')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.398882Z",
+     "start_time": "2024-04-12T16:23:46.354160Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recall: 0.35986159169550175\n",
+      "Precision: 0.895724392494617\n",
+      "F1-score: 0.513444415057745\n"
+     ]
+    }
+   ],
+   "source": [
+    "precision = tp/(tp+fp)\n",
+    "recall = tp/(tp+fn)\n",
+    "print(f'Recall: {recall}')\n",
+    "print(f'Precision: {precision}')\n",
+    "print(f'F1-score: {2*precision*recall/(precision+recall)}')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:23:46.414223Z",
+     "start_time": "2024-04-12T16:23:46.400883Z"
+    }
+   }
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/scripts/benchmark/benchmark_resNet.ipynb b/scripts/benchmark/benchmark_resNet.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..afa50b8496fce1c9f02710092dd2a691a59d08dd
--- /dev/null
+++ b/scripts/benchmark/benchmark_resNet.ipynb
@@ -0,0 +1,615 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "WARNING:tensorflow:From C:\\Users\\adam-ix592mtcb219gzd\\Anaconda3\\envs\\spc2ml_new\\lib\\site-packages\\keras\\src\\losses.py:2976: The name tf.losses.sparse_softmax_cross_entropy is deprecated. Please use tf.compat.v1.losses.sparse_softmax_cross_entropy instead.\n"
+     ]
+    }
+   ],
+   "source": [
+    "import warnings\n",
+    "\n",
+    "from scripts.benchmark.benchmark_utils import prepare_benchmark_data\n",
+    "\n",
+    "warnings.simplefilter(action='ignore', category=FutureWarning)\n",
+    "\n",
+    "import pandas\n",
+    "import matplotlib.pyplot as plt\n",
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "from tensorflow import keras\n",
+    "\n",
+    "\n",
+    "from G_SPC.spc import ShewartIndividualsChart\n",
+    "from sklearn.metrics import confusion_matrix, recall_score, precision_score, ConfusionMatrixDisplay\n",
+    "\n",
+    "from G_SPC.nn.layer import MeanSubLayer, ResNetConvBlock, ResNetIdentityBlock\n",
+    "from G_SPC.nn.model import rebalanced_nok_mse\n",
+    "\n",
+    "%matplotlib notebook"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T17:13:54.885459Z",
+     "start_time": "2024-04-12T17:13:43.563475Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Benchmark SPC\n",
+    "### Load data"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "outputs": [],
+   "source": [
+    "x= pandas.read_csv(\n",
+    "        \"../../data/test/x.csv\",\n",
+    "        sep=\",\",\n",
+    "        decimal=\".\",\n",
+    "        dtype=float,\n",
+    "        header=None\n",
+    "    )\n",
+    "x = x[0].tolist()\n",
+    "y_true = pandas.read_csv(\n",
+    "        \"../../data/test/y.csv\",\n",
+    "        sep=\",\",\n",
+    "        decimal=\".\",\n",
+    "        dtype=float,\n",
+    "        header=None\n",
+    "    )\n",
+    "y_true = y_true[0].to_list()"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:03.994079Z",
+     "start_time": "2024-04-12T16:34:03.962893Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Perform SPC"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Indices of data-points violating limits: [31, 77, 108, 116, 133, 171, 187, 191, 196, 211, 219, 258, 264, 349, 367, 403, 457, 460, 485, 516, 522, 562, 571, 628, 654, 664, 675, 695, 699, 721, 731, 735, 737, 746, 752, 763, 782, 789, 798, 803, 805, 817, 832, 961, 964, 965, 978, 990, 992, 994, 995, 997, 998, 1019, 1021, 1051, 1059, 1060, 1061, 1066, 1068, 1070, 1084, 1087, 1094, 1097, 1099, 1100, 1107, 1117, 1130, 1140, 1145, 1164, 1174, 1198, 1207, 1209, 1217, 1218, 1224, 1229, 1234, 1245, 1252, 1260, 1261, 1263, 1269, 1272, 1273, 1278, 1283, 1285, 1287, 1291, 1294, 1300, 1302, 1303, 1306, 1307, 1318, 1319, 1323, 1334, 1336, 1337, 1340, 1343, 1344, 1348, 1349, 1352, 1355, 1358, 1365, 1370, 1371, 1372, 1378, 1379, 1381, 1383, 1384, 1387, 1388, 1389, 1390, 1391, 1395, 1396, 1399, 1401, 1402, 1412, 1418, 1421, 1423, 1425, 1428, 1429, 1431, 1441, 1442, 1444, 1445, 1452, 1454, 1459, 1464, 1466, 1470, 1473, 1474, 1476, 1477, 1478, 1480, 1485, 1487, 1492, 1493, 1497, 1498, 1500, 1501, 1504, 1505, 1507, 1508, 1509, 1510, 1511, 1512, 1515, 1516, 1518, 1521, 1522, 1524, 1525, 1526, 1529, 1532, 1535, 1538, 1539, 1541, 1543, 1544, 1545, 1548, 1552, 1553, 1554, 1555, 1557, 1561, 1564, 1565, 1566, 1569, 1570, 1572, 1574, 1575, 1576, 1580, 1581, 1582, 1585, 1586, 1587, 1588, 1589, 1590, 1592, 1593, 1595, 1597, 1598, 1602, 1603, 1604, 1607, 1608, 1614, 1618, 1619, 1622, 1624, 1625, 1631, 1632, 1633, 1635, 1637, 1639, 1641, 1642, 1644, 1649, 1651, 1652, 1653, 1655, 1656, 1657, 1659, 1662, 1664, 1666, 1667, 1668, 1669, 1672, 1674, 1675, 1677, 1678, 1679, 1680, 1682, 1683, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1694, 1695, 1698, 1699, 1700, 1703, 1704, 1705, 1706, 1708, 1709, 1710, 1711, 1712, 1714, 1716, 1717, 1718, 1719, 1720, 1722, 1724, 1726, 1728, 1729, 1730, 1731, 1732, 1734, 1738, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1751, 1753, 1754, 1755, 1756, 1758, 1759, 1761, 1762, 1763, 1764, 1765, 1767, 1769, 1772, 1773, 1774, 1775, 1776, 1777, 1779, 1781, 1782, 1783, 1784, 1785, 1786, 1789, 1790, 1791, 1793, 1794, 1796, 1800, 1801, 1802, 1804, 1805, 1806, 1807, 1808, 1810, 1812, 1813, 1815, 1816, 1817, 1818, 1820, 1821, 1822, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1832, 1833, 1835, 1837, 1838, 1840, 1841, 1842, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1852, 1853, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1868, 1870, 1871, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1903, 1906, 1907, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1918, 1919, 1921, 1922, 1923, 1924, 1925, 1926, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1969, 1971, 1972, 1973, 1974, 1976, 1977, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2001, 2009, 2071, 2117, 2139, 2199, 2215, 2222, 2245, 2250, 2267, 2293, 2317, 2333, 2346, 2356, 2376, 2380, 2392, 2425, 2438, 2445, 2453, 2459, 2465, 2490, 2517, 2522, 2534, 2575, 2601, 2620, 2653, 2669, 2683, 2686, 2688, 2695, 2701, 2729, 2752, 2755, 2761, 2779, 2894, 2914, 2926, 2941, 2956, 2999, 3037, 3055, 3056, 3059, 3065, 3067, 3068, 3087, 3100, 3116, 3124, 3125, 3128, 3137, 3139, 3140, 3146, 3147, 3148, 3150, 3163, 3166, 3172, 3179, 3181, 3189, 3201, 3207, 3213, 3216, 3223, 3224, 3225, 3229, 3235, 3240, 3244, 3249, 3251, 3254, 3259, 3261, 3265, 3267, 3269, 3272, 3274, 3277, 3286, 3288, 3291, 3293, 3294, 3295, 3296, 3300, 3301, 3311, 3313, 3320, 3322, 3325, 3326, 3330, 3331, 3334, 3338, 3339, 3342, 3346, 3347, 3348, 3354, 3358, 3364, 3365, 3366, 3367, 3371, 3372, 3373, 3376, 3380, 3384, 3387, 3388, 3391, 3395, 3397, 3399, 3400, 3407, 3410, 3411, 3412, 3413, 3415, 3418, 3419, 3421, 3426, 3428, 3429, 3434, 3436, 3437, 3438, 3442, 3443, 3447, 3448, 3449, 3451, 3452, 3454, 3455, 3460, 3461, 3466, 3468, 3471, 3472, 3474, 3479, 3480, 3482, 3488, 3489, 3490, 3496, 3497, 3503, 3504, 3505, 3507, 3510, 3512, 3515, 3519, 3521, 3522, 3526, 3533, 3537, 3539, 3540, 3541, 3542, 3544, 3547, 3550, 3552, 3553, 3555, 3561, 3562, 3564, 3566, 3567, 3568, 3569, 3570, 3573, 3578, 3580, 3581, 3582, 3584, 3586, 3587, 3593, 3594, 3596, 3599, 3602, 3606, 3607, 3608, 3609, 3611, 3612, 3615, 3616, 3617, 3619, 3621, 3622, 3625, 3626, 3629, 3632, 3633, 3635, 3637, 3638, 3640, 3643, 3645, 3647, 3649, 3650, 3652, 3653, 3654, 3655, 3656, 3660, 3661, 3663, 3666, 3670, 3672, 3675, 3676, 3677, 3678, 3681, 3682, 3683, 3685, 3688, 3689, 3690, 3691, 3700, 3701, 3702, 3703, 3704, 3706, 3707, 3709, 3711, 3713, 3714, 3717, 3718, 3720, 3721, 3722, 3723, 3724, 3726, 3728, 3730, 3732, 3733, 3735, 3736, 3737, 3738, 3739, 3742, 3744, 3745, 3746, 3748, 3749, 3750, 3751, 3752, 3755, 3758, 3759, 3760, 3761, 3763, 3764, 3766, 3767, 3768, 3770, 3771, 3773, 3775, 3776, 3778, 3779, 3780, 3781, 3783, 3784, 3785, 3789, 3791, 3792, 3793, 3795, 3796, 3797, 3798, 3799, 3800, 3801, 3807, 3808, 3810, 3811, 3812, 3813, 3815, 3816, 3819, 3820, 3821, 3822, 3823, 3824, 3825, 3826, 3828, 3829, 3830, 3832, 3833, 3835, 3836, 3837, 3838, 3840, 3843, 3844, 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3861, 3863, 3865, 3866, 3868, 3869, 3870, 3871, 3874, 3875, 3876, 3878, 3882, 3883, 3884, 3885, 3886, 3887, 3888, 3889, 3890, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3909, 3910, 3911, 3912, 3913, 3914, 3915, 3917, 3918, 3919, 3920, 3921, 3922, 3923, 3924, 3925, 3926, 3927, 3928, 3929, 3930, 3931, 3932, 3933, 3934, 3935, 3936, 3937, 3938, 3940, 3941, 3942, 3943, 3944, 3945, 3946, 3947, 3948, 3949, 3950, 3951, 3953, 3954, 3955, 3956, 3957, 3959, 3960, 3962, 3963, 3964, 3965, 3966, 3967, 3968, 3969, 3970, 3971, 3973, 3974, 3976, 3979, 3980, 3982, 3983, 3984, 3985, 3986, 3987, 3988, 3989, 3990, 3992, 3993, 3994, 3995, 3996, 3997, 3998, 3999, 4007, 4013, 4031, 4036, 4054, 4064, 4074, 4123, 4124, 4125, 4127, 4170, 4186, 4187, 4219, 4247, 4269, 4279, 4337, 4342, 4357, 4360, 4374, 4403, 4410, 4480, 4488, 4504, 4509, 4527, 4540, 4541, 4549, 4560, 4626, 4695, 4707, 4736, 4745, 4772, 4802, 4808, 4819, 4854, 4910, 4931, 4937, 4979, 4994, 4995, 4996, 4997, 4998, 4999, 5025, 5028, 5029, 5079, 5083, 5092, 5095, 5102, 5103, 5104, 5108, 5113, 5128, 5140, 5141, 5144, 5145, 5148, 5156, 5157, 5161, 5163, 5165, 5166, 5167, 5175, 5186, 5201, 5205, 5209, 5210, 5220, 5223, 5231, 5235, 5241, 5242, 5245, 5255, 5256, 5257, 5266, 5271, 5274, 5293, 5301, 5304, 5306, 5308, 5309, 5311, 5313, 5315, 5316, 5324, 5325, 5330, 5335, 5339, 5342, 5345, 5347, 5363, 5365, 5381, 5383, 5384, 5386, 5391, 5392, 5396, 5400, 5401, 5404, 5409, 5411, 5413, 5414, 5418, 5419, 5420, 5423, 5427, 5433, 5434, 5435, 5436, 5437, 5438, 5440, 5441, 5444, 5447, 5450, 5453, 5456, 5461, 5462, 5465, 5468, 5474, 5475, 5480, 5482, 5484, 5485, 5488, 5490, 5495, 5496, 5501, 5502, 5504, 5507, 5508, 5511, 5514, 5518, 5522, 5524, 5528, 5529, 5531, 5533, 5535, 5539, 5543, 5547, 5548, 5550, 5551, 5554, 5555, 5556, 5559, 5564, 5566, 5567, 5569, 5572, 5575, 5577, 5579, 5581, 5582, 5584, 5587, 5588, 5589, 5590, 5591, 5593, 5594, 5595, 5597, 5598, 5599, 5602, 5607, 5608, 5609, 5610, 5612, 5615, 5619, 5620, 5624, 5626, 5632, 5633, 5635, 5637, 5639, 5643, 5644, 5645, 5647, 5648, 5649, 5650, 5651, 5653, 5655, 5656, 5660, 5662, 5663, 5664, 5665, 5667, 5669, 5670, 5671, 5674, 5675, 5677, 5678, 5680, 5685, 5686, 5687, 5691, 5692, 5693, 5695, 5696, 5697, 5699, 5700, 5703, 5704, 5706, 5707, 5709, 5710, 5712, 5713, 5714, 5715, 5717, 5718, 5720, 5722, 5724, 5726, 5728, 5733, 5735, 5736, 5737, 5741, 5742, 5743, 5744, 5745, 5747, 5748, 5749, 5751, 5753, 5757, 5758, 5760, 5763, 5765, 5768, 5770, 5771, 5772, 5773, 5774, 5775, 5776, 5777, 5779, 5780, 5783, 5785, 5786, 5787, 5788, 5789, 5790, 5791, 5792, 5793, 5795, 5796, 5798, 5799, 5800, 5801, 5802, 5803, 5804, 5807, 5809, 5813, 5814, 5816, 5817, 5818, 5821, 5822, 5823, 5825, 5827, 5830, 5831, 5832, 5833, 5834, 5836, 5838, 5839, 5840, 5841, 5843, 5844, 5845, 5846, 5847, 5848, 5852, 5855, 5857, 5858, 5859, 5861, 5862, 5863, 5864, 5865, 5866, 5868, 5870, 5871, 5874, 5875, 5877, 5878, 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, 5887, 5889, 5890, 5891, 5893, 5894, 5895, 5896, 5897, 5898, 5899, 5900, 5902, 5904, 5906, 5908, 5909, 5910, 5912, 5913, 5914, 5915, 5917, 5918, 5919, 5920, 5921, 5924, 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, 5933, 5935, 5936, 5937, 5938, 5939, 5941, 5943, 5946, 5948, 5949, 5951, 5953, 5954, 5955, 5956, 5957, 5958, 5959, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, 5970, 5971, 5972, 5974, 5975, 5976, 5979, 5980, 5982, 5983, 5984, 5985, 5987, 5988, 5990, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999, 6023, 6048, 6099, 6118, 6130, 6163, 6165, 6175, 6197, 6199, 6225, 6228, 6255, 6262, 6278, 6315, 6334, 6342, 6415, 6461, 6483, 6490, 6498, 6530, 6532, 6535, 6538, 6557, 6579, 6601, 6615, 6635, 6651, 6757, 6766, 6775, 6789, 6793, 6807, 6826, 6847, 6850, 6867, 6911, 6963, 6969, 6980, 6985, 6987, 6988, 6992, 6993, 6995, 6996, 6999, 7007, 7014, 7030, 7033, 7057, 7065, 7068, 7092, 7098, 7100, 7117, 7120, 7143, 7145, 7146, 7169, 7179, 7180, 7185, 7192, 7194, 7202, 7205, 7210, 7213, 7216, 7219, 7224, 7227, 7238, 7240, 7243, 7247, 7248, 7250, 7251, 7252, 7254, 7256, 7265, 7267, 7277, 7280, 7286, 7290, 7298, 7300, 7305, 7306, 7314, 7316, 7323, 7324, 7327, 7329, 7333, 7334, 7337, 7340, 7341, 7343, 7350, 7357, 7358, 7362, 7363, 7374, 7375, 7376, 7380, 7384, 7388, 7392, 7393, 7395, 7398, 7404, 7406, 7410, 7413, 7415, 7416, 7417, 7418, 7424, 7426, 7427, 7429, 7432, 7434, 7435, 7436, 7439, 7441, 7444, 7445, 7446, 7447, 7450, 7459, 7460, 7461, 7466, 7467, 7468, 7471, 7476, 7477, 7480, 7482, 7485, 7486, 7487, 7489, 7497, 7501, 7502, 7505, 7506, 7508, 7509, 7511, 7512, 7516, 7519, 7522, 7523, 7525, 7527, 7532, 7534, 7536, 7541, 7542, 7544, 7550, 7551, 7552, 7556, 7557, 7561, 7563, 7565, 7567, 7568, 7573, 7576, 7578, 7581, 7584, 7585, 7587, 7589, 7590, 7593, 7594, 7595, 7596, 7597, 7598, 7599, 7601, 7602, 7603, 7605, 7608, 7611, 7612, 7613, 7619, 7621, 7623, 7628, 7629, 7630, 7631, 7632, 7634, 7639, 7640, 7641, 7642, 7643, 7645, 7646, 7648, 7649, 7650, 7651, 7653, 7654, 7656, 7657, 7660, 7665, 7674, 7675, 7676, 7677, 7679, 7683, 7686, 7689, 7691, 7693, 7694, 7696, 7698, 7701, 7702, 7703, 7704, 7705, 7706, 7708, 7709, 7710, 7712, 7714, 7717, 7720, 7722, 7723, 7724, 7725, 7726, 7727, 7728, 7730, 7731, 7733, 7736, 7738, 7739, 7740, 7744, 7747, 7749, 7751, 7752, 7753, 7755, 7756, 7758, 7760, 7763, 7764, 7765, 7766, 7768, 7769, 7770, 7772, 7777, 7778, 7779, 7780, 7781, 7783, 7784, 7785, 7786, 7787, 7789, 7792, 7794, 7795, 7796, 7798, 7800, 7801, 7802, 7803, 7806, 7807, 7808, 7809, 7810, 7811, 7812, 7813, 7815, 7816, 7817, 7818, 7820, 7821, 7822, 7823, 7824, 7825, 7826, 7829, 7831, 7832, 7833, 7834, 7835, 7836, 7837, 7838, 7839, 7840, 7841, 7842, 7843, 7844, 7845, 7846, 7847, 7849, 7850, 7851, 7852, 7853, 7860, 7861, 7863, 7866, 7867, 7868, 7869, 7870, 7872, 7873, 7874, 7875, 7877, 7880, 7882, 7883, 7884, 7885, 7886, 7888, 7889, 7891, 7892, 7893, 7894, 7895, 7896, 7897, 7900, 7901, 7902, 7903, 7904, 7905, 7906, 7907, 7908, 7909, 7912, 7913, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7925, 7927, 7928, 7929, 7930, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7943, 7944, 7945, 7946, 7947, 7948, 7951, 7952, 7953, 7955, 7956, 7957, 7958, 7959, 7960, 7961, 7962, 7964, 7966, 7967, 7970, 7971, 7972, 7974, 7975, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7986, 7987, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8015, 8030, 8036, 8038, 8105, 8119, 8124, 8149, 8206, 8266, 8277, 8278, 8304, 8330, 8333, 8338, 8376, 8426, 8430, 8455, 8462, 8475, 8508, 8521, 8533, 8556, 8566, 8587, 8594, 8598, 8605, 8616, 8630, 8665, 8667, 8682, 8688, 8702, 8727, 8738, 8743, 8754, 8764, 8766, 8801, 8808, 8823, 8855, 8878, 8885, 8912, 8920, 8923, 8954, 8985, 8987, 8988, 8994, 8995, 8996, 8997, 8998, 8999, 9017, 9037, 9043, 9056, 9060, 9066, 9083, 9092, 9118, 9124, 9130, 9146, 9148, 9153, 9158, 9164, 9166, 9173, 9176, 9181, 9184, 9185, 9189, 9191, 9192, 9202, 9213, 9217, 9219, 9220, 9222, 9224, 9227, 9229, 9234, 9269, 9278, 9283, 9284, 9299, 9304, 9305, 9306, 9307, 9311, 9314, 9315, 9319, 9323, 9326, 9332, 9341, 9342, 9348, 9349, 9354, 9357, 9359, 9360, 9365, 9366, 9373, 9375, 9376, 9386, 9388, 9391, 9396, 9398, 9402, 9403, 9404, 9406, 9409, 9410, 9411, 9415, 9419, 9420, 9421, 9425, 9433, 9439, 9440, 9441, 9461, 9465, 9467, 9472, 9473, 9474, 9481, 9488, 9495, 9496, 9500, 9502, 9505, 9522, 9524, 9525, 9526, 9528, 9529, 9531, 9535, 9537, 9540, 9542, 9543, 9544, 9547, 9548, 9549, 9551, 9552, 9553, 9556, 9558, 9559, 9562, 9571, 9573, 9577, 9578, 9580, 9586, 9587, 9590, 9592, 9594, 9598, 9599, 9605, 9609, 9613, 9617, 9620, 9624, 9627, 9630, 9632, 9637, 9640, 9643, 9645, 9651, 9656, 9658, 9659, 9660, 9662, 9663, 9667, 9668, 9671, 9675, 9676, 9677, 9684, 9685, 9686, 9687, 9689, 9690, 9691, 9693, 9696, 9697, 9698, 9701, 9702, 9703, 9713, 9714, 9722, 9726, 9731, 9735, 9739, 9742, 9744, 9745, 9747, 9753, 9754, 9757, 9761, 9763, 9764, 9767, 9768, 9771, 9772, 9773, 9777, 9779, 9780, 9785, 9786, 9791, 9792, 9794, 9795, 9798, 9803, 9805, 9806, 9810, 9812, 9813, 9816, 9817, 9819, 9821, 9822, 9825, 9828, 9830, 9831, 9832, 9835, 9841, 9845, 9846, 9847, 9848, 9849, 9850, 9851, 9852, 9853, 9854, 9856, 9857, 9859, 9860, 9862, 9868, 9869, 9870, 9873, 9874, 9876, 9877, 9879, 9880, 9881, 9882, 9883, 9889, 9891, 9894, 9897, 9898, 9899, 9900, 9901, 9902, 9903, 9905, 9906, 9907, 9911, 9913, 9914, 9917, 9922, 9923, 9927, 9929, 9930, 9931, 9933, 9935, 9937, 9939, 9940, 9943, 9944, 9948, 9949, 9950, 9951, 9952, 9956, 9958, 9959, 9963, 9966, 9969, 9973, 9975, 9976, 9978, 9979, 9980, 9985, 9987, 9989, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9998, 9999, 10001, 10008, 10031, 10069, 10087, 10125, 10153, 10202, 10206, 10211, 10237, 10295, 10297, 10317, 10325, 10329, 10344, 10348, 10350, 10358, 10359, 10365, 10405, 10413, 10422, 10434, 10462, 10472, 10508, 10555, 10572, 10580, 10589, 10603, 10605, 10614, 10625, 10627, 10637, 10647, 10672, 10691, 10706, 10722, 10737, 10757, 10760, 10769, 10786, 10809, 10819, 10840, 10852, 10879, 10896, 10907, 10919, 10947, 10954, 10969, 10981, 10983, 10984, 10986, 10987, 10988, 10990, 10992, 10998, 10999, 11004, 11035, 11058, 11071, 11093, 11100, 11103, 11122, 11125, 11126, 11134, 11136, 11155, 11175, 11176, 11183, 11196, 11206, 11218, 11219, 11230, 11233, 11238, 11247, 11249, 11260, 11261, 11262, 11265, 11280, 11286, 11292, 11293, 11294, 11302, 11305, 11316, 11322, 11323, 11325, 11327, 11333, 11334, 11335, 11337, 11341, 11345, 11346, 11348, 11355, 11361, 11369, 11370, 11371, 11379, 11389, 11390, 11395, 11396, 11397, 11399, 11400, 11402, 11403, 11405, 11406, 11407, 11409, 11410, 11413, 11415, 11416, 11421, 11422, 11423, 11426, 11427, 11428, 11432, 11443, 11446, 11448, 11450, 11451, 11458, 11463, 11467, 11469, 11470, 11472, 11475, 11478, 11479, 11484, 11485, 11487, 11498, 11500, 11502, 11503, 11506, 11507, 11510, 11511, 11513, 11518, 11520, 11524, 11526, 11527, 11529, 11537, 11540, 11543, 11547, 11548, 11552, 11555, 11556, 11560, 11562, 11563, 11566, 11568, 11570, 11572, 11574, 11578, 11579, 11580, 11584, 11585, 11590, 11595, 11596, 11597, 11605, 11609, 11611, 11613, 11614, 11615, 11620, 11621, 11622, 11628, 11629, 11630, 11633, 11634, 11635, 11637, 11642, 11643, 11646, 11649, 11650, 11651, 11653, 11654, 11659, 11660, 11661, 11662, 11666, 11667, 11674, 11676, 11678, 11679, 11681, 11683, 11684, 11685, 11686, 11689, 11690, 11692, 11694, 11695, 11697, 11700, 11701, 11702, 11703, 11705, 11706, 11707, 11709, 11712, 11715, 11716, 11718, 11721, 11723, 11724, 11726, 11727, 11729, 11732, 11733, 11735, 11737, 11738, 11739, 11740, 11743, 11746, 11749, 11751, 11752, 11754, 11755, 11756, 11757, 11758, 11759, 11760, 11761, 11762, 11763, 11764, 11765, 11767, 11768, 11769, 11770, 11771, 11772, 11775, 11776, 11779, 11780, 11782, 11783, 11784, 11785, 11788, 11789, 11791, 11792, 11793, 11794, 11799, 11801, 11802, 11803, 11807, 11808, 11811, 11813, 11814, 11816, 11817, 11820, 11821, 11822, 11823, 11824, 11825, 11826, 11830, 11831, 11832, 11834, 11835, 11836, 11837, 11838, 11839, 11841, 11842, 11844, 11845, 11848, 11849, 11851, 11852, 11853, 11854, 11855, 11859, 11860, 11861, 11862, 11863, 11865, 11866, 11867, 11871, 11872, 11873, 11874, 11875, 11877, 11878, 11880, 11882, 11883, 11888, 11889, 11890, 11891, 11892, 11894, 11895, 11896, 11897, 11898, 11900, 11901, 11902, 11903, 11904, 11905, 11906, 11907, 11908, 11910, 11912, 11913, 11915, 11916, 11917, 11919, 11920, 11922, 11925, 11926, 11930, 11931, 11932, 11933, 11935, 11936, 11937, 11940, 11942, 11943, 11944, 11946, 11947, 11948, 11949, 11950, 11952, 11955, 11956, 11957, 11959, 11960, 11961, 11962, 11963, 11964, 11967, 11969, 11970, 11971, 11972, 11976, 11977, 11978, 11980, 11982, 11983, 11984, 11985, 11986, 11987, 11988, 11989, 11991, 11992, 11993, 11994, 11995, 11998, 12016, 12027, 12030, 12036, 12090, 12115, 12129, 12186, 12188, 12192, 12348, 12359, 12367, 12381, 12388, 12393, 12431, 12445, 12447, 12454, 12460, 12461, 12481, 12483, 12485, 12487, 12494, 12502, 12504, 12520, 12533, 12534, 12547, 12639, 12665, 12666, 12680, 12695, 12704, 12751, 12755, 12813, 12818, 12825, 12835, 12837, 12853, 12908, 12963, 12968, 12987, 12990, 12991, 12992, 12993, 12996, 12997, 12998, 12999, 13024, 13046, 13056, 13075, 13079, 13084, 13102, 13103, 13107, 13116, 13119, 13120, 13123, 13124, 13126, 13128, 13129, 13131, 13142, 13149, 13151, 13152, 13154, 13156, 13158, 13162, 13167, 13184, 13194, 13195, 13196, 13199, 13202, 13204, 13205, 13211, 13215, 13217, 13220, 13227, 13237, 13242, 13244, 13248, 13252, 13255, 13257, 13260, 13261, 13265, 13266, 13268, 13269, 13280, 13283, 13285, 13300, 13304, 13308, 13309, 13317, 13319, 13320, 13321, 13322, 13324, 13325, 13328, 13329, 13336, 13338, 13340, 13341, 13342, 13344, 13345, 13350, 13352, 13355, 13357, 13359, 13360, 13361, 13366, 13370, 13372, 13375, 13376, 13378, 13381, 13385, 13387, 13391, 13393, 13397, 13403, 13405, 13406, 13407, 13408, 13412, 13414, 13415, 13417, 13418, 13423, 13424, 13425, 13426, 13427, 13429, 13434, 13436, 13438, 13440, 13442, 13447, 13450, 13451, 13456, 13457, 13463, 13468, 13473, 13474, 13477, 13478, 13480, 13482, 13483, 13484, 13485, 13492, 13493, 13495, 13496, 13499, 13500, 13503, 13504, 13505, 13506, 13507, 13509, 13511, 13513, 13514, 13515, 13516, 13517, 13518, 13519, 13521, 13524, 13527, 13530, 13533, 13535, 13537, 13539, 13540, 13542, 13544, 13545, 13546, 13548, 13551, 13552, 13554, 13555, 13557, 13559, 13561, 13564, 13567, 13570, 13572, 13573, 13576, 13578, 13579, 13580, 13581, 13582, 13583, 13584, 13585, 13587, 13588, 13589, 13592, 13594, 13595, 13596, 13597, 13599, 13601, 13603, 13605, 13606, 13611, 13613, 13614, 13617, 13622, 13623, 13626, 13627, 13629, 13630, 13632, 13634, 13636, 13637, 13641, 13643, 13645, 13646, 13647, 13648, 13649, 13650, 13651, 13652, 13653, 13655, 13656, 13657, 13659, 13660, 13661, 13663, 13664, 13668, 13670, 13671, 13672, 13673, 13678, 13679, 13680, 13683, 13685, 13687, 13689, 13690, 13691, 13692, 13693, 13694, 13695, 13696, 13698, 13699, 13701, 13702, 13703, 13705, 13706, 13707, 13708, 13710, 13711, 13712, 13713, 13715, 13720, 13721, 13723, 13725, 13728, 13729, 13730, 13731, 13733, 13734, 13736, 13737, 13738, 13740, 13741, 13743, 13744, 13745, 13746, 13747, 13748, 13750, 13751, 13754, 13755, 13756, 13757, 13758, 13759, 13760, 13761, 13762, 13763, 13765, 13766, 13768, 13769, 13771, 13772, 13773, 13775, 13777, 13778, 13779, 13781, 13783, 13784, 13785, 13788, 13789, 13790, 13792, 13793, 13794, 13795, 13796, 13797, 13798, 13800, 13801, 13803, 13804, 13805, 13807, 13809, 13810, 13812, 13813, 13814, 13815, 13816, 13817, 13820, 13821, 13822, 13823, 13824, 13829, 13830, 13831, 13832, 13833, 13834, 13835, 13838, 13839, 13840, 13841, 13842, 13844, 13845, 13846, 13847, 13848, 13849, 13850, 13851, 13852, 13855, 13856, 13857, 13859, 13860, 13862, 13863, 13865, 13866, 13867, 13868, 13869, 13870, 13872, 13874, 13875, 13876, 13877, 13878, 13879, 13880, 13881, 13882, 13883, 13884, 13885, 13886, 13887, 13888, 13889, 13890, 13893, 13894, 13895, 13896, 13897, 13898, 13899, 13901, 13903, 13904, 13905, 13906, 13907, 13908, 13909, 13910, 13912, 13914, 13915, 13916, 13917, 13918, 13919, 13920, 13921, 13923, 13924, 13925, 13926, 13927, 13929, 13931, 13932, 13933, 13934, 13935, 13936, 13937, 13938, 13940, 13941, 13942, 13943, 13944, 13945, 13947, 13948, 13949, 13950, 13952, 13954, 13955, 13956, 13957, 13958, 13959, 13960, 13961, 13962, 13963, 13964, 13965, 13966, 13967, 13968, 13969, 13971, 13973, 13974, 13975, 13976, 13979, 13980, 13981, 13982, 13983, 13984, 13985, 13987, 13988, 13989, 13990, 13991, 13992, 13993, 13994, 13997, 13998, 13999, 14001, 14017, 14030, 14032, 14039, 14057, 14059, 14102, 14119, 14128, 14164, 14166, 14174, 14185, 14210, 14220, 14221, 14237, 14288, 14314, 14327, 14330, 14343, 14346, 14381, 14455, 14467, 14469, 14471, 14539, 14550, 14584, 14611, 14616, 14624, 14637, 14662, 14666, 14702, 14707, 14737, 14795, 14800, 14803, 14806, 14820, 14882, 14905, 14931, 14940, 14958, 14990, 14992, 14993, 14998, 14999, 15042, 15057, 15066, 15072, 15077, 15099, 15103, 15105, 15106, 15111, 15116, 15117, 15119, 15134, 15135, 15143, 15163, 15167, 15170, 15172, 15175, 15187, 15212, 15213, 15217, 15219, 15226, 15228, 15229, 15232, 15235, 15238, 15244, 15249, 15251, 15262, 15264, 15276, 15281, 15286, 15291, 15293, 15305, 15306, 15307, 15319, 15322, 15323, 15325, 15331, 15337, 15343, 15344, 15347, 15348, 15350, 15352, 15353, 15354, 15355, 15356, 15361, 15365, 15367, 15368, 15370, 15371, 15375, 15376, 15377, 15380, 15384, 15386, 15387, 15389, 15396, 15397, 15400, 15401, 15405, 15407, 15410, 15412, 15418, 15419, 15428, 15429, 15436, 15438, 15442, 15451, 15453, 15455, 15459, 15460, 15461, 15462, 15463, 15473, 15478, 15481, 15483, 15484, 15489, 15495, 15496, 15500, 15502, 15503, 15505, 15507, 15510, 15514, 15515, 15518, 15520, 15521, 15523, 15524, 15531, 15532, 15534, 15535, 15538, 15539, 15540, 15541, 15544, 15545, 15546, 15547, 15550, 15551, 15552, 15554, 15555, 15560, 15561, 15562, 15564, 15566, 15568, 15571, 15573, 15574, 15579, 15580, 15581, 15585, 15586, 15588, 15591, 15592, 15593, 15595, 15596, 15599, 15602, 15606, 15608, 15609, 15613, 15614, 15616, 15617, 15619, 15620, 15622, 15623, 15625, 15628, 15632, 15633, 15634, 15635, 15642, 15643, 15644, 15647, 15648, 15650, 15653, 15654, 15656, 15657, 15658, 15659, 15660, 15661, 15662, 15664, 15667, 15668, 15670, 15671, 15672, 15674, 15676, 15678, 15680, 15681, 15682, 15684, 15685, 15686, 15688, 15690, 15692, 15693, 15695, 15698, 15702, 15703, 15704, 15705, 15706, 15707, 15712, 15713, 15715, 15716, 15719, 15720, 15721, 15722, 15723, 15725, 15726, 15728, 15730, 15733, 15734, 15735, 15736, 15738, 15740, 15741, 15743, 15744, 15745, 15746, 15747, 15748, 15749, 15750, 15751, 15752, 15753, 15754, 15756, 15757, 15759, 15763, 15764, 15767, 15769, 15770, 15772, 15773, 15774, 15776, 15777, 15778, 15779, 15780, 15781, 15782, 15783, 15784, 15785, 15786, 15788, 15789, 15790, 15792, 15793, 15794, 15796, 15797, 15798, 15799, 15800, 15803, 15805, 15806, 15807, 15808, 15809, 15811, 15812, 15814, 15815, 15816, 15817, 15818, 15820, 15821, 15823, 15827, 15828, 15829, 15831, 15834, 15836, 15837, 15838, 15839, 15840, 15842, 15844, 15847, 15848, 15849, 15850, 15851, 15852, 15853, 15854, 15855, 15857, 15858, 15859, 15861, 15862, 15864, 15865, 15866, 15867, 15868, 15869, 15872, 15874, 15875, 15876, 15877, 15878, 15879, 15880, 15881, 15883, 15885, 15886, 15887, 15888, 15889, 15890, 15892, 15893, 15895, 15897, 15898, 15899, 15900, 15901, 15903, 15907, 15910, 15911, 15913, 15914, 15915, 15916, 15917, 15918, 15919, 15920, 15921, 15923, 15924, 15925, 15926, 15928, 15929, 15931, 15932, 15933, 15934, 15937, 15939, 15940, 15941, 15942, 15944, 15945, 15947, 15950, 15952, 15953, 15954, 15955, 15956, 15958, 15959, 15961, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15974, 15975, 15976, 15978, 15980, 15982, 15984, 15985, 15986, 15987, 15988, 15989, 15990, 15991, 15992, 15993, 15994, 15995, 15996, 15997, 15998, 16010, 16019, 16022, 16046, 16076, 16106, 16111, 16119, 16120, 16134, 16155, 16157, 16201, 16205, 16220, 16222, 16258, 16260, 16275, 16284, 16287, 16305, 16307, 16313, 16342, 16368, 16371, 16447, 16454, 16493, 16513, 16524, 16575, 16583, 16585, 16597, 16622, 16646, 16675, 16682, 16748, 16767, 16831, 16832, 16838, 16841, 16880, 16885, 16891, 16895, 16906, 16945, 16952, 16965, 16970, 16975, 16978, 16995, 16996, 16997, 17005, 17025, 17026, 17030, 17040, 17050, 17077, 17081, 17086, 17110, 17122, 17127, 17137, 17157, 17159, 17160, 17169, 17174, 17188, 17195, 17196, 17197, 17202, 17213, 17214, 17217, 17218, 17220, 17231, 17232, 17245, 17246, 17249, 17257, 17261, 17262, 17271, 17272, 17273, 17274, 17277, 17281, 17284, 17286, 17288, 17292, 17297, 17310, 17312, 17315, 17321, 17322, 17325, 17327, 17330, 17344, 17345, 17347, 17354, 17355, 17361, 17369, 17373, 17375, 17376, 17378, 17389, 17390, 17393, 17395, 17397, 17401, 17402, 17403, 17411, 17414, 17420, 17422, 17423, 17429, 17440, 17443, 17445, 17448, 17449, 17451, 17452, 17456, 17457, 17459, 17465, 17467, 17469, 17470, 17473, 17475, 17476, 17478, 17480, 17482, 17484, 17488, 17490, 17492, 17493, 17494, 17495, 17496, 17498, 17502, 17504, 17505, 17511, 17515, 17517, 17521, 17526, 17529, 17530, 17532, 17536, 17537, 17539, 17543, 17544, 17548, 17554, 17555, 17556, 17558, 17559, 17561, 17562, 17564, 17567, 17568, 17569, 17572, 17574, 17575, 17579, 17580, 17582, 17584, 17585, 17586, 17587, 17588, 17589, 17599, 17602, 17604, 17605, 17606, 17607, 17609, 17611, 17612, 17613, 17614, 17620, 17622, 17624, 17625, 17626, 17628, 17629, 17630, 17631, 17633, 17634, 17636, 17637, 17638, 17643, 17644, 17646, 17647, 17649, 17650, 17651, 17652, 17653, 17655, 17656, 17657, 17658, 17659, 17662, 17668, 17669, 17670, 17672, 17676, 17678, 17679, 17680, 17681, 17684, 17685, 17686, 17687, 17688, 17689, 17691, 17692, 17695, 17697, 17698, 17700, 17701, 17702, 17703, 17704, 17705, 17709, 17712, 17713, 17716, 17719, 17720, 17722, 17723, 17726, 17727, 17729, 17731, 17732, 17733, 17734, 17737, 17738, 17740, 17741, 17742, 17743, 17744, 17746, 17751, 17753, 17754, 17755, 17756, 17757, 17758, 17759, 17760, 17761, 17762, 17763, 17769, 17772, 17773, 17774, 17775, 17777, 17779, 17780, 17781, 17782, 17783, 17784, 17786, 17787, 17788, 17790, 17791, 17792, 17794, 17797, 17798, 17801, 17802, 17804, 17808, 17810, 17812, 17815, 17816, 17817, 17819, 17821, 17822, 17823, 17824, 17826, 17827, 17830, 17832, 17833, 17834, 17838, 17840, 17841, 17844, 17846, 17847, 17849, 17850, 17851, 17852, 17853, 17854, 17856, 17858, 17859, 17860, 17862, 17864, 17865, 17866, 17867, 17868, 17869, 17870, 17871, 17872, 17873, 17874, 17875, 17877, 17879, 17880, 17882, 17884, 17885, 17886, 17888, 17889, 17891, 17892, 17894, 17897, 17898, 17900, 17901, 17902, 17903, 17904, 17905, 17906, 17908, 17909, 17910, 17911, 17912, 17914, 17917, 17919, 17920, 17922, 17923, 17924, 17926, 17928, 17929, 17931, 17933, 17934, 17936, 17937, 17938, 17939, 17940, 17941, 17943, 17944, 17945, 17946, 17947, 17948, 17949, 17955, 17956, 17957, 17959, 17960, 17962, 17963, 17964, 17965, 17966, 17969, 17971, 17972, 17973, 17974, 17975, 17976, 17978, 17979, 17981, 17983, 17984, 17985, 17988, 17989, 17990, 17991, 17993, 17994, 17995, 17996, 17997, 17998, 18006, 18009, 18036, 18079, 18121, 18192, 18251, 18255, 18293, 18308, 18316, 18368, 18370, 18377, 18390, 18427, 18433, 18444, 18454, 18496, 18556, 18560, 18563, 18576, 18633, 18634, 18639, 18647, 18656, 18658, 18663, 18672, 18674, 18716, 18752, 18753, 18767, 18884, 18918, 18921, 18944, 18956, 18971, 18974, 18980, 18993, 18996, 18997, 18999, 19001, 19026, 19042, 19058, 19063, 19067, 19074, 19076, 19096, 19121, 19128, 19134, 19137, 19147, 19164, 19168, 19170, 19178, 19180, 19182, 19190, 19198, 19199, 19204, 19206, 19209, 19218, 19221, 19225, 19228, 19233, 19236, 19237, 19248, 19250, 19267, 19276, 19277, 19285, 19291, 19294, 19298, 19306, 19314, 19320, 19322, 19326, 19328, 19331, 19338, 19339, 19340, 19347, 19351, 19353, 19360, 19365, 19367, 19368, 19371, 19374, 19378, 19379, 19382, 19384, 19390, 19392, 19394, 19396, 19398, 19401, 19403, 19407, 19411, 19414, 19415, 19422, 19423, 19429, 19432, 19435, 19438, 19441, 19442, 19444, 19448, 19449, 19450, 19456, 19458, 19462, 19464, 19465, 19469, 19470, 19472, 19474, 19476, 19483, 19484, 19485, 19488, 19489, 19491, 19494, 19501, 19504, 19516, 19519, 19520, 19523, 19524, 19526, 19527, 19528, 19530, 19531, 19537, 19538, 19541, 19551, 19555, 19556, 19558, 19562, 19564, 19566, 19567, 19568, 19571, 19572, 19575, 19577, 19578, 19580, 19582, 19583, 19584, 19591, 19598, 19602, 19604, 19606, 19610, 19613, 19614, 19615, 19618, 19623, 19624, 19625, 19626, 19630, 19631, 19633, 19639, 19640, 19641, 19643, 19644, 19646, 19648, 19652, 19655, 19657, 19658, 19662, 19663, 19665, 19667, 19668, 19677, 19678, 19679, 19682, 19683, 19684, 19685, 19689, 19694, 19695, 19699, 19701, 19703, 19704, 19708, 19710, 19711, 19712, 19714, 19720, 19722, 19724, 19726, 19727, 19728, 19729, 19731, 19732, 19736, 19737, 19738, 19743, 19747, 19748, 19749, 19750, 19751, 19752, 19754, 19757, 19760, 19761, 19762, 19763, 19765, 19766, 19767, 19768, 19771, 19775, 19776, 19777, 19778, 19780, 19781, 19783, 19784, 19786, 19788, 19789, 19792, 19793, 19795, 19798, 19801, 19802, 19804, 19805, 19808, 19812, 19814, 19815, 19817, 19818, 19819, 19822, 19824, 19828, 19829, 19831, 19832, 19833, 19834, 19835, 19837, 19838, 19839, 19841, 19842, 19845, 19852, 19853, 19857, 19858, 19866, 19867, 19868, 19869, 19871, 19872, 19875, 19879, 19882, 19883, 19885, 19886, 19888, 19889, 19890, 19891, 19898, 19901, 19904, 19906, 19907, 19908, 19910, 19911, 19912, 19914, 19916, 19917, 19918, 19919, 19920, 19921, 19923, 19924, 19925, 19927, 19928, 19931, 19933, 19934, 19935, 19937, 19938, 19940, 19941, 19943, 19946, 19947, 19950, 19951, 19952, 19953, 19954, 19958, 19959, 19962, 19964, 19966, 19967, 19968, 19970, 19971, 19972, 19976, 19979, 19980, 19981, 19983, 19984, 19988, 19989, 19990, 19991, 19993, 19995, 19996, 19997, 19999]\n",
+      "Indices of data-points that are subject to a run: [26, 27, 142, 143, 144, 145, 146, 147, 162, 220, 221, 222, 223, 245, 290, 291, 299, 300, 301, 328, 329, 356, 357, 410, 411, 456, 592, 593, 594, 595, 660, 661, 709, 717, 718, 839, 840, 1004, 1005, 1064, 1065, 1066, 1098, 1099, 1147, 1148, 1149, 1150, 1207, 1215, 1216, 1217, 1218, 1319, 1361, 1362, 1461, 1462, 1463, 1583, 1697, 1698, 1699, 1763, 1880, 2246, 2247, 2262, 2476, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2593, 2696, 2697, 2738, 2739, 2740, 2819, 2820, 2821, 2822, 2823, 2855, 2856, 2857, 2858, 2866, 2867, 2908, 2933, 3028, 3029, 3051, 3262, 3263, 3264, 3307, 3308, 3336, 3337, 3421, 3422, 3423, 3505, 3599, 3635, 3817, 4173, 4174, 4328, 4329, 4508, 4547, 4548, 4587, 4659, 4660, 4661, 4662, 4663, 4741, 4799, 4800, 4815, 4816, 4881, 4882, 4883, 4884, 4918, 5058, 5059, 5060, 5061, 5109, 5139, 5183, 5237, 5618, 5676, 5704, 5705, 5828, 6127, 6128, 6129, 6196, 6316, 6368, 6406, 6407, 6670, 6683, 6720, 6855, 6856, 6857, 6858, 6951, 6952, 6953, 6954, 6985, 6986, 6987, 7208, 7209, 7210, 7416, 7417, 7418, 7436, 7474, 7616, 7617, 7738, 7912, 7984, 7985, 7986, 7987, 7988, 7989, 8081, 8326, 8327, 8328, 8329, 8396, 8397, 8433, 8434, 8452, 8453, 8454, 8455, 8456, 8457, 8555, 8556, 8601, 8932, 8933, 8934, 8951, 9001, 9105, 9137, 9138, 9139, 9180, 9521, 9570, 9571, 9590, 9591, 9592, 9593, 9594, 10023, 10024, 10025, 10026, 10202, 10203, 10412, 10497, 10657, 10658, 10659, 10743, 10744, 10830, 10831, 10967, 10968, 10989, 11066, 11067, 11068, 11147, 11148, 11149, 11162, 11263, 11264, 11313, 11314, 11322, 11376, 11377, 11435, 11455, 11456, 11457, 11458, 11459, 11460, 11490, 11491, 11492, 11493, 11494, 11512, 11513, 11528, 11571, 11572, 11624, 11625, 11634, 11699, 11700, 11701, 11702, 11822, 11823, 11824, 11825, 11826, 11834, 11881, 11898, 11899, 11900, 12057, 12058, 12059, 12060, 12061, 12077, 12201, 12326, 12417, 12443, 12444, 12445, 12487, 12488, 12489, 12591, 12592, 12593, 12594, 12602, 12603, 12604, 12656, 12673, 12674, 12754, 12755, 12779, 12780, 12927, 12928, 12936, 13045, 13046, 13047, 13048, 13049, 13050, 13051, 13052, 13177, 13178, 13179, 13219, 13220, 13221, 13222, 13223, 13224, 13275, 13276, 13277, 13278, 13305, 13402, 13437, 13438, 13439, 13653, 13654, 13655, 13656, 13706, 13746, 13747, 13748, 13749, 14029, 14137, 14233, 14234, 14235, 14296, 14385, 14392, 14520, 14521, 14522, 14789, 14790, 14791, 14932, 14933, 15342, 15343, 15344, 15345, 15425, 15426, 15427, 15428, 15527, 15588, 15589, 15590, 15711, 15893, 15943, 15944, 15945, 16267, 16268, 16471, 16472, 16506, 16507, 16508, 16509, 16510, 16523, 16524, 16525, 16563, 16643, 16644, 16690, 16691, 16757, 16758, 16759, 16843, 16902, 16903, 16904, 16935, 16964, 17045, 17244, 17257, 17292, 17410, 17418, 17419, 17500, 17534, 17535, 17585, 17586, 17629, 17687, 17688, 17689, 17690, 17691, 17692, 17899, 17900, 17901, 17956, 17957, 17958, 17959, 18057, 18103, 18104, 18105, 18106, 18107, 18197, 18198, 18284, 18285, 18376, 18392, 18393, 18435, 18646, 18687, 18696, 18697, 18801, 18847, 18944, 18945, 19118, 19205, 19300, 19301, 19487, 19513, 19530, 19531, 19532, 19533, 19549, 19599, 19600, 19601, 19602, 19756, 19774, 19775, 19776, 19821, 19822]\n",
+      "Indices of data-points that are subject to a trend: [4900, 5141, 9473, 19737]\n",
+      "Indices of data-points violating limits: [31, 77, 108, 116, 133, 171, 187, 191, 196, 211, 219, 258, 264, 349, 367, 403, 457, 460, 485, 516, 522, 562, 571, 628, 654, 664, 675, 695, 699, 721, 731, 735, 737, 746, 752, 763, 782, 789, 798, 803, 805, 817, 832, 961, 964, 965, 978, 990, 992, 994, 995, 997, 998, 1019, 1021, 1051, 1059, 1060, 1061, 1066, 1068, 1070, 1084, 1087, 1094, 1097, 1099, 1100, 1107, 1117, 1130, 1140, 1145, 1164, 1174, 1198, 1207, 1209, 1217, 1218, 1224, 1229, 1234, 1245, 1252, 1260, 1261, 1263, 1269, 1272, 1273, 1278, 1283, 1285, 1287, 1291, 1294, 1300, 1302, 1303, 1306, 1307, 1318, 1319, 1323, 1334, 1336, 1337, 1340, 1343, 1344, 1348, 1349, 1352, 1355, 1358, 1365, 1370, 1371, 1372, 1378, 1379, 1381, 1383, 1384, 1387, 1388, 1389, 1390, 1391, 1395, 1396, 1399, 1401, 1402, 1412, 1418, 1421, 1423, 1425, 1428, 1429, 1431, 1441, 1442, 1444, 1445, 1452, 1454, 1459, 1464, 1466, 1470, 1473, 1474, 1476, 1477, 1478, 1480, 1485, 1487, 1492, 1493, 1497, 1498, 1500, 1501, 1504, 1505, 1507, 1508, 1509, 1510, 1511, 1512, 1515, 1516, 1518, 1521, 1522, 1524, 1525, 1526, 1529, 1532, 1535, 1538, 1539, 1541, 1543, 1544, 1545, 1548, 1552, 1553, 1554, 1555, 1557, 1561, 1564, 1565, 1566, 1569, 1570, 1572, 1574, 1575, 1576, 1580, 1581, 1582, 1585, 1586, 1587, 1588, 1589, 1590, 1592, 1593, 1595, 1597, 1598, 1602, 1603, 1604, 1607, 1608, 1614, 1618, 1619, 1622, 1624, 1625, 1631, 1632, 1633, 1635, 1637, 1639, 1641, 1642, 1644, 1649, 1651, 1652, 1653, 1655, 1656, 1657, 1659, 1662, 1664, 1666, 1667, 1668, 1669, 1672, 1674, 1675, 1677, 1678, 1679, 1680, 1682, 1683, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1694, 1695, 1698, 1699, 1700, 1703, 1704, 1705, 1706, 1708, 1709, 1710, 1711, 1712, 1714, 1716, 1717, 1718, 1719, 1720, 1722, 1724, 1726, 1728, 1729, 1730, 1731, 1732, 1734, 1738, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1751, 1753, 1754, 1755, 1756, 1758, 1759, 1761, 1762, 1763, 1764, 1765, 1767, 1769, 1772, 1773, 1774, 1775, 1776, 1777, 1779, 1781, 1782, 1783, 1784, 1785, 1786, 1789, 1790, 1791, 1793, 1794, 1796, 1800, 1801, 1802, 1804, 1805, 1806, 1807, 1808, 1810, 1812, 1813, 1815, 1816, 1817, 1818, 1820, 1821, 1822, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1832, 1833, 1835, 1837, 1838, 1840, 1841, 1842, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1852, 1853, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1868, 1870, 1871, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1903, 1906, 1907, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1918, 1919, 1921, 1922, 1923, 1924, 1925, 1926, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1969, 1971, 1972, 1973, 1974, 1976, 1977, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2001, 2009, 2071, 2117, 2139, 2199, 2215, 2222, 2245, 2250, 2267, 2293, 2317, 2333, 2346, 2356, 2376, 2380, 2392, 2425, 2438, 2445, 2453, 2459, 2465, 2490, 2517, 2522, 2534, 2575, 2601, 2620, 2653, 2669, 2683, 2686, 2688, 2695, 2701, 2729, 2752, 2755, 2761, 2779, 2894, 2914, 2926, 2941, 2956, 2999, 3037, 3055, 3056, 3059, 3065, 3067, 3068, 3087, 3100, 3116, 3124, 3125, 3128, 3137, 3139, 3140, 3146, 3147, 3148, 3150, 3163, 3166, 3172, 3179, 3181, 3189, 3201, 3207, 3213, 3216, 3223, 3224, 3225, 3229, 3235, 3240, 3244, 3249, 3251, 3254, 3259, 3261, 3265, 3267, 3269, 3272, 3274, 3277, 3286, 3288, 3291, 3293, 3294, 3295, 3296, 3300, 3301, 3311, 3313, 3320, 3322, 3325, 3326, 3330, 3331, 3334, 3338, 3339, 3342, 3346, 3347, 3348, 3354, 3358, 3364, 3365, 3366, 3367, 3371, 3372, 3373, 3376, 3380, 3384, 3387, 3388, 3391, 3395, 3397, 3399, 3400, 3407, 3410, 3411, 3412, 3413, 3415, 3418, 3419, 3421, 3426, 3428, 3429, 3434, 3436, 3437, 3438, 3442, 3443, 3447, 3448, 3449, 3451, 3452, 3454, 3455, 3460, 3461, 3466, 3468, 3471, 3472, 3474, 3479, 3480, 3482, 3488, 3489, 3490, 3496, 3497, 3503, 3504, 3505, 3507, 3510, 3512, 3515, 3519, 3521, 3522, 3526, 3533, 3537, 3539, 3540, 3541, 3542, 3544, 3547, 3550, 3552, 3553, 3555, 3561, 3562, 3564, 3566, 3567, 3568, 3569, 3570, 3573, 3578, 3580, 3581, 3582, 3584, 3586, 3587, 3593, 3594, 3596, 3599, 3602, 3606, 3607, 3608, 3609, 3611, 3612, 3615, 3616, 3617, 3619, 3621, 3622, 3625, 3626, 3629, 3632, 3633, 3635, 3637, 3638, 3640, 3643, 3645, 3647, 3649, 3650, 3652, 3653, 3654, 3655, 3656, 3660, 3661, 3663, 3666, 3670, 3672, 3675, 3676, 3677, 3678, 3681, 3682, 3683, 3685, 3688, 3689, 3690, 3691, 3700, 3701, 3702, 3703, 3704, 3706, 3707, 3709, 3711, 3713, 3714, 3717, 3718, 3720, 3721, 3722, 3723, 3724, 3726, 3728, 3730, 3732, 3733, 3735, 3736, 3737, 3738, 3739, 3742, 3744, 3745, 3746, 3748, 3749, 3750, 3751, 3752, 3755, 3758, 3759, 3760, 3761, 3763, 3764, 3766, 3767, 3768, 3770, 3771, 3773, 3775, 3776, 3778, 3779, 3780, 3781, 3783, 3784, 3785, 3789, 3791, 3792, 3793, 3795, 3796, 3797, 3798, 3799, 3800, 3801, 3807, 3808, 3810, 3811, 3812, 3813, 3815, 3816, 3819, 3820, 3821, 3822, 3823, 3824, 3825, 3826, 3828, 3829, 3830, 3832, 3833, 3835, 3836, 3837, 3838, 3840, 3843, 3844, 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3853, 3854, 3855, 3857, 3858, 3859, 3861, 3863, 3865, 3866, 3868, 3869, 3870, 3871, 3874, 3875, 3876, 3878, 3882, 3883, 3884, 3885, 3886, 3887, 3888, 3889, 3890, 3891, 3892, 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, 3901, 3902, 3903, 3904, 3905, 3906, 3907, 3908, 3909, 3910, 3911, 3912, 3913, 3914, 3915, 3917, 3918, 3919, 3920, 3921, 3922, 3923, 3924, 3925, 3926, 3927, 3928, 3929, 3930, 3931, 3932, 3933, 3934, 3935, 3936, 3937, 3938, 3940, 3941, 3942, 3943, 3944, 3945, 3946, 3947, 3948, 3949, 3950, 3951, 3953, 3954, 3955, 3956, 3957, 3959, 3960, 3962, 3963, 3964, 3965, 3966, 3967, 3968, 3969, 3970, 3971, 3973, 3974, 3976, 3979, 3980, 3982, 3983, 3984, 3985, 3986, 3987, 3988, 3989, 3990, 3992, 3993, 3994, 3995, 3996, 3997, 3998, 3999, 4007, 4013, 4031, 4036, 4054, 4064, 4074, 4123, 4124, 4125, 4127, 4170, 4186, 4187, 4219, 4247, 4269, 4279, 4337, 4342, 4357, 4360, 4374, 4403, 4410, 4480, 4488, 4504, 4509, 4527, 4540, 4541, 4549, 4560, 4626, 4695, 4707, 4736, 4745, 4772, 4802, 4808, 4819, 4854, 4910, 4931, 4937, 4979, 4994, 4995, 4996, 4997, 4998, 4999, 5025, 5028, 5029, 5079, 5083, 5092, 5095, 5102, 5103, 5104, 5108, 5113, 5128, 5140, 5141, 5144, 5145, 5148, 5156, 5157, 5161, 5163, 5165, 5166, 5167, 5175, 5186, 5201, 5205, 5209, 5210, 5220, 5223, 5231, 5235, 5241, 5242, 5245, 5255, 5256, 5257, 5266, 5271, 5274, 5293, 5301, 5304, 5306, 5308, 5309, 5311, 5313, 5315, 5316, 5324, 5325, 5330, 5335, 5339, 5342, 5345, 5347, 5363, 5365, 5381, 5383, 5384, 5386, 5391, 5392, 5396, 5400, 5401, 5404, 5409, 5411, 5413, 5414, 5418, 5419, 5420, 5423, 5427, 5433, 5434, 5435, 5436, 5437, 5438, 5440, 5441, 5444, 5447, 5450, 5453, 5456, 5461, 5462, 5465, 5468, 5474, 5475, 5480, 5482, 5484, 5485, 5488, 5490, 5495, 5496, 5501, 5502, 5504, 5507, 5508, 5511, 5514, 5518, 5522, 5524, 5528, 5529, 5531, 5533, 5535, 5539, 5543, 5547, 5548, 5550, 5551, 5554, 5555, 5556, 5559, 5564, 5566, 5567, 5569, 5572, 5575, 5577, 5579, 5581, 5582, 5584, 5587, 5588, 5589, 5590, 5591, 5593, 5594, 5595, 5597, 5598, 5599, 5602, 5607, 5608, 5609, 5610, 5612, 5615, 5619, 5620, 5624, 5626, 5632, 5633, 5635, 5637, 5639, 5643, 5644, 5645, 5647, 5648, 5649, 5650, 5651, 5653, 5655, 5656, 5660, 5662, 5663, 5664, 5665, 5667, 5669, 5670, 5671, 5674, 5675, 5677, 5678, 5680, 5685, 5686, 5687, 5691, 5692, 5693, 5695, 5696, 5697, 5699, 5700, 5703, 5704, 5706, 5707, 5709, 5710, 5712, 5713, 5714, 5715, 5717, 5718, 5720, 5722, 5724, 5726, 5728, 5733, 5735, 5736, 5737, 5741, 5742, 5743, 5744, 5745, 5747, 5748, 5749, 5751, 5753, 5757, 5758, 5760, 5763, 5765, 5768, 5770, 5771, 5772, 5773, 5774, 5775, 5776, 5777, 5779, 5780, 5783, 5785, 5786, 5787, 5788, 5789, 5790, 5791, 5792, 5793, 5795, 5796, 5798, 5799, 5800, 5801, 5802, 5803, 5804, 5807, 5809, 5813, 5814, 5816, 5817, 5818, 5821, 5822, 5823, 5825, 5827, 5830, 5831, 5832, 5833, 5834, 5836, 5838, 5839, 5840, 5841, 5843, 5844, 5845, 5846, 5847, 5848, 5852, 5855, 5857, 5858, 5859, 5861, 5862, 5863, 5864, 5865, 5866, 5868, 5870, 5871, 5874, 5875, 5877, 5878, 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, 5887, 5889, 5890, 5891, 5893, 5894, 5895, 5896, 5897, 5898, 5899, 5900, 5902, 5904, 5906, 5908, 5909, 5910, 5912, 5913, 5914, 5915, 5917, 5918, 5919, 5920, 5921, 5924, 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, 5933, 5935, 5936, 5937, 5938, 5939, 5941, 5943, 5946, 5948, 5949, 5951, 5953, 5954, 5955, 5956, 5957, 5958, 5959, 5961, 5962, 5963, 5964, 5965, 5966, 5967, 5968, 5970, 5971, 5972, 5974, 5975, 5976, 5979, 5980, 5982, 5983, 5984, 5985, 5987, 5988, 5990, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999, 6023, 6048, 6099, 6118, 6130, 6163, 6165, 6175, 6197, 6199, 6225, 6228, 6255, 6262, 6278, 6315, 6334, 6342, 6415, 6461, 6483, 6490, 6498, 6530, 6532, 6535, 6538, 6557, 6579, 6601, 6615, 6635, 6651, 6757, 6766, 6775, 6789, 6793, 6807, 6826, 6847, 6850, 6867, 6911, 6963, 6969, 6980, 6985, 6987, 6988, 6992, 6993, 6995, 6996, 6999, 7007, 7014, 7030, 7033, 7057, 7065, 7068, 7092, 7098, 7100, 7117, 7120, 7143, 7145, 7146, 7169, 7179, 7180, 7185, 7192, 7194, 7202, 7205, 7210, 7213, 7216, 7219, 7224, 7227, 7238, 7240, 7243, 7247, 7248, 7250, 7251, 7252, 7254, 7256, 7265, 7267, 7277, 7280, 7286, 7290, 7298, 7300, 7305, 7306, 7314, 7316, 7323, 7324, 7327, 7329, 7333, 7334, 7337, 7340, 7341, 7343, 7350, 7357, 7358, 7362, 7363, 7374, 7375, 7376, 7380, 7384, 7388, 7392, 7393, 7395, 7398, 7404, 7406, 7410, 7413, 7415, 7416, 7417, 7418, 7424, 7426, 7427, 7429, 7432, 7434, 7435, 7436, 7439, 7441, 7444, 7445, 7446, 7447, 7450, 7459, 7460, 7461, 7466, 7467, 7468, 7471, 7476, 7477, 7480, 7482, 7485, 7486, 7487, 7489, 7497, 7501, 7502, 7505, 7506, 7508, 7509, 7511, 7512, 7516, 7519, 7522, 7523, 7525, 7527, 7532, 7534, 7536, 7541, 7542, 7544, 7550, 7551, 7552, 7556, 7557, 7561, 7563, 7565, 7567, 7568, 7573, 7576, 7578, 7581, 7584, 7585, 7587, 7589, 7590, 7593, 7594, 7595, 7596, 7597, 7598, 7599, 7601, 7602, 7603, 7605, 7608, 7611, 7612, 7613, 7619, 7621, 7623, 7628, 7629, 7630, 7631, 7632, 7634, 7639, 7640, 7641, 7642, 7643, 7645, 7646, 7648, 7649, 7650, 7651, 7653, 7654, 7656, 7657, 7660, 7665, 7674, 7675, 7676, 7677, 7679, 7683, 7686, 7689, 7691, 7693, 7694, 7696, 7698, 7701, 7702, 7703, 7704, 7705, 7706, 7708, 7709, 7710, 7712, 7714, 7717, 7720, 7722, 7723, 7724, 7725, 7726, 7727, 7728, 7730, 7731, 7733, 7736, 7738, 7739, 7740, 7744, 7747, 7749, 7751, 7752, 7753, 7755, 7756, 7758, 7760, 7763, 7764, 7765, 7766, 7768, 7769, 7770, 7772, 7777, 7778, 7779, 7780, 7781, 7783, 7784, 7785, 7786, 7787, 7789, 7792, 7794, 7795, 7796, 7798, 7800, 7801, 7802, 7803, 7806, 7807, 7808, 7809, 7810, 7811, 7812, 7813, 7815, 7816, 7817, 7818, 7820, 7821, 7822, 7823, 7824, 7825, 7826, 7829, 7831, 7832, 7833, 7834, 7835, 7836, 7837, 7838, 7839, 7840, 7841, 7842, 7843, 7844, 7845, 7846, 7847, 7849, 7850, 7851, 7852, 7853, 7860, 7861, 7863, 7866, 7867, 7868, 7869, 7870, 7872, 7873, 7874, 7875, 7877, 7880, 7882, 7883, 7884, 7885, 7886, 7888, 7889, 7891, 7892, 7893, 7894, 7895, 7896, 7897, 7900, 7901, 7902, 7903, 7904, 7905, 7906, 7907, 7908, 7909, 7912, 7913, 7915, 7916, 7917, 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7925, 7927, 7928, 7929, 7930, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7943, 7944, 7945, 7946, 7947, 7948, 7951, 7952, 7953, 7955, 7956, 7957, 7958, 7959, 7960, 7961, 7962, 7964, 7966, 7967, 7970, 7971, 7972, 7974, 7975, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7986, 7987, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8015, 8030, 8036, 8038, 8105, 8119, 8124, 8149, 8206, 8266, 8277, 8278, 8304, 8330, 8333, 8338, 8376, 8426, 8430, 8455, 8462, 8475, 8508, 8521, 8533, 8556, 8566, 8587, 8594, 8598, 8605, 8616, 8630, 8665, 8667, 8682, 8688, 8702, 8727, 8738, 8743, 8754, 8764, 8766, 8801, 8808, 8823, 8855, 8878, 8885, 8912, 8920, 8923, 8954, 8985, 8987, 8988, 8994, 8995, 8996, 8997, 8998, 8999, 9017, 9037, 9043, 9056, 9060, 9066, 9083, 9092, 9118, 9124, 9130, 9146, 9148, 9153, 9158, 9164, 9166, 9173, 9176, 9181, 9184, 9185, 9189, 9191, 9192, 9202, 9213, 9217, 9219, 9220, 9222, 9224, 9227, 9229, 9234, 9269, 9278, 9283, 9284, 9299, 9304, 9305, 9306, 9307, 9311, 9314, 9315, 9319, 9323, 9326, 9332, 9341, 9342, 9348, 9349, 9354, 9357, 9359, 9360, 9365, 9366, 9373, 9375, 9376, 9386, 9388, 9391, 9396, 9398, 9402, 9403, 9404, 9406, 9409, 9410, 9411, 9415, 9419, 9420, 9421, 9425, 9433, 9439, 9440, 9441, 9461, 9465, 9467, 9472, 9473, 9474, 9481, 9488, 9495, 9496, 9500, 9502, 9505, 9522, 9524, 9525, 9526, 9528, 9529, 9531, 9535, 9537, 9540, 9542, 9543, 9544, 9547, 9548, 9549, 9551, 9552, 9553, 9556, 9558, 9559, 9562, 9571, 9573, 9577, 9578, 9580, 9586, 9587, 9590, 9592, 9594, 9598, 9599, 9605, 9609, 9613, 9617, 9620, 9624, 9627, 9630, 9632, 9637, 9640, 9643, 9645, 9651, 9656, 9658, 9659, 9660, 9662, 9663, 9667, 9668, 9671, 9675, 9676, 9677, 9684, 9685, 9686, 9687, 9689, 9690, 9691, 9693, 9696, 9697, 9698, 9701, 9702, 9703, 9713, 9714, 9722, 9726, 9731, 9735, 9739, 9742, 9744, 9745, 9747, 9753, 9754, 9757, 9761, 9763, 9764, 9767, 9768, 9771, 9772, 9773, 9777, 9779, 9780, 9785, 9786, 9791, 9792, 9794, 9795, 9798, 9803, 9805, 9806, 9810, 9812, 9813, 9816, 9817, 9819, 9821, 9822, 9825, 9828, 9830, 9831, 9832, 9835, 9841, 9845, 9846, 9847, 9848, 9849, 9850, 9851, 9852, 9853, 9854, 9856, 9857, 9859, 9860, 9862, 9868, 9869, 9870, 9873, 9874, 9876, 9877, 9879, 9880, 9881, 9882, 9883, 9889, 9891, 9894, 9897, 9898, 9899, 9900, 9901, 9902, 9903, 9905, 9906, 9907, 9911, 9913, 9914, 9917, 9922, 9923, 9927, 9929, 9930, 9931, 9933, 9935, 9937, 9939, 9940, 9943, 9944, 9948, 9949, 9950, 9951, 9952, 9956, 9958, 9959, 9963, 9966, 9969, 9973, 9975, 9976, 9978, 9979, 9980, 9985, 9987, 9989, 9990, 9991, 9992, 9993, 9994, 9995, 9996, 9998, 9999, 10001, 10008, 10031, 10069, 10087, 10125, 10153, 10202, 10206, 10211, 10237, 10295, 10297, 10317, 10325, 10329, 10344, 10348, 10350, 10358, 10359, 10365, 10405, 10413, 10422, 10434, 10462, 10472, 10508, 10555, 10572, 10580, 10589, 10603, 10605, 10614, 10625, 10627, 10637, 10647, 10672, 10691, 10706, 10722, 10737, 10757, 10760, 10769, 10786, 10809, 10819, 10840, 10852, 10879, 10896, 10907, 10919, 10947, 10954, 10969, 10981, 10983, 10984, 10986, 10987, 10988, 10990, 10992, 10998, 10999, 11004, 11035, 11058, 11071, 11093, 11100, 11103, 11122, 11125, 11126, 11134, 11136, 11155, 11175, 11176, 11183, 11196, 11206, 11218, 11219, 11230, 11233, 11238, 11247, 11249, 11260, 11261, 11262, 11265, 11280, 11286, 11292, 11293, 11294, 11302, 11305, 11316, 11322, 11323, 11325, 11327, 11333, 11334, 11335, 11337, 11341, 11345, 11346, 11348, 11355, 11361, 11369, 11370, 11371, 11379, 11389, 11390, 11395, 11396, 11397, 11399, 11400, 11402, 11403, 11405, 11406, 11407, 11409, 11410, 11413, 11415, 11416, 11421, 11422, 11423, 11426, 11427, 11428, 11432, 11443, 11446, 11448, 11450, 11451, 11458, 11463, 11467, 11469, 11470, 11472, 11475, 11478, 11479, 11484, 11485, 11487, 11498, 11500, 11502, 11503, 11506, 11507, 11510, 11511, 11513, 11518, 11520, 11524, 11526, 11527, 11529, 11537, 11540, 11543, 11547, 11548, 11552, 11555, 11556, 11560, 11562, 11563, 11566, 11568, 11570, 11572, 11574, 11578, 11579, 11580, 11584, 11585, 11590, 11595, 11596, 11597, 11605, 11609, 11611, 11613, 11614, 11615, 11620, 11621, 11622, 11628, 11629, 11630, 11633, 11634, 11635, 11637, 11642, 11643, 11646, 11649, 11650, 11651, 11653, 11654, 11659, 11660, 11661, 11662, 11666, 11667, 11674, 11676, 11678, 11679, 11681, 11683, 11684, 11685, 11686, 11689, 11690, 11692, 11694, 11695, 11697, 11700, 11701, 11702, 11703, 11705, 11706, 11707, 11709, 11712, 11715, 11716, 11718, 11721, 11723, 11724, 11726, 11727, 11729, 11732, 11733, 11735, 11737, 11738, 11739, 11740, 11743, 11746, 11749, 11751, 11752, 11754, 11755, 11756, 11757, 11758, 11759, 11760, 11761, 11762, 11763, 11764, 11765, 11767, 11768, 11769, 11770, 11771, 11772, 11775, 11776, 11779, 11780, 11782, 11783, 11784, 11785, 11788, 11789, 11791, 11792, 11793, 11794, 11799, 11801, 11802, 11803, 11807, 11808, 11811, 11813, 11814, 11816, 11817, 11820, 11821, 11822, 11823, 11824, 11825, 11826, 11830, 11831, 11832, 11834, 11835, 11836, 11837, 11838, 11839, 11841, 11842, 11844, 11845, 11848, 11849, 11851, 11852, 11853, 11854, 11855, 11859, 11860, 11861, 11862, 11863, 11865, 11866, 11867, 11871, 11872, 11873, 11874, 11875, 11877, 11878, 11880, 11882, 11883, 11888, 11889, 11890, 11891, 11892, 11894, 11895, 11896, 11897, 11898, 11900, 11901, 11902, 11903, 11904, 11905, 11906, 11907, 11908, 11910, 11912, 11913, 11915, 11916, 11917, 11919, 11920, 11922, 11925, 11926, 11930, 11931, 11932, 11933, 11935, 11936, 11937, 11940, 11942, 11943, 11944, 11946, 11947, 11948, 11949, 11950, 11952, 11955, 11956, 11957, 11959, 11960, 11961, 11962, 11963, 11964, 11967, 11969, 11970, 11971, 11972, 11976, 11977, 11978, 11980, 11982, 11983, 11984, 11985, 11986, 11987, 11988, 11989, 11991, 11992, 11993, 11994, 11995, 11998, 12016, 12027, 12030, 12036, 12090, 12115, 12129, 12186, 12188, 12192, 12348, 12359, 12367, 12381, 12388, 12393, 12431, 12445, 12447, 12454, 12460, 12461, 12481, 12483, 12485, 12487, 12494, 12502, 12504, 12520, 12533, 12534, 12547, 12639, 12665, 12666, 12680, 12695, 12704, 12751, 12755, 12813, 12818, 12825, 12835, 12837, 12853, 12908, 12963, 12968, 12987, 12990, 12991, 12992, 12993, 12996, 12997, 12998, 12999, 13024, 13046, 13056, 13075, 13079, 13084, 13102, 13103, 13107, 13116, 13119, 13120, 13123, 13124, 13126, 13128, 13129, 13131, 13142, 13149, 13151, 13152, 13154, 13156, 13158, 13162, 13167, 13184, 13194, 13195, 13196, 13199, 13202, 13204, 13205, 13211, 13215, 13217, 13220, 13227, 13237, 13242, 13244, 13248, 13252, 13255, 13257, 13260, 13261, 13265, 13266, 13268, 13269, 13280, 13283, 13285, 13300, 13304, 13308, 13309, 13317, 13319, 13320, 13321, 13322, 13324, 13325, 13328, 13329, 13336, 13338, 13340, 13341, 13342, 13344, 13345, 13350, 13352, 13355, 13357, 13359, 13360, 13361, 13366, 13370, 13372, 13375, 13376, 13378, 13381, 13385, 13387, 13391, 13393, 13397, 13403, 13405, 13406, 13407, 13408, 13412, 13414, 13415, 13417, 13418, 13423, 13424, 13425, 13426, 13427, 13429, 13434, 13436, 13438, 13440, 13442, 13447, 13450, 13451, 13456, 13457, 13463, 13468, 13473, 13474, 13477, 13478, 13480, 13482, 13483, 13484, 13485, 13492, 13493, 13495, 13496, 13499, 13500, 13503, 13504, 13505, 13506, 13507, 13509, 13511, 13513, 13514, 13515, 13516, 13517, 13518, 13519, 13521, 13524, 13527, 13530, 13533, 13535, 13537, 13539, 13540, 13542, 13544, 13545, 13546, 13548, 13551, 13552, 13554, 13555, 13557, 13559, 13561, 13564, 13567, 13570, 13572, 13573, 13576, 13578, 13579, 13580, 13581, 13582, 13583, 13584, 13585, 13587, 13588, 13589, 13592, 13594, 13595, 13596, 13597, 13599, 13601, 13603, 13605, 13606, 13611, 13613, 13614, 13617, 13622, 13623, 13626, 13627, 13629, 13630, 13632, 13634, 13636, 13637, 13641, 13643, 13645, 13646, 13647, 13648, 13649, 13650, 13651, 13652, 13653, 13655, 13656, 13657, 13659, 13660, 13661, 13663, 13664, 13668, 13670, 13671, 13672, 13673, 13678, 13679, 13680, 13683, 13685, 13687, 13689, 13690, 13691, 13692, 13693, 13694, 13695, 13696, 13698, 13699, 13701, 13702, 13703, 13705, 13706, 13707, 13708, 13710, 13711, 13712, 13713, 13715, 13720, 13721, 13723, 13725, 13728, 13729, 13730, 13731, 13733, 13734, 13736, 13737, 13738, 13740, 13741, 13743, 13744, 13745, 13746, 13747, 13748, 13750, 13751, 13754, 13755, 13756, 13757, 13758, 13759, 13760, 13761, 13762, 13763, 13765, 13766, 13768, 13769, 13771, 13772, 13773, 13775, 13777, 13778, 13779, 13781, 13783, 13784, 13785, 13788, 13789, 13790, 13792, 13793, 13794, 13795, 13796, 13797, 13798, 13800, 13801, 13803, 13804, 13805, 13807, 13809, 13810, 13812, 13813, 13814, 13815, 13816, 13817, 13820, 13821, 13822, 13823, 13824, 13829, 13830, 13831, 13832, 13833, 13834, 13835, 13838, 13839, 13840, 13841, 13842, 13844, 13845, 13846, 13847, 13848, 13849, 13850, 13851, 13852, 13855, 13856, 13857, 13859, 13860, 13862, 13863, 13865, 13866, 13867, 13868, 13869, 13870, 13872, 13874, 13875, 13876, 13877, 13878, 13879, 13880, 13881, 13882, 13883, 13884, 13885, 13886, 13887, 13888, 13889, 13890, 13893, 13894, 13895, 13896, 13897, 13898, 13899, 13901, 13903, 13904, 13905, 13906, 13907, 13908, 13909, 13910, 13912, 13914, 13915, 13916, 13917, 13918, 13919, 13920, 13921, 13923, 13924, 13925, 13926, 13927, 13929, 13931, 13932, 13933, 13934, 13935, 13936, 13937, 13938, 13940, 13941, 13942, 13943, 13944, 13945, 13947, 13948, 13949, 13950, 13952, 13954, 13955, 13956, 13957, 13958, 13959, 13960, 13961, 13962, 13963, 13964, 13965, 13966, 13967, 13968, 13969, 13971, 13973, 13974, 13975, 13976, 13979, 13980, 13981, 13982, 13983, 13984, 13985, 13987, 13988, 13989, 13990, 13991, 13992, 13993, 13994, 13997, 13998, 13999, 14001, 14017, 14030, 14032, 14039, 14057, 14059, 14102, 14119, 14128, 14164, 14166, 14174, 14185, 14210, 14220, 14221, 14237, 14288, 14314, 14327, 14330, 14343, 14346, 14381, 14455, 14467, 14469, 14471, 14539, 14550, 14584, 14611, 14616, 14624, 14637, 14662, 14666, 14702, 14707, 14737, 14795, 14800, 14803, 14806, 14820, 14882, 14905, 14931, 14940, 14958, 14990, 14992, 14993, 14998, 14999, 15042, 15057, 15066, 15072, 15077, 15099, 15103, 15105, 15106, 15111, 15116, 15117, 15119, 15134, 15135, 15143, 15163, 15167, 15170, 15172, 15175, 15187, 15212, 15213, 15217, 15219, 15226, 15228, 15229, 15232, 15235, 15238, 15244, 15249, 15251, 15262, 15264, 15276, 15281, 15286, 15291, 15293, 15305, 15306, 15307, 15319, 15322, 15323, 15325, 15331, 15337, 15343, 15344, 15347, 15348, 15350, 15352, 15353, 15354, 15355, 15356, 15361, 15365, 15367, 15368, 15370, 15371, 15375, 15376, 15377, 15380, 15384, 15386, 15387, 15389, 15396, 15397, 15400, 15401, 15405, 15407, 15410, 15412, 15418, 15419, 15428, 15429, 15436, 15438, 15442, 15451, 15453, 15455, 15459, 15460, 15461, 15462, 15463, 15473, 15478, 15481, 15483, 15484, 15489, 15495, 15496, 15500, 15502, 15503, 15505, 15507, 15510, 15514, 15515, 15518, 15520, 15521, 15523, 15524, 15531, 15532, 15534, 15535, 15538, 15539, 15540, 15541, 15544, 15545, 15546, 15547, 15550, 15551, 15552, 15554, 15555, 15560, 15561, 15562, 15564, 15566, 15568, 15571, 15573, 15574, 15579, 15580, 15581, 15585, 15586, 15588, 15591, 15592, 15593, 15595, 15596, 15599, 15602, 15606, 15608, 15609, 15613, 15614, 15616, 15617, 15619, 15620, 15622, 15623, 15625, 15628, 15632, 15633, 15634, 15635, 15642, 15643, 15644, 15647, 15648, 15650, 15653, 15654, 15656, 15657, 15658, 15659, 15660, 15661, 15662, 15664, 15667, 15668, 15670, 15671, 15672, 15674, 15676, 15678, 15680, 15681, 15682, 15684, 15685, 15686, 15688, 15690, 15692, 15693, 15695, 15698, 15702, 15703, 15704, 15705, 15706, 15707, 15712, 15713, 15715, 15716, 15719, 15720, 15721, 15722, 15723, 15725, 15726, 15728, 15730, 15733, 15734, 15735, 15736, 15738, 15740, 15741, 15743, 15744, 15745, 15746, 15747, 15748, 15749, 15750, 15751, 15752, 15753, 15754, 15756, 15757, 15759, 15763, 15764, 15767, 15769, 15770, 15772, 15773, 15774, 15776, 15777, 15778, 15779, 15780, 15781, 15782, 15783, 15784, 15785, 15786, 15788, 15789, 15790, 15792, 15793, 15794, 15796, 15797, 15798, 15799, 15800, 15803, 15805, 15806, 15807, 15808, 15809, 15811, 15812, 15814, 15815, 15816, 15817, 15818, 15820, 15821, 15823, 15827, 15828, 15829, 15831, 15834, 15836, 15837, 15838, 15839, 15840, 15842, 15844, 15847, 15848, 15849, 15850, 15851, 15852, 15853, 15854, 15855, 15857, 15858, 15859, 15861, 15862, 15864, 15865, 15866, 15867, 15868, 15869, 15872, 15874, 15875, 15876, 15877, 15878, 15879, 15880, 15881, 15883, 15885, 15886, 15887, 15888, 15889, 15890, 15892, 15893, 15895, 15897, 15898, 15899, 15900, 15901, 15903, 15907, 15910, 15911, 15913, 15914, 15915, 15916, 15917, 15918, 15919, 15920, 15921, 15923, 15924, 15925, 15926, 15928, 15929, 15931, 15932, 15933, 15934, 15937, 15939, 15940, 15941, 15942, 15944, 15945, 15947, 15950, 15952, 15953, 15954, 15955, 15956, 15958, 15959, 15961, 15962, 15963, 15964, 15965, 15966, 15967, 15968, 15969, 15970, 15971, 15972, 15973, 15974, 15975, 15976, 15978, 15980, 15982, 15984, 15985, 15986, 15987, 15988, 15989, 15990, 15991, 15992, 15993, 15994, 15995, 15996, 15997, 15998, 16010, 16019, 16022, 16046, 16076, 16106, 16111, 16119, 16120, 16134, 16155, 16157, 16201, 16205, 16220, 16222, 16258, 16260, 16275, 16284, 16287, 16305, 16307, 16313, 16342, 16368, 16371, 16447, 16454, 16493, 16513, 16524, 16575, 16583, 16585, 16597, 16622, 16646, 16675, 16682, 16748, 16767, 16831, 16832, 16838, 16841, 16880, 16885, 16891, 16895, 16906, 16945, 16952, 16965, 16970, 16975, 16978, 16995, 16996, 16997, 17005, 17025, 17026, 17030, 17040, 17050, 17077, 17081, 17086, 17110, 17122, 17127, 17137, 17157, 17159, 17160, 17169, 17174, 17188, 17195, 17196, 17197, 17202, 17213, 17214, 17217, 17218, 17220, 17231, 17232, 17245, 17246, 17249, 17257, 17261, 17262, 17271, 17272, 17273, 17274, 17277, 17281, 17284, 17286, 17288, 17292, 17297, 17310, 17312, 17315, 17321, 17322, 17325, 17327, 17330, 17344, 17345, 17347, 17354, 17355, 17361, 17369, 17373, 17375, 17376, 17378, 17389, 17390, 17393, 17395, 17397, 17401, 17402, 17403, 17411, 17414, 17420, 17422, 17423, 17429, 17440, 17443, 17445, 17448, 17449, 17451, 17452, 17456, 17457, 17459, 17465, 17467, 17469, 17470, 17473, 17475, 17476, 17478, 17480, 17482, 17484, 17488, 17490, 17492, 17493, 17494, 17495, 17496, 17498, 17502, 17504, 17505, 17511, 17515, 17517, 17521, 17526, 17529, 17530, 17532, 17536, 17537, 17539, 17543, 17544, 17548, 17554, 17555, 17556, 17558, 17559, 17561, 17562, 17564, 17567, 17568, 17569, 17572, 17574, 17575, 17579, 17580, 17582, 17584, 17585, 17586, 17587, 17588, 17589, 17599, 17602, 17604, 17605, 17606, 17607, 17609, 17611, 17612, 17613, 17614, 17620, 17622, 17624, 17625, 17626, 17628, 17629, 17630, 17631, 17633, 17634, 17636, 17637, 17638, 17643, 17644, 17646, 17647, 17649, 17650, 17651, 17652, 17653, 17655, 17656, 17657, 17658, 17659, 17662, 17668, 17669, 17670, 17672, 17676, 17678, 17679, 17680, 17681, 17684, 17685, 17686, 17687, 17688, 17689, 17691, 17692, 17695, 17697, 17698, 17700, 17701, 17702, 17703, 17704, 17705, 17709, 17712, 17713, 17716, 17719, 17720, 17722, 17723, 17726, 17727, 17729, 17731, 17732, 17733, 17734, 17737, 17738, 17740, 17741, 17742, 17743, 17744, 17746, 17751, 17753, 17754, 17755, 17756, 17757, 17758, 17759, 17760, 17761, 17762, 17763, 17769, 17772, 17773, 17774, 17775, 17777, 17779, 17780, 17781, 17782, 17783, 17784, 17786, 17787, 17788, 17790, 17791, 17792, 17794, 17797, 17798, 17801, 17802, 17804, 17808, 17810, 17812, 17815, 17816, 17817, 17819, 17821, 17822, 17823, 17824, 17826, 17827, 17830, 17832, 17833, 17834, 17838, 17840, 17841, 17844, 17846, 17847, 17849, 17850, 17851, 17852, 17853, 17854, 17856, 17858, 17859, 17860, 17862, 17864, 17865, 17866, 17867, 17868, 17869, 17870, 17871, 17872, 17873, 17874, 17875, 17877, 17879, 17880, 17882, 17884, 17885, 17886, 17888, 17889, 17891, 17892, 17894, 17897, 17898, 17900, 17901, 17902, 17903, 17904, 17905, 17906, 17908, 17909, 17910, 17911, 17912, 17914, 17917, 17919, 17920, 17922, 17923, 17924, 17926, 17928, 17929, 17931, 17933, 17934, 17936, 17937, 17938, 17939, 17940, 17941, 17943, 17944, 17945, 17946, 17947, 17948, 17949, 17955, 17956, 17957, 17959, 17960, 17962, 17963, 17964, 17965, 17966, 17969, 17971, 17972, 17973, 17974, 17975, 17976, 17978, 17979, 17981, 17983, 17984, 17985, 17988, 17989, 17990, 17991, 17993, 17994, 17995, 17996, 17997, 17998, 18006, 18009, 18036, 18079, 18121, 18192, 18251, 18255, 18293, 18308, 18316, 18368, 18370, 18377, 18390, 18427, 18433, 18444, 18454, 18496, 18556, 18560, 18563, 18576, 18633, 18634, 18639, 18647, 18656, 18658, 18663, 18672, 18674, 18716, 18752, 18753, 18767, 18884, 18918, 18921, 18944, 18956, 18971, 18974, 18980, 18993, 18996, 18997, 18999, 19001, 19026, 19042, 19058, 19063, 19067, 19074, 19076, 19096, 19121, 19128, 19134, 19137, 19147, 19164, 19168, 19170, 19178, 19180, 19182, 19190, 19198, 19199, 19204, 19206, 19209, 19218, 19221, 19225, 19228, 19233, 19236, 19237, 19248, 19250, 19267, 19276, 19277, 19285, 19291, 19294, 19298, 19306, 19314, 19320, 19322, 19326, 19328, 19331, 19338, 19339, 19340, 19347, 19351, 19353, 19360, 19365, 19367, 19368, 19371, 19374, 19378, 19379, 19382, 19384, 19390, 19392, 19394, 19396, 19398, 19401, 19403, 19407, 19411, 19414, 19415, 19422, 19423, 19429, 19432, 19435, 19438, 19441, 19442, 19444, 19448, 19449, 19450, 19456, 19458, 19462, 19464, 19465, 19469, 19470, 19472, 19474, 19476, 19483, 19484, 19485, 19488, 19489, 19491, 19494, 19501, 19504, 19516, 19519, 19520, 19523, 19524, 19526, 19527, 19528, 19530, 19531, 19537, 19538, 19541, 19551, 19555, 19556, 19558, 19562, 19564, 19566, 19567, 19568, 19571, 19572, 19575, 19577, 19578, 19580, 19582, 19583, 19584, 19591, 19598, 19602, 19604, 19606, 19610, 19613, 19614, 19615, 19618, 19623, 19624, 19625, 19626, 19630, 19631, 19633, 19639, 19640, 19641, 19643, 19644, 19646, 19648, 19652, 19655, 19657, 19658, 19662, 19663, 19665, 19667, 19668, 19677, 19678, 19679, 19682, 19683, 19684, 19685, 19689, 19694, 19695, 19699, 19701, 19703, 19704, 19708, 19710, 19711, 19712, 19714, 19720, 19722, 19724, 19726, 19727, 19728, 19729, 19731, 19732, 19736, 19737, 19738, 19743, 19747, 19748, 19749, 19750, 19751, 19752, 19754, 19757, 19760, 19761, 19762, 19763, 19765, 19766, 19767, 19768, 19771, 19775, 19776, 19777, 19778, 19780, 19781, 19783, 19784, 19786, 19788, 19789, 19792, 19793, 19795, 19798, 19801, 19802, 19804, 19805, 19808, 19812, 19814, 19815, 19817, 19818, 19819, 19822, 19824, 19828, 19829, 19831, 19832, 19833, 19834, 19835, 19837, 19838, 19839, 19841, 19842, 19845, 19852, 19853, 19857, 19858, 19866, 19867, 19868, 19869, 19871, 19872, 19875, 19879, 19882, 19883, 19885, 19886, 19888, 19889, 19890, 19891, 19898, 19901, 19904, 19906, 19907, 19908, 19910, 19911, 19912, 19914, 19916, 19917, 19918, 19919, 19920, 19921, 19923, 19924, 19925, 19927, 19928, 19931, 19933, 19934, 19935, 19937, 19938, 19940, 19941, 19943, 19946, 19947, 19950, 19951, 19952, 19953, 19954, 19958, 19959, 19962, 19964, 19966, 19967, 19968, 19970, 19971, 19972, 19976, 19979, 19980, 19981, 19983, 19984, 19988, 19989, 19990, 19991, 19993, 19995, 19996, 19997, 19999]\n",
+      "Indices of data-points that are subject to a run: [26, 27, 142, 143, 144, 145, 146, 147, 162, 220, 221, 222, 223, 245, 290, 291, 299, 300, 301, 328, 329, 356, 357, 410, 411, 456, 592, 593, 594, 595, 660, 661, 709, 717, 718, 839, 840, 1004, 1005, 1064, 1065, 1066, 1098, 1099, 1147, 1148, 1149, 1150, 1207, 1215, 1216, 1217, 1218, 1319, 1361, 1362, 1461, 1462, 1463, 1583, 1697, 1698, 1699, 1763, 1880, 2246, 2247, 2262, 2476, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2593, 2696, 2697, 2738, 2739, 2740, 2819, 2820, 2821, 2822, 2823, 2855, 2856, 2857, 2858, 2866, 2867, 2908, 2933, 3028, 3029, 3051, 3262, 3263, 3264, 3307, 3308, 3336, 3337, 3421, 3422, 3423, 3505, 3599, 3635, 3817, 4173, 4174, 4328, 4329, 4508, 4547, 4548, 4587, 4659, 4660, 4661, 4662, 4663, 4741, 4799, 4800, 4815, 4816, 4881, 4882, 4883, 4884, 4918, 5058, 5059, 5060, 5061, 5109, 5139, 5183, 5237, 5618, 5676, 5704, 5705, 5828, 6127, 6128, 6129, 6196, 6316, 6368, 6406, 6407, 6670, 6683, 6720, 6855, 6856, 6857, 6858, 6951, 6952, 6953, 6954, 6985, 6986, 6987, 7208, 7209, 7210, 7416, 7417, 7418, 7436, 7474, 7616, 7617, 7738, 7912, 7984, 7985, 7986, 7987, 7988, 7989, 8081, 8326, 8327, 8328, 8329, 8396, 8397, 8433, 8434, 8452, 8453, 8454, 8455, 8456, 8457, 8555, 8556, 8601, 8932, 8933, 8934, 8951, 9001, 9105, 9137, 9138, 9139, 9180, 9521, 9570, 9571, 9590, 9591, 9592, 9593, 9594, 10023, 10024, 10025, 10026, 10202, 10203, 10412, 10497, 10657, 10658, 10659, 10743, 10744, 10830, 10831, 10967, 10968, 10989, 11066, 11067, 11068, 11147, 11148, 11149, 11162, 11263, 11264, 11313, 11314, 11322, 11376, 11377, 11435, 11455, 11456, 11457, 11458, 11459, 11460, 11490, 11491, 11492, 11493, 11494, 11512, 11513, 11528, 11571, 11572, 11624, 11625, 11634, 11699, 11700, 11701, 11702, 11822, 11823, 11824, 11825, 11826, 11834, 11881, 11898, 11899, 11900, 12057, 12058, 12059, 12060, 12061, 12077, 12201, 12326, 12417, 12443, 12444, 12445, 12487, 12488, 12489, 12591, 12592, 12593, 12594, 12602, 12603, 12604, 12656, 12673, 12674, 12754, 12755, 12779, 12780, 12927, 12928, 12936, 13045, 13046, 13047, 13048, 13049, 13050, 13051, 13052, 13177, 13178, 13179, 13219, 13220, 13221, 13222, 13223, 13224, 13275, 13276, 13277, 13278, 13305, 13402, 13437, 13438, 13439, 13653, 13654, 13655, 13656, 13706, 13746, 13747, 13748, 13749, 14029, 14137, 14233, 14234, 14235, 14296, 14385, 14392, 14520, 14521, 14522, 14789, 14790, 14791, 14932, 14933, 15342, 15343, 15344, 15345, 15425, 15426, 15427, 15428, 15527, 15588, 15589, 15590, 15711, 15893, 15943, 15944, 15945, 16267, 16268, 16471, 16472, 16506, 16507, 16508, 16509, 16510, 16523, 16524, 16525, 16563, 16643, 16644, 16690, 16691, 16757, 16758, 16759, 16843, 16902, 16903, 16904, 16935, 16964, 17045, 17244, 17257, 17292, 17410, 17418, 17419, 17500, 17534, 17535, 17585, 17586, 17629, 17687, 17688, 17689, 17690, 17691, 17692, 17899, 17900, 17901, 17956, 17957, 17958, 17959, 18057, 18103, 18104, 18105, 18106, 18107, 18197, 18198, 18284, 18285, 18376, 18392, 18393, 18435, 18646, 18687, 18696, 18697, 18801, 18847, 18944, 18945, 19118, 19205, 19300, 19301, 19487, 19513, 19530, 19531, 19532, 19533, 19549, 19599, 19600, 19601, 19602, 19756, 19774, 19775, 19776, 19821, 19822]\n",
+      "Indices of data-points that are subject to a trend: [4900, 5141, 9473, 19737]\n"
+     ]
+    }
+   ],
+   "source": [
+    "# calculate SPC using Shewart Individual Chart with limits based on the values of the first OK phase\n",
+    "chart = ShewartIndividualsChart(x[:950], x, estimated=False, alpha=0.05)\n",
+    "chart.calculate_spc()\n",
+    "info = chart.stability_info()"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.056703Z",
+     "start_time": "2024-04-12T16:34:03.996293Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Calculate Benchmark\n",
+    "Predicted label: Consider violations of charts as positive and all other datapoints as negative\n",
+    "\n",
+    "\n",
+    "True label: Consider violations of limits as positive and all datapoints within tolerance limits as negative"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "outputs": [],
+   "source": [
+    "violations = list(set().union(*[info['out_of_limits'], info['run'], info['trend']]))\n",
+    "replacements = [0]*len(violations)\n",
+    "y_pred = [1]*len(y_true)\n",
+    "for (index, replacement) in zip(violations, replacements):\n",
+    "        y_pred[index] = replacement\n",
+    "tp = [1 if t == 0 and p == 0 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "fn = [1 if t == 0 and p == 1 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "fp = [1 if t == 1 and p == 0 else 0 for t, p in zip(y_true, y_pred)].count(1)\n",
+    "tn = 0\n",
+    "conf_mat = confusion_matrix(y_true, y_pred, normalize='all')\n",
+    "recall = recall_score(y_true, y_pred, pos_label=0)\n",
+    "precision = precision_score(y_true, y_pred, pos_label=0)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.129144Z",
+     "start_time": "2024-04-12T16:34:04.058826Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "### Display SPC benchmark results\n",
+    "#### Confusion matrix"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "outputs": [
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.Javascript object>",
+      "application/javascript": "/* Put everything inside the global mpl namespace */\n/* global mpl */\nwindow.mpl = {};\n\nmpl.get_websocket_type = function () {\n    if (typeof WebSocket !== 'undefined') {\n        return WebSocket;\n    } else if (typeof MozWebSocket !== 'undefined') {\n        return MozWebSocket;\n    } else {\n        alert(\n            'Your browser does not have WebSocket support. ' +\n                'Please try Chrome, Safari or Firefox ≥ 6. ' +\n                'Firefox 4 and 5 are also supported but you ' +\n                'have to enable WebSockets in about:config.'\n        );\n    }\n};\n\nmpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n    this.id = figure_id;\n\n    this.ws = websocket;\n\n    this.supports_binary = this.ws.binaryType !== undefined;\n\n    if (!this.supports_binary) {\n        var warnings = document.getElementById('mpl-warnings');\n        if (warnings) {\n            warnings.style.display = 'block';\n            warnings.textContent =\n                'This browser does not support binary websocket messages. ' +\n                'Performance may be slow.';\n        }\n    }\n\n    this.imageObj = new Image();\n\n    this.context = undefined;\n    this.message = undefined;\n    this.canvas = undefined;\n    this.rubberband_canvas = undefined;\n    this.rubberband_context = undefined;\n    this.format_dropdown = undefined;\n\n    this.image_mode = 'full';\n\n    this.root = document.createElement('div');\n    this.root.setAttribute('style', 'display: inline-block');\n    this._root_extra_style(this.root);\n\n    parent_element.appendChild(this.root);\n\n    this._init_header(this);\n    this._init_canvas(this);\n    this._init_toolbar(this);\n\n    var fig = this;\n\n    this.waiting = false;\n\n    this.ws.onopen = function () {\n        fig.send_message('supports_binary', { value: fig.supports_binary });\n        fig.send_message('send_image_mode', {});\n        if (fig.ratio !== 1) {\n            fig.send_message('set_device_pixel_ratio', {\n                device_pixel_ratio: fig.ratio,\n            });\n        }\n        fig.send_message('refresh', {});\n    };\n\n    this.imageObj.onload = function () {\n        if (fig.image_mode === 'full') {\n            // Full images could contain transparency (where diff images\n            // almost always do), so we need to clear the canvas so that\n            // there is no ghosting.\n            fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n        }\n        fig.context.drawImage(fig.imageObj, 0, 0);\n    };\n\n    this.imageObj.onunload = function () {\n        fig.ws.close();\n    };\n\n    this.ws.onmessage = this._make_on_message_function(this);\n\n    this.ondownload = ondownload;\n};\n\nmpl.figure.prototype._init_header = function () {\n    var titlebar = document.createElement('div');\n    titlebar.classList =\n        'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n    var titletext = document.createElement('div');\n    titletext.classList = 'ui-dialog-title';\n    titletext.setAttribute(\n        'style',\n        'width: 100%; text-align: center; padding: 3px;'\n    );\n    titlebar.appendChild(titletext);\n    this.root.appendChild(titlebar);\n    this.header = titletext;\n};\n\nmpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._init_canvas = function () {\n    var fig = this;\n\n    var canvas_div = (this.canvas_div = document.createElement('div'));\n    canvas_div.setAttribute('tabindex', '0');\n    canvas_div.setAttribute(\n        'style',\n        'border: 1px solid #ddd;' +\n            'box-sizing: content-box;' +\n            'clear: both;' +\n            'min-height: 1px;' +\n            'min-width: 1px;' +\n            'outline: 0;' +\n            'overflow: hidden;' +\n            'position: relative;' +\n            'resize: both;' +\n            'z-index: 2;'\n    );\n\n    function on_keyboard_event_closure(name) {\n        return function (event) {\n            return fig.key_event(event, name);\n        };\n    }\n\n    canvas_div.addEventListener(\n        'keydown',\n        on_keyboard_event_closure('key_press')\n    );\n    canvas_div.addEventListener(\n        'keyup',\n        on_keyboard_event_closure('key_release')\n    );\n\n    this._canvas_extra_style(canvas_div);\n    this.root.appendChild(canvas_div);\n\n    var canvas = (this.canvas = document.createElement('canvas'));\n    canvas.classList.add('mpl-canvas');\n    canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'pointer-events: none;' +\n            'position: relative;' +\n            'z-index: 0;'\n    );\n\n    this.context = canvas.getContext('2d');\n\n    var backingStore =\n        this.context.backingStorePixelRatio ||\n        this.context.webkitBackingStorePixelRatio ||\n        this.context.mozBackingStorePixelRatio ||\n        this.context.msBackingStorePixelRatio ||\n        this.context.oBackingStorePixelRatio ||\n        this.context.backingStorePixelRatio ||\n        1;\n\n    this.ratio = (window.devicePixelRatio || 1) / backingStore;\n\n    var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n        'canvas'\n    ));\n    rubberband_canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'left: 0;' +\n            'pointer-events: none;' +\n            'position: absolute;' +\n            'top: 0;' +\n            'z-index: 1;'\n    );\n\n    // Apply a ponyfill if ResizeObserver is not implemented by browser.\n    if (this.ResizeObserver === undefined) {\n        if (window.ResizeObserver !== undefined) {\n            this.ResizeObserver = window.ResizeObserver;\n        } else {\n            var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n            this.ResizeObserver = obs.ResizeObserver;\n        }\n    }\n\n    this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n        var nentries = entries.length;\n        for (var i = 0; i < nentries; i++) {\n            var entry = entries[i];\n            var width, height;\n            if (entry.contentBoxSize) {\n                if (entry.contentBoxSize instanceof Array) {\n                    // Chrome 84 implements new version of spec.\n                    width = entry.contentBoxSize[0].inlineSize;\n                    height = entry.contentBoxSize[0].blockSize;\n                } else {\n                    // Firefox implements old version of spec.\n                    width = entry.contentBoxSize.inlineSize;\n                    height = entry.contentBoxSize.blockSize;\n                }\n            } else {\n                // Chrome <84 implements even older version of spec.\n                width = entry.contentRect.width;\n                height = entry.contentRect.height;\n            }\n\n            // Keep the size of the canvas and rubber band canvas in sync with\n            // the canvas container.\n            if (entry.devicePixelContentBoxSize) {\n                // Chrome 84 implements new version of spec.\n                canvas.setAttribute(\n                    'width',\n                    entry.devicePixelContentBoxSize[0].inlineSize\n                );\n                canvas.setAttribute(\n                    'height',\n                    entry.devicePixelContentBoxSize[0].blockSize\n                );\n            } else {\n                canvas.setAttribute('width', width * fig.ratio);\n                canvas.setAttribute('height', height * fig.ratio);\n            }\n            /* This rescales the canvas back to display pixels, so that it\n             * appears correct on HiDPI screens. */\n            canvas.style.width = width + 'px';\n            canvas.style.height = height + 'px';\n\n            rubberband_canvas.setAttribute('width', width);\n            rubberband_canvas.setAttribute('height', height);\n\n            // And update the size in Python. We ignore the initial 0/0 size\n            // that occurs as the element is placed into the DOM, which should\n            // otherwise not happen due to the minimum size styling.\n            if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n                fig.request_resize(width, height);\n            }\n        }\n    });\n    this.resizeObserverInstance.observe(canvas_div);\n\n    function on_mouse_event_closure(name) {\n        /* User Agent sniffing is bad, but WebKit is busted:\n         * https://bugs.webkit.org/show_bug.cgi?id=144526\n         * https://bugs.webkit.org/show_bug.cgi?id=181818\n         * The worst that happens here is that they get an extra browser\n         * selection when dragging, if this check fails to catch them.\n         */\n        var UA = navigator.userAgent;\n        var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n        if(isWebKit) {\n            return function (event) {\n                /* This prevents the web browser from automatically changing to\n                 * the text insertion cursor when the button is pressed. We\n                 * want to control all of the cursor setting manually through\n                 * the 'cursor' event from matplotlib */\n                event.preventDefault()\n                return fig.mouse_event(event, name);\n            };\n        } else {\n            return function (event) {\n                return fig.mouse_event(event, name);\n            };\n        }\n    }\n\n    canvas_div.addEventListener(\n        'mousedown',\n        on_mouse_event_closure('button_press')\n    );\n    canvas_div.addEventListener(\n        'mouseup',\n        on_mouse_event_closure('button_release')\n    );\n    canvas_div.addEventListener(\n        'dblclick',\n        on_mouse_event_closure('dblclick')\n    );\n    // Throttle sequential mouse events to 1 every 20ms.\n    canvas_div.addEventListener(\n        'mousemove',\n        on_mouse_event_closure('motion_notify')\n    );\n\n    canvas_div.addEventListener(\n        'mouseenter',\n        on_mouse_event_closure('figure_enter')\n    );\n    canvas_div.addEventListener(\n        'mouseleave',\n        on_mouse_event_closure('figure_leave')\n    );\n\n    canvas_div.addEventListener('wheel', function (event) {\n        if (event.deltaY < 0) {\n            event.step = 1;\n        } else {\n            event.step = -1;\n        }\n        on_mouse_event_closure('scroll')(event);\n    });\n\n    canvas_div.appendChild(canvas);\n    canvas_div.appendChild(rubberband_canvas);\n\n    this.rubberband_context = rubberband_canvas.getContext('2d');\n    this.rubberband_context.strokeStyle = '#000000';\n\n    this._resize_canvas = function (width, height, forward) {\n        if (forward) {\n            canvas_div.style.width = width + 'px';\n            canvas_div.style.height = height + 'px';\n        }\n    };\n\n    // Disable right mouse context menu.\n    canvas_div.addEventListener('contextmenu', function (_e) {\n        event.preventDefault();\n        return false;\n    });\n\n    function set_focus() {\n        canvas.focus();\n        canvas_div.focus();\n    }\n\n    window.setTimeout(set_focus, 100);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'mpl-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'mpl-button-group';\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'mpl-button-group';\n            continue;\n        }\n\n        var button = (fig.buttons[name] = document.createElement('button'));\n        button.classList = 'mpl-widget';\n        button.setAttribute('role', 'button');\n        button.setAttribute('aria-disabled', 'false');\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n\n        var icon_img = document.createElement('img');\n        icon_img.src = '_images/' + image + '.png';\n        icon_img.srcset = '_images/' + image + '_large.png 2x';\n        icon_img.alt = tooltip;\n        button.appendChild(icon_img);\n\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    var fmt_picker = document.createElement('select');\n    fmt_picker.classList = 'mpl-widget';\n    toolbar.appendChild(fmt_picker);\n    this.format_dropdown = fmt_picker;\n\n    for (var ind in mpl.extensions) {\n        var fmt = mpl.extensions[ind];\n        var option = document.createElement('option');\n        option.selected = fmt === mpl.default_extension;\n        option.innerHTML = fmt;\n        fmt_picker.appendChild(option);\n    }\n\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n};\n\nmpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n    // which will in turn request a refresh of the image.\n    this.send_message('resize', { width: x_pixels, height: y_pixels });\n};\n\nmpl.figure.prototype.send_message = function (type, properties) {\n    properties['type'] = type;\n    properties['figure_id'] = this.id;\n    this.ws.send(JSON.stringify(properties));\n};\n\nmpl.figure.prototype.send_draw_message = function () {\n    if (!this.waiting) {\n        this.waiting = true;\n        this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    var format_dropdown = fig.format_dropdown;\n    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n    fig.ondownload(fig, format);\n};\n\nmpl.figure.prototype.handle_resize = function (fig, msg) {\n    var size = msg['size'];\n    if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n        fig._resize_canvas(size[0], size[1], msg['forward']);\n        fig.send_message('refresh', {});\n    }\n};\n\nmpl.figure.prototype.handle_rubberband = function (fig, msg) {\n    var x0 = msg['x0'] / fig.ratio;\n    var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n    var x1 = msg['x1'] / fig.ratio;\n    var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n    x0 = Math.floor(x0) + 0.5;\n    y0 = Math.floor(y0) + 0.5;\n    x1 = Math.floor(x1) + 0.5;\n    y1 = Math.floor(y1) + 0.5;\n    var min_x = Math.min(x0, x1);\n    var min_y = Math.min(y0, y1);\n    var width = Math.abs(x1 - x0);\n    var height = Math.abs(y1 - y0);\n\n    fig.rubberband_context.clearRect(\n        0,\n        0,\n        fig.canvas.width / fig.ratio,\n        fig.canvas.height / fig.ratio\n    );\n\n    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n};\n\nmpl.figure.prototype.handle_figure_label = function (fig, msg) {\n    // Updates the figure title.\n    fig.header.textContent = msg['label'];\n};\n\nmpl.figure.prototype.handle_cursor = function (fig, msg) {\n    fig.canvas_div.style.cursor = msg['cursor'];\n};\n\nmpl.figure.prototype.handle_message = function (fig, msg) {\n    fig.message.textContent = msg['message'];\n};\n\nmpl.figure.prototype.handle_draw = function (fig, _msg) {\n    // Request the server to send over a new figure.\n    fig.send_draw_message();\n};\n\nmpl.figure.prototype.handle_image_mode = function (fig, msg) {\n    fig.image_mode = msg['mode'];\n};\n\nmpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n    for (var key in msg) {\n        if (!(key in fig.buttons)) {\n            continue;\n        }\n        fig.buttons[key].disabled = !msg[key];\n        fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n    }\n};\n\nmpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n    if (msg['mode'] === 'PAN') {\n        fig.buttons['Pan'].classList.add('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    } else if (msg['mode'] === 'ZOOM') {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.add('active');\n    } else {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    }\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Called whenever the canvas gets updated.\n    this.send_message('ack', {});\n};\n\n// A function to construct a web socket function for onmessage handling.\n// Called in the figure constructor.\nmpl.figure.prototype._make_on_message_function = function (fig) {\n    return function socket_on_message(evt) {\n        if (evt.data instanceof Blob) {\n            var img = evt.data;\n            if (img.type !== 'image/png') {\n                /* FIXME: We get \"Resource interpreted as Image but\n                 * transferred with MIME type text/plain:\" errors on\n                 * Chrome.  But how to set the MIME type?  It doesn't seem\n                 * to be part of the websocket stream */\n                img.type = 'image/png';\n            }\n\n            /* Free the memory for the previous frames */\n            if (fig.imageObj.src) {\n                (window.URL || window.webkitURL).revokeObjectURL(\n                    fig.imageObj.src\n                );\n            }\n\n            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n                img\n            );\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        } else if (\n            typeof evt.data === 'string' &&\n            evt.data.slice(0, 21) === 'data:image/png;base64'\n        ) {\n            fig.imageObj.src = evt.data;\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        }\n\n        var msg = JSON.parse(evt.data);\n        var msg_type = msg['type'];\n\n        // Call the  \"handle_{type}\" callback, which takes\n        // the figure and JSON message as its only arguments.\n        try {\n            var callback = fig['handle_' + msg_type];\n        } catch (e) {\n            console.log(\n                \"No handler for the '\" + msg_type + \"' message type: \",\n                msg\n            );\n            return;\n        }\n\n        if (callback) {\n            try {\n                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n                callback(fig, msg);\n            } catch (e) {\n                console.log(\n                    \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n                    e,\n                    e.stack,\n                    msg\n                );\n            }\n        }\n    };\n};\n\nfunction getModifiers(event) {\n    var mods = [];\n    if (event.ctrlKey) {\n        mods.push('ctrl');\n    }\n    if (event.altKey) {\n        mods.push('alt');\n    }\n    if (event.shiftKey) {\n        mods.push('shift');\n    }\n    if (event.metaKey) {\n        mods.push('meta');\n    }\n    return mods;\n}\n\n/*\n * return a copy of an object with only non-object keys\n * we need this to avoid circular references\n * https://stackoverflow.com/a/24161582/3208463\n */\nfunction simpleKeys(original) {\n    return Object.keys(original).reduce(function (obj, key) {\n        if (typeof original[key] !== 'object') {\n            obj[key] = original[key];\n        }\n        return obj;\n    }, {});\n}\n\nmpl.figure.prototype.mouse_event = function (event, name) {\n    if (name === 'button_press') {\n        this.canvas.focus();\n        this.canvas_div.focus();\n    }\n\n    // from https://stackoverflow.com/q/1114465\n    var boundingRect = this.canvas.getBoundingClientRect();\n    var x = (event.clientX - boundingRect.left) * this.ratio;\n    var y = (event.clientY - boundingRect.top) * this.ratio;\n\n    this.send_message(name, {\n        x: x,\n        y: y,\n        button: event.button,\n        step: event.step,\n        modifiers: getModifiers(event),\n        guiEvent: simpleKeys(event),\n    });\n\n    return false;\n};\n\nmpl.figure.prototype._key_event_extra = function (_event, _name) {\n    // Handle any extra behaviour associated with a key event\n};\n\nmpl.figure.prototype.key_event = function (event, name) {\n    // Prevent repeat events\n    if (name === 'key_press') {\n        if (event.key === this._key) {\n            return;\n        } else {\n            this._key = event.key;\n        }\n    }\n    if (name === 'key_release') {\n        this._key = null;\n    }\n\n    var value = '';\n    if (event.ctrlKey && event.key !== 'Control') {\n        value += 'ctrl+';\n    }\n    else if (event.altKey && event.key !== 'Alt') {\n        value += 'alt+';\n    }\n    else if (event.shiftKey && event.key !== 'Shift') {\n        value += 'shift+';\n    }\n\n    value += 'k' + event.key;\n\n    this._key_event_extra(event, name);\n\n    this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n    return false;\n};\n\nmpl.figure.prototype.toolbar_button_onclick = function (name) {\n    if (name === 'download') {\n        this.handle_save(this, null);\n    } else {\n        this.send_message('toolbar_button', { name: name });\n    }\n};\n\nmpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n    this.message.textContent = tooltip;\n};\n\n///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n// prettier-ignore\nvar _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\nmpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n\nmpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n\nmpl.default_extension = \"png\";/* global mpl */\n\nvar comm_websocket_adapter = function (comm) {\n    // Create a \"websocket\"-like object which calls the given IPython comm\n    // object with the appropriate methods. Currently this is a non binary\n    // socket, so there is still some room for performance tuning.\n    var ws = {};\n\n    ws.binaryType = comm.kernel.ws.binaryType;\n    ws.readyState = comm.kernel.ws.readyState;\n    function updateReadyState(_event) {\n        if (comm.kernel.ws) {\n            ws.readyState = comm.kernel.ws.readyState;\n        } else {\n            ws.readyState = 3; // Closed state.\n        }\n    }\n    comm.kernel.ws.addEventListener('open', updateReadyState);\n    comm.kernel.ws.addEventListener('close', updateReadyState);\n    comm.kernel.ws.addEventListener('error', updateReadyState);\n\n    ws.close = function () {\n        comm.close();\n    };\n    ws.send = function (m) {\n        //console.log('sending', m);\n        comm.send(m);\n    };\n    // Register the callback with on_msg.\n    comm.on_msg(function (msg) {\n        //console.log('receiving', msg['content']['data'], msg);\n        var data = msg['content']['data'];\n        if (data['blob'] !== undefined) {\n            data = {\n                data: new Blob(msg['buffers'], { type: data['blob'] }),\n            };\n        }\n        // Pass the mpl event to the overridden (by mpl) onmessage function.\n        ws.onmessage(data);\n    });\n    return ws;\n};\n\nmpl.mpl_figure_comm = function (comm, msg) {\n    // This is the function which gets called when the mpl process\n    // starts-up an IPython Comm through the \"matplotlib\" channel.\n\n    var id = msg.content.data.id;\n    // Get hold of the div created by the display call when the Comm\n    // socket was opened in Python.\n    var element = document.getElementById(id);\n    var ws_proxy = comm_websocket_adapter(comm);\n\n    function ondownload(figure, _format) {\n        window.open(figure.canvas.toDataURL());\n    }\n\n    var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n\n    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n    // web socket which is closed, not our websocket->open comm proxy.\n    ws_proxy.onopen();\n\n    fig.parent_element = element;\n    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n    if (!fig.cell_info) {\n        console.error('Failed to find cell for figure', id, fig);\n        return;\n    }\n    fig.cell_info[0].output_area.element.on(\n        'cleared',\n        { fig: fig },\n        fig._remove_fig_handler\n    );\n};\n\nmpl.figure.prototype.handle_close = function (fig, msg) {\n    var width = fig.canvas.width / fig.ratio;\n    fig.cell_info[0].output_area.element.off(\n        'cleared',\n        fig._remove_fig_handler\n    );\n    fig.resizeObserverInstance.unobserve(fig.canvas_div);\n\n    // Update the output cell to use the data from the current canvas.\n    fig.push_to_output();\n    var dataURL = fig.canvas.toDataURL();\n    // Re-enable the keyboard manager in IPython - without this line, in FF,\n    // the notebook keyboard shortcuts fail.\n    IPython.keyboard_manager.enable();\n    fig.parent_element.innerHTML =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n    fig.close_ws(fig, msg);\n};\n\nmpl.figure.prototype.close_ws = function (fig, msg) {\n    fig.send_message('closing', msg);\n    // fig.ws.close()\n};\n\nmpl.figure.prototype.push_to_output = function (_remove_interactive) {\n    // Turn the data on the canvas into data in the output cell.\n    var width = this.canvas.width / this.ratio;\n    var dataURL = this.canvas.toDataURL();\n    this.cell_info[1]['text/html'] =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Tell IPython that the notebook contents must change.\n    IPython.notebook.set_dirty(true);\n    this.send_message('ack', {});\n    var fig = this;\n    // Wait a second, then push the new image to the DOM so\n    // that it is saved nicely (might be nice to debounce this).\n    setTimeout(function () {\n        fig.push_to_output();\n    }, 1000);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'btn-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'btn-group';\n    var button;\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'btn-group';\n            continue;\n        }\n\n        button = fig.buttons[name] = document.createElement('button');\n        button.classList = 'btn btn-default';\n        button.href = '#';\n        button.title = name;\n        button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    // Add the status bar.\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message pull-right';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n\n    // Add the close button to the window.\n    var buttongrp = document.createElement('div');\n    buttongrp.classList = 'btn-group inline pull-right';\n    button = document.createElement('button');\n    button.classList = 'btn btn-mini btn-primary';\n    button.href = '#';\n    button.title = 'Stop Interaction';\n    button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n    button.addEventListener('click', function (_evt) {\n        fig.handle_close(fig, {});\n    });\n    button.addEventListener(\n        'mouseover',\n        on_mouseover_closure('Stop Interaction')\n    );\n    buttongrp.appendChild(button);\n    var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n    titlebar.insertBefore(buttongrp, titlebar.firstChild);\n};\n\nmpl.figure.prototype._remove_fig_handler = function (event) {\n    var fig = event.data.fig;\n    if (event.target !== this) {\n        // Ignore bubbled events from children.\n        return;\n    }\n    fig.close_ws(fig, {});\n};\n\nmpl.figure.prototype._root_extra_style = function (el) {\n    el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n};\n\nmpl.figure.prototype._canvas_extra_style = function (el) {\n    // this is important to make the div 'focusable\n    el.setAttribute('tabindex', 0);\n    // reach out to IPython and tell the keyboard manager to turn it's self\n    // off when our div gets focus\n\n    // location in version 3\n    if (IPython.notebook.keyboard_manager) {\n        IPython.notebook.keyboard_manager.register_events(el);\n    } else {\n        // location in version 2\n        IPython.keyboard_manager.register_events(el);\n    }\n};\n\nmpl.figure.prototype._key_event_extra = function (event, _name) {\n    // Check for shift+enter\n    if (event.shiftKey && event.which === 13) {\n        this.canvas_div.blur();\n        // select the cell after this one\n        var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n        IPython.notebook.select(index + 1);\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    fig.ondownload(fig, null);\n};\n\nmpl.find_output_cell = function (html_output) {\n    // Return the cell and output element which can be found *uniquely* in the notebook.\n    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n    // IPython event is triggered only after the cells have been serialised, which for\n    // our purposes (turning an active figure into a static one), is too late.\n    var cells = IPython.notebook.get_cells();\n    var ncells = cells.length;\n    for (var i = 0; i < ncells; i++) {\n        var cell = cells[i];\n        if (cell.cell_type === 'code') {\n            for (var j = 0; j < cell.output_area.outputs.length; j++) {\n                var data = cell.output_area.outputs[j];\n                if (data.data) {\n                    // IPython >= 3 moved mimebundle to data attribute of output\n                    data = data.data;\n                }\n                if (data['text/html'] === html_output) {\n                    return [cell, data, j];\n                }\n            }\n        }\n    }\n};\n\n// Register the function which deals with the matplotlib target/channel.\n// The kernel may be null if the page has been refreshed.\nif (IPython.notebook.kernel !== null) {\n    IPython.notebook.kernel.comm_manager.register_target(\n        'matplotlib',\n        mpl.mpl_figure_comm\n    );\n}\n"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.HTML object>",
+      "text/html": "<div id='86e69fa5-e628-49ff-86b8-85e7fae7d4bd'></div>"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True positive: 2887\n",
+      "False negative: 0\n",
+      "False positive: 2173\n",
+      "True negative: Not defined\n"
+     ]
+    }
+   ],
+   "source": [
+    "disp = ConfusionMatrixDisplay(conf_mat)\n",
+    "disp.plot()\n",
+    "plt.grid(False)\n",
+    "plt.show()\n",
+    "\n",
+    "print(f'True positive: {tp}')\n",
+    "print(f'False negative: {fn}')\n",
+    "print(f'False positive: {fp}')\n",
+    "print(f'True negative: Not defined')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.176389Z",
+     "start_time": "2024-04-12T16:34:04.130801Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "#### Precision, recall and F1-score"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recall: 1.0\n",
+      "Precision: 0.5705533596837945\n",
+      "F1-score: 0.7265634830753743\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f'Recall: {recall}')\n",
+    "print(f'Precision: {precision}')\n",
+    "print(f'F1-score: {2*precision*recall/(precision+recall)}')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.192104Z",
+     "start_time": "2024-04-12T16:34:04.179373Z"
+    }
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "# Benchmark Convolutional Neural Network"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "markdown",
+   "source": [
+    "True label: Positive (0) when there is a limit violation within the time window (In case of synthetic generated data: and if any value of the input sliding window stems from NOK phase)\n",
+    "Predicted: Positive (0) when there is a signal\n",
+    "\n",
+    "This results in the following scheme for the confusion matrix:\n",
+    "Synthetic data:\n",
+    "\n",
+    "| --- | --- | --- | --- |\n",
+    "| Case | A | B | C |\n",
+    "| True Positive | 0 | 0 | 0 |\n",
+    "| False Positive | 0 | 1 | 0/1 |\n",
+    "|  | 0 | 0 | 1 |\n",
+    "| False Negative | 1 | 0 | 0 |\n",
+    "\n",
+    "Real data (NOK phase unknown):\n",
+    "\n",
+    "| --- | --- | --- |\n",
+    "| Case | A | C |\n",
+    "| True Positive | 0 | 0 |\n",
+    "| False Positive | 0 | 1 |\n",
+    "| False Negative | 1 | 0 |\n",
+    "\n",
+    "\n",
+    "True Negative is not defined. The reason for this is provided in the paper.\n",
+    "\n",
+    "Process / Prediction is OK?\n",
+    "True = 1\n",
+    "False = 0\n",
+    "\n",
+    "Assumption: Actual NOK-Phase where sum of weights of NOK distributions >= 0.5\n",
+    "\n",
+    "A: Predicted (Signal from CNN?):\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | anomaly score meets or exceeds threshold |\n",
+    "| 1 | anomaly score below threshold|\n",
+    "\n",
+    "B: Datapoint from NOK phase?:\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | at least one data point in sliding window is from NOK phase |\n",
+    "| 1 | all datapoints in sliding window stem from OK phase |\n",
+    "\n",
+    "C: Violation of tolerance limits?:\n",
+    "\n",
+    "| --- | --- |\n",
+    "| 0 | at least one violation of tolerance limits within sliding window |\n",
+    "| 1 | no violation of tolerance limits within sliding window |"
+   ],
+   "metadata": {
+    "collapsed": false
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 23,
+   "outputs": [],
+   "source": [
+    "PTH_OBS = \"../../data/test/x.csv\"\n",
+    "PTH_INSP = \"../../data/test/y.csv\"\n",
+    "PTH_KMAT = \"../../data/test/k_matrix.csv\"\n",
+    "PTH_PHASE = \"../../data/test/phase.csv\"\n",
+    "OBS_WIN = 18\n",
+    "RAMP_LEN = 3\n",
+    "ANOM_SCALE = 0.5\n",
+    "ANOM_THRESHOLD = 0.1\n",
+    "\n",
+    "n = 18"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.207230Z",
+     "start_time": "2024-04-12T16:34:04.195398Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "outputs": [],
+   "source": [
+    "# Preparations and B (phase_info)\n",
+    "kmat = pd.read_csv(PTH_KMAT, header=None)\n",
+    "phase = (kmat.iloc[:, -1] < 0.000001).to_numpy()\n",
+    "np.savetxt(PTH_PHASE, phase, delimiter=',')\n",
+    "def load_fn(x):\n",
+    "    return pd.read_csv(x, header=None).to_numpy().flatten()\n",
+    "x, y, phase_info = prepare_benchmark_data(\n",
+    "        pth_observation=PTH_OBS,\n",
+    "        pth_inspect=PTH_INSP,\n",
+    "        pth_phase=PTH_KMAT,\n",
+    "        load_fn=load_fn,\n",
+    "        observation_length=OBS_WIN,\n",
+    "        ramp_length=RAMP_LEN,\n",
+    "        anomaly_scale=ANOM_SCALE,\n",
+    "    )"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:04.612353Z",
+     "start_time": "2024-04-12T16:34:04.209432Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 25,
+   "outputs": [],
+   "source": [
+    "# load model\n",
+    "custom_objects = {\n",
+    "        'MeanSubLayer': MeanSubLayer,\n",
+    "        \"ResNetIdentityBlock\": ResNetIdentityBlock,\n",
+    "        \"ResNetConvBlock\": ResNetConvBlock,\n",
+    "        \"rebalanced_nok_mse\": rebalanced_nok_mse,\n",
+    "}\n",
+    "model = keras.models.load_model(\"../model_training/ResNet/model/best_model.h5\", custom_objects=custom_objects)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:05.844474Z",
+     "start_time": "2024-04-12T16:34:04.613387Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 26,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "625/625 [==============================] - 8s 12ms/step\n"
+     ]
+    }
+   ],
+   "source": [
+    "# make predictions\n",
+    "ys = model.predict(x)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.273434Z",
+     "start_time": "2024-04-12T16:34:05.845554Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 27,
+   "outputs": [],
+   "source": [
+    "# A\n",
+    "y_pred = np.array(ys)\n",
+    "# Process OK? True->1, False->0\n",
+    "pred = np.where(y_pred < ANOM_THRESHOLD, 1, 0)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.288911Z",
+     "start_time": "2024-04-12T16:34:14.276626Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "outputs": [],
+   "source": [
+    "# C\n",
+    "limit_violation = []\n",
+    "\n",
+    "for window in y:\n",
+    "        # only the last n steps of time window considered\n",
+    "        limit_violation.append(not window[-n:].any())"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.397253Z",
+     "start_time": "2024-04-12T16:34:14.293208Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "outputs": [
+    {
+     "data": {
+      "text/plain": "Counter({True: 10645, False: 9337})"
+     },
+     "execution_count": 29,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from collections import Counter\n",
+    "Counter(limit_violation)"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.427637Z",
+     "start_time": "2024-04-12T16:34:14.399633Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "outputs": [],
+   "source": [
+    "if len(phase_info.shape)== 1:\n",
+    "    tp = [1 if a == 0 and b == 0 and c == 0 else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "    fn = [1 if a == 1 and b == 0 and c == 0 else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "    fp = [1 if (a == 0 and b == 1 ) or (a == 0 and b == 0 and c == 1) else 0 for a, b, c in zip(pred, phase_info, limit_violation)].count(1)\n",
+    "elif len(phase_info.shape)== 0:\n",
+    "    tp = [1 if a == 0 and c == 0 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "    fn = [1 if a == 1 and c == 0 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "    fp = [1 if a == 0 and c == 1 else 0 for a, c in zip(pred, limit_violation)].count(1)\n",
+    "else:\n",
+    "    raise ValueError('The given format of phase_info is not supported. It has to be a 0- or 1-dimensional numpy array.')\n",
+    "\n",
+    "tn = 0\n",
+    "n_total = ys.shape[0]"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.646138Z",
+     "start_time": "2024-04-12T16:34:14.430891Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 31,
+   "outputs": [
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.Javascript object>",
+      "application/javascript": "/* Put everything inside the global mpl namespace */\n/* global mpl */\nwindow.mpl = {};\n\nmpl.get_websocket_type = function () {\n    if (typeof WebSocket !== 'undefined') {\n        return WebSocket;\n    } else if (typeof MozWebSocket !== 'undefined') {\n        return MozWebSocket;\n    } else {\n        alert(\n            'Your browser does not have WebSocket support. ' +\n                'Please try Chrome, Safari or Firefox ≥ 6. ' +\n                'Firefox 4 and 5 are also supported but you ' +\n                'have to enable WebSockets in about:config.'\n        );\n    }\n};\n\nmpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n    this.id = figure_id;\n\n    this.ws = websocket;\n\n    this.supports_binary = this.ws.binaryType !== undefined;\n\n    if (!this.supports_binary) {\n        var warnings = document.getElementById('mpl-warnings');\n        if (warnings) {\n            warnings.style.display = 'block';\n            warnings.textContent =\n                'This browser does not support binary websocket messages. ' +\n                'Performance may be slow.';\n        }\n    }\n\n    this.imageObj = new Image();\n\n    this.context = undefined;\n    this.message = undefined;\n    this.canvas = undefined;\n    this.rubberband_canvas = undefined;\n    this.rubberband_context = undefined;\n    this.format_dropdown = undefined;\n\n    this.image_mode = 'full';\n\n    this.root = document.createElement('div');\n    this.root.setAttribute('style', 'display: inline-block');\n    this._root_extra_style(this.root);\n\n    parent_element.appendChild(this.root);\n\n    this._init_header(this);\n    this._init_canvas(this);\n    this._init_toolbar(this);\n\n    var fig = this;\n\n    this.waiting = false;\n\n    this.ws.onopen = function () {\n        fig.send_message('supports_binary', { value: fig.supports_binary });\n        fig.send_message('send_image_mode', {});\n        if (fig.ratio !== 1) {\n            fig.send_message('set_device_pixel_ratio', {\n                device_pixel_ratio: fig.ratio,\n            });\n        }\n        fig.send_message('refresh', {});\n    };\n\n    this.imageObj.onload = function () {\n        if (fig.image_mode === 'full') {\n            // Full images could contain transparency (where diff images\n            // almost always do), so we need to clear the canvas so that\n            // there is no ghosting.\n            fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n        }\n        fig.context.drawImage(fig.imageObj, 0, 0);\n    };\n\n    this.imageObj.onunload = function () {\n        fig.ws.close();\n    };\n\n    this.ws.onmessage = this._make_on_message_function(this);\n\n    this.ondownload = ondownload;\n};\n\nmpl.figure.prototype._init_header = function () {\n    var titlebar = document.createElement('div');\n    titlebar.classList =\n        'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n    var titletext = document.createElement('div');\n    titletext.classList = 'ui-dialog-title';\n    titletext.setAttribute(\n        'style',\n        'width: 100%; text-align: center; padding: 3px;'\n    );\n    titlebar.appendChild(titletext);\n    this.root.appendChild(titlebar);\n    this.header = titletext;\n};\n\nmpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n\nmpl.figure.prototype._init_canvas = function () {\n    var fig = this;\n\n    var canvas_div = (this.canvas_div = document.createElement('div'));\n    canvas_div.setAttribute('tabindex', '0');\n    canvas_div.setAttribute(\n        'style',\n        'border: 1px solid #ddd;' +\n            'box-sizing: content-box;' +\n            'clear: both;' +\n            'min-height: 1px;' +\n            'min-width: 1px;' +\n            'outline: 0;' +\n            'overflow: hidden;' +\n            'position: relative;' +\n            'resize: both;' +\n            'z-index: 2;'\n    );\n\n    function on_keyboard_event_closure(name) {\n        return function (event) {\n            return fig.key_event(event, name);\n        };\n    }\n\n    canvas_div.addEventListener(\n        'keydown',\n        on_keyboard_event_closure('key_press')\n    );\n    canvas_div.addEventListener(\n        'keyup',\n        on_keyboard_event_closure('key_release')\n    );\n\n    this._canvas_extra_style(canvas_div);\n    this.root.appendChild(canvas_div);\n\n    var canvas = (this.canvas = document.createElement('canvas'));\n    canvas.classList.add('mpl-canvas');\n    canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'pointer-events: none;' +\n            'position: relative;' +\n            'z-index: 0;'\n    );\n\n    this.context = canvas.getContext('2d');\n\n    var backingStore =\n        this.context.backingStorePixelRatio ||\n        this.context.webkitBackingStorePixelRatio ||\n        this.context.mozBackingStorePixelRatio ||\n        this.context.msBackingStorePixelRatio ||\n        this.context.oBackingStorePixelRatio ||\n        this.context.backingStorePixelRatio ||\n        1;\n\n    this.ratio = (window.devicePixelRatio || 1) / backingStore;\n\n    var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n        'canvas'\n    ));\n    rubberband_canvas.setAttribute(\n        'style',\n        'box-sizing: content-box;' +\n            'left: 0;' +\n            'pointer-events: none;' +\n            'position: absolute;' +\n            'top: 0;' +\n            'z-index: 1;'\n    );\n\n    // Apply a ponyfill if ResizeObserver is not implemented by browser.\n    if (this.ResizeObserver === undefined) {\n        if (window.ResizeObserver !== undefined) {\n            this.ResizeObserver = window.ResizeObserver;\n        } else {\n            var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n            this.ResizeObserver = obs.ResizeObserver;\n        }\n    }\n\n    this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n        var nentries = entries.length;\n        for (var i = 0; i < nentries; i++) {\n            var entry = entries[i];\n            var width, height;\n            if (entry.contentBoxSize) {\n                if (entry.contentBoxSize instanceof Array) {\n                    // Chrome 84 implements new version of spec.\n                    width = entry.contentBoxSize[0].inlineSize;\n                    height = entry.contentBoxSize[0].blockSize;\n                } else {\n                    // Firefox implements old version of spec.\n                    width = entry.contentBoxSize.inlineSize;\n                    height = entry.contentBoxSize.blockSize;\n                }\n            } else {\n                // Chrome <84 implements even older version of spec.\n                width = entry.contentRect.width;\n                height = entry.contentRect.height;\n            }\n\n            // Keep the size of the canvas and rubber band canvas in sync with\n            // the canvas container.\n            if (entry.devicePixelContentBoxSize) {\n                // Chrome 84 implements new version of spec.\n                canvas.setAttribute(\n                    'width',\n                    entry.devicePixelContentBoxSize[0].inlineSize\n                );\n                canvas.setAttribute(\n                    'height',\n                    entry.devicePixelContentBoxSize[0].blockSize\n                );\n            } else {\n                canvas.setAttribute('width', width * fig.ratio);\n                canvas.setAttribute('height', height * fig.ratio);\n            }\n            /* This rescales the canvas back to display pixels, so that it\n             * appears correct on HiDPI screens. */\n            canvas.style.width = width + 'px';\n            canvas.style.height = height + 'px';\n\n            rubberband_canvas.setAttribute('width', width);\n            rubberband_canvas.setAttribute('height', height);\n\n            // And update the size in Python. We ignore the initial 0/0 size\n            // that occurs as the element is placed into the DOM, which should\n            // otherwise not happen due to the minimum size styling.\n            if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n                fig.request_resize(width, height);\n            }\n        }\n    });\n    this.resizeObserverInstance.observe(canvas_div);\n\n    function on_mouse_event_closure(name) {\n        /* User Agent sniffing is bad, but WebKit is busted:\n         * https://bugs.webkit.org/show_bug.cgi?id=144526\n         * https://bugs.webkit.org/show_bug.cgi?id=181818\n         * The worst that happens here is that they get an extra browser\n         * selection when dragging, if this check fails to catch them.\n         */\n        var UA = navigator.userAgent;\n        var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA);\n        if(isWebKit) {\n            return function (event) {\n                /* This prevents the web browser from automatically changing to\n                 * the text insertion cursor when the button is pressed. We\n                 * want to control all of the cursor setting manually through\n                 * the 'cursor' event from matplotlib */\n                event.preventDefault()\n                return fig.mouse_event(event, name);\n            };\n        } else {\n            return function (event) {\n                return fig.mouse_event(event, name);\n            };\n        }\n    }\n\n    canvas_div.addEventListener(\n        'mousedown',\n        on_mouse_event_closure('button_press')\n    );\n    canvas_div.addEventListener(\n        'mouseup',\n        on_mouse_event_closure('button_release')\n    );\n    canvas_div.addEventListener(\n        'dblclick',\n        on_mouse_event_closure('dblclick')\n    );\n    // Throttle sequential mouse events to 1 every 20ms.\n    canvas_div.addEventListener(\n        'mousemove',\n        on_mouse_event_closure('motion_notify')\n    );\n\n    canvas_div.addEventListener(\n        'mouseenter',\n        on_mouse_event_closure('figure_enter')\n    );\n    canvas_div.addEventListener(\n        'mouseleave',\n        on_mouse_event_closure('figure_leave')\n    );\n\n    canvas_div.addEventListener('wheel', function (event) {\n        if (event.deltaY < 0) {\n            event.step = 1;\n        } else {\n            event.step = -1;\n        }\n        on_mouse_event_closure('scroll')(event);\n    });\n\n    canvas_div.appendChild(canvas);\n    canvas_div.appendChild(rubberband_canvas);\n\n    this.rubberband_context = rubberband_canvas.getContext('2d');\n    this.rubberband_context.strokeStyle = '#000000';\n\n    this._resize_canvas = function (width, height, forward) {\n        if (forward) {\n            canvas_div.style.width = width + 'px';\n            canvas_div.style.height = height + 'px';\n        }\n    };\n\n    // Disable right mouse context menu.\n    canvas_div.addEventListener('contextmenu', function (_e) {\n        event.preventDefault();\n        return false;\n    });\n\n    function set_focus() {\n        canvas.focus();\n        canvas_div.focus();\n    }\n\n    window.setTimeout(set_focus, 100);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'mpl-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'mpl-button-group';\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'mpl-button-group';\n            continue;\n        }\n\n        var button = (fig.buttons[name] = document.createElement('button'));\n        button.classList = 'mpl-widget';\n        button.setAttribute('role', 'button');\n        button.setAttribute('aria-disabled', 'false');\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n\n        var icon_img = document.createElement('img');\n        icon_img.src = '_images/' + image + '.png';\n        icon_img.srcset = '_images/' + image + '_large.png 2x';\n        icon_img.alt = tooltip;\n        button.appendChild(icon_img);\n\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    var fmt_picker = document.createElement('select');\n    fmt_picker.classList = 'mpl-widget';\n    toolbar.appendChild(fmt_picker);\n    this.format_dropdown = fmt_picker;\n\n    for (var ind in mpl.extensions) {\n        var fmt = mpl.extensions[ind];\n        var option = document.createElement('option');\n        option.selected = fmt === mpl.default_extension;\n        option.innerHTML = fmt;\n        fmt_picker.appendChild(option);\n    }\n\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n};\n\nmpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n    // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n    // which will in turn request a refresh of the image.\n    this.send_message('resize', { width: x_pixels, height: y_pixels });\n};\n\nmpl.figure.prototype.send_message = function (type, properties) {\n    properties['type'] = type;\n    properties['figure_id'] = this.id;\n    this.ws.send(JSON.stringify(properties));\n};\n\nmpl.figure.prototype.send_draw_message = function () {\n    if (!this.waiting) {\n        this.waiting = true;\n        this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    var format_dropdown = fig.format_dropdown;\n    var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n    fig.ondownload(fig, format);\n};\n\nmpl.figure.prototype.handle_resize = function (fig, msg) {\n    var size = msg['size'];\n    if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n        fig._resize_canvas(size[0], size[1], msg['forward']);\n        fig.send_message('refresh', {});\n    }\n};\n\nmpl.figure.prototype.handle_rubberband = function (fig, msg) {\n    var x0 = msg['x0'] / fig.ratio;\n    var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n    var x1 = msg['x1'] / fig.ratio;\n    var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n    x0 = Math.floor(x0) + 0.5;\n    y0 = Math.floor(y0) + 0.5;\n    x1 = Math.floor(x1) + 0.5;\n    y1 = Math.floor(y1) + 0.5;\n    var min_x = Math.min(x0, x1);\n    var min_y = Math.min(y0, y1);\n    var width = Math.abs(x1 - x0);\n    var height = Math.abs(y1 - y0);\n\n    fig.rubberband_context.clearRect(\n        0,\n        0,\n        fig.canvas.width / fig.ratio,\n        fig.canvas.height / fig.ratio\n    );\n\n    fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n};\n\nmpl.figure.prototype.handle_figure_label = function (fig, msg) {\n    // Updates the figure title.\n    fig.header.textContent = msg['label'];\n};\n\nmpl.figure.prototype.handle_cursor = function (fig, msg) {\n    fig.canvas_div.style.cursor = msg['cursor'];\n};\n\nmpl.figure.prototype.handle_message = function (fig, msg) {\n    fig.message.textContent = msg['message'];\n};\n\nmpl.figure.prototype.handle_draw = function (fig, _msg) {\n    // Request the server to send over a new figure.\n    fig.send_draw_message();\n};\n\nmpl.figure.prototype.handle_image_mode = function (fig, msg) {\n    fig.image_mode = msg['mode'];\n};\n\nmpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n    for (var key in msg) {\n        if (!(key in fig.buttons)) {\n            continue;\n        }\n        fig.buttons[key].disabled = !msg[key];\n        fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n    }\n};\n\nmpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n    if (msg['mode'] === 'PAN') {\n        fig.buttons['Pan'].classList.add('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    } else if (msg['mode'] === 'ZOOM') {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.add('active');\n    } else {\n        fig.buttons['Pan'].classList.remove('active');\n        fig.buttons['Zoom'].classList.remove('active');\n    }\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Called whenever the canvas gets updated.\n    this.send_message('ack', {});\n};\n\n// A function to construct a web socket function for onmessage handling.\n// Called in the figure constructor.\nmpl.figure.prototype._make_on_message_function = function (fig) {\n    return function socket_on_message(evt) {\n        if (evt.data instanceof Blob) {\n            var img = evt.data;\n            if (img.type !== 'image/png') {\n                /* FIXME: We get \"Resource interpreted as Image but\n                 * transferred with MIME type text/plain:\" errors on\n                 * Chrome.  But how to set the MIME type?  It doesn't seem\n                 * to be part of the websocket stream */\n                img.type = 'image/png';\n            }\n\n            /* Free the memory for the previous frames */\n            if (fig.imageObj.src) {\n                (window.URL || window.webkitURL).revokeObjectURL(\n                    fig.imageObj.src\n                );\n            }\n\n            fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n                img\n            );\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        } else if (\n            typeof evt.data === 'string' &&\n            evt.data.slice(0, 21) === 'data:image/png;base64'\n        ) {\n            fig.imageObj.src = evt.data;\n            fig.updated_canvas_event();\n            fig.waiting = false;\n            return;\n        }\n\n        var msg = JSON.parse(evt.data);\n        var msg_type = msg['type'];\n\n        // Call the  \"handle_{type}\" callback, which takes\n        // the figure and JSON message as its only arguments.\n        try {\n            var callback = fig['handle_' + msg_type];\n        } catch (e) {\n            console.log(\n                \"No handler for the '\" + msg_type + \"' message type: \",\n                msg\n            );\n            return;\n        }\n\n        if (callback) {\n            try {\n                // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n                callback(fig, msg);\n            } catch (e) {\n                console.log(\n                    \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n                    e,\n                    e.stack,\n                    msg\n                );\n            }\n        }\n    };\n};\n\nfunction getModifiers(event) {\n    var mods = [];\n    if (event.ctrlKey) {\n        mods.push('ctrl');\n    }\n    if (event.altKey) {\n        mods.push('alt');\n    }\n    if (event.shiftKey) {\n        mods.push('shift');\n    }\n    if (event.metaKey) {\n        mods.push('meta');\n    }\n    return mods;\n}\n\n/*\n * return a copy of an object with only non-object keys\n * we need this to avoid circular references\n * https://stackoverflow.com/a/24161582/3208463\n */\nfunction simpleKeys(original) {\n    return Object.keys(original).reduce(function (obj, key) {\n        if (typeof original[key] !== 'object') {\n            obj[key] = original[key];\n        }\n        return obj;\n    }, {});\n}\n\nmpl.figure.prototype.mouse_event = function (event, name) {\n    if (name === 'button_press') {\n        this.canvas.focus();\n        this.canvas_div.focus();\n    }\n\n    // from https://stackoverflow.com/q/1114465\n    var boundingRect = this.canvas.getBoundingClientRect();\n    var x = (event.clientX - boundingRect.left) * this.ratio;\n    var y = (event.clientY - boundingRect.top) * this.ratio;\n\n    this.send_message(name, {\n        x: x,\n        y: y,\n        button: event.button,\n        step: event.step,\n        modifiers: getModifiers(event),\n        guiEvent: simpleKeys(event),\n    });\n\n    return false;\n};\n\nmpl.figure.prototype._key_event_extra = function (_event, _name) {\n    // Handle any extra behaviour associated with a key event\n};\n\nmpl.figure.prototype.key_event = function (event, name) {\n    // Prevent repeat events\n    if (name === 'key_press') {\n        if (event.key === this._key) {\n            return;\n        } else {\n            this._key = event.key;\n        }\n    }\n    if (name === 'key_release') {\n        this._key = null;\n    }\n\n    var value = '';\n    if (event.ctrlKey && event.key !== 'Control') {\n        value += 'ctrl+';\n    }\n    else if (event.altKey && event.key !== 'Alt') {\n        value += 'alt+';\n    }\n    else if (event.shiftKey && event.key !== 'Shift') {\n        value += 'shift+';\n    }\n\n    value += 'k' + event.key;\n\n    this._key_event_extra(event, name);\n\n    this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n    return false;\n};\n\nmpl.figure.prototype.toolbar_button_onclick = function (name) {\n    if (name === 'download') {\n        this.handle_save(this, null);\n    } else {\n        this.send_message('toolbar_button', { name: name });\n    }\n};\n\nmpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n    this.message.textContent = tooltip;\n};\n\n///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n// prettier-ignore\nvar _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\nmpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis\", \"fa fa-square-o\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o\", \"download\"]];\n\nmpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\", \"webp\"];\n\nmpl.default_extension = \"png\";/* global mpl */\n\nvar comm_websocket_adapter = function (comm) {\n    // Create a \"websocket\"-like object which calls the given IPython comm\n    // object with the appropriate methods. Currently this is a non binary\n    // socket, so there is still some room for performance tuning.\n    var ws = {};\n\n    ws.binaryType = comm.kernel.ws.binaryType;\n    ws.readyState = comm.kernel.ws.readyState;\n    function updateReadyState(_event) {\n        if (comm.kernel.ws) {\n            ws.readyState = comm.kernel.ws.readyState;\n        } else {\n            ws.readyState = 3; // Closed state.\n        }\n    }\n    comm.kernel.ws.addEventListener('open', updateReadyState);\n    comm.kernel.ws.addEventListener('close', updateReadyState);\n    comm.kernel.ws.addEventListener('error', updateReadyState);\n\n    ws.close = function () {\n        comm.close();\n    };\n    ws.send = function (m) {\n        //console.log('sending', m);\n        comm.send(m);\n    };\n    // Register the callback with on_msg.\n    comm.on_msg(function (msg) {\n        //console.log('receiving', msg['content']['data'], msg);\n        var data = msg['content']['data'];\n        if (data['blob'] !== undefined) {\n            data = {\n                data: new Blob(msg['buffers'], { type: data['blob'] }),\n            };\n        }\n        // Pass the mpl event to the overridden (by mpl) onmessage function.\n        ws.onmessage(data);\n    });\n    return ws;\n};\n\nmpl.mpl_figure_comm = function (comm, msg) {\n    // This is the function which gets called when the mpl process\n    // starts-up an IPython Comm through the \"matplotlib\" channel.\n\n    var id = msg.content.data.id;\n    // Get hold of the div created by the display call when the Comm\n    // socket was opened in Python.\n    var element = document.getElementById(id);\n    var ws_proxy = comm_websocket_adapter(comm);\n\n    function ondownload(figure, _format) {\n        window.open(figure.canvas.toDataURL());\n    }\n\n    var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n\n    // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n    // web socket which is closed, not our websocket->open comm proxy.\n    ws_proxy.onopen();\n\n    fig.parent_element = element;\n    fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n    if (!fig.cell_info) {\n        console.error('Failed to find cell for figure', id, fig);\n        return;\n    }\n    fig.cell_info[0].output_area.element.on(\n        'cleared',\n        { fig: fig },\n        fig._remove_fig_handler\n    );\n};\n\nmpl.figure.prototype.handle_close = function (fig, msg) {\n    var width = fig.canvas.width / fig.ratio;\n    fig.cell_info[0].output_area.element.off(\n        'cleared',\n        fig._remove_fig_handler\n    );\n    fig.resizeObserverInstance.unobserve(fig.canvas_div);\n\n    // Update the output cell to use the data from the current canvas.\n    fig.push_to_output();\n    var dataURL = fig.canvas.toDataURL();\n    // Re-enable the keyboard manager in IPython - without this line, in FF,\n    // the notebook keyboard shortcuts fail.\n    IPython.keyboard_manager.enable();\n    fig.parent_element.innerHTML =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n    fig.close_ws(fig, msg);\n};\n\nmpl.figure.prototype.close_ws = function (fig, msg) {\n    fig.send_message('closing', msg);\n    // fig.ws.close()\n};\n\nmpl.figure.prototype.push_to_output = function (_remove_interactive) {\n    // Turn the data on the canvas into data in the output cell.\n    var width = this.canvas.width / this.ratio;\n    var dataURL = this.canvas.toDataURL();\n    this.cell_info[1]['text/html'] =\n        '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n};\n\nmpl.figure.prototype.updated_canvas_event = function () {\n    // Tell IPython that the notebook contents must change.\n    IPython.notebook.set_dirty(true);\n    this.send_message('ack', {});\n    var fig = this;\n    // Wait a second, then push the new image to the DOM so\n    // that it is saved nicely (might be nice to debounce this).\n    setTimeout(function () {\n        fig.push_to_output();\n    }, 1000);\n};\n\nmpl.figure.prototype._init_toolbar = function () {\n    var fig = this;\n\n    var toolbar = document.createElement('div');\n    toolbar.classList = 'btn-toolbar';\n    this.root.appendChild(toolbar);\n\n    function on_click_closure(name) {\n        return function (_event) {\n            return fig.toolbar_button_onclick(name);\n        };\n    }\n\n    function on_mouseover_closure(tooltip) {\n        return function (event) {\n            if (!event.currentTarget.disabled) {\n                return fig.toolbar_button_onmouseover(tooltip);\n            }\n        };\n    }\n\n    fig.buttons = {};\n    var buttonGroup = document.createElement('div');\n    buttonGroup.classList = 'btn-group';\n    var button;\n    for (var toolbar_ind in mpl.toolbar_items) {\n        var name = mpl.toolbar_items[toolbar_ind][0];\n        var tooltip = mpl.toolbar_items[toolbar_ind][1];\n        var image = mpl.toolbar_items[toolbar_ind][2];\n        var method_name = mpl.toolbar_items[toolbar_ind][3];\n\n        if (!name) {\n            /* Instead of a spacer, we start a new button group. */\n            if (buttonGroup.hasChildNodes()) {\n                toolbar.appendChild(buttonGroup);\n            }\n            buttonGroup = document.createElement('div');\n            buttonGroup.classList = 'btn-group';\n            continue;\n        }\n\n        button = fig.buttons[name] = document.createElement('button');\n        button.classList = 'btn btn-default';\n        button.href = '#';\n        button.title = name;\n        button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n        button.addEventListener('click', on_click_closure(method_name));\n        button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n        buttonGroup.appendChild(button);\n    }\n\n    if (buttonGroup.hasChildNodes()) {\n        toolbar.appendChild(buttonGroup);\n    }\n\n    // Add the status bar.\n    var status_bar = document.createElement('span');\n    status_bar.classList = 'mpl-message pull-right';\n    toolbar.appendChild(status_bar);\n    this.message = status_bar;\n\n    // Add the close button to the window.\n    var buttongrp = document.createElement('div');\n    buttongrp.classList = 'btn-group inline pull-right';\n    button = document.createElement('button');\n    button.classList = 'btn btn-mini btn-primary';\n    button.href = '#';\n    button.title = 'Stop Interaction';\n    button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n    button.addEventListener('click', function (_evt) {\n        fig.handle_close(fig, {});\n    });\n    button.addEventListener(\n        'mouseover',\n        on_mouseover_closure('Stop Interaction')\n    );\n    buttongrp.appendChild(button);\n    var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n    titlebar.insertBefore(buttongrp, titlebar.firstChild);\n};\n\nmpl.figure.prototype._remove_fig_handler = function (event) {\n    var fig = event.data.fig;\n    if (event.target !== this) {\n        // Ignore bubbled events from children.\n        return;\n    }\n    fig.close_ws(fig, {});\n};\n\nmpl.figure.prototype._root_extra_style = function (el) {\n    el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n};\n\nmpl.figure.prototype._canvas_extra_style = function (el) {\n    // this is important to make the div 'focusable\n    el.setAttribute('tabindex', 0);\n    // reach out to IPython and tell the keyboard manager to turn it's self\n    // off when our div gets focus\n\n    // location in version 3\n    if (IPython.notebook.keyboard_manager) {\n        IPython.notebook.keyboard_manager.register_events(el);\n    } else {\n        // location in version 2\n        IPython.keyboard_manager.register_events(el);\n    }\n};\n\nmpl.figure.prototype._key_event_extra = function (event, _name) {\n    // Check for shift+enter\n    if (event.shiftKey && event.which === 13) {\n        this.canvas_div.blur();\n        // select the cell after this one\n        var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n        IPython.notebook.select(index + 1);\n    }\n};\n\nmpl.figure.prototype.handle_save = function (fig, _msg) {\n    fig.ondownload(fig, null);\n};\n\nmpl.find_output_cell = function (html_output) {\n    // Return the cell and output element which can be found *uniquely* in the notebook.\n    // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n    // IPython event is triggered only after the cells have been serialised, which for\n    // our purposes (turning an active figure into a static one), is too late.\n    var cells = IPython.notebook.get_cells();\n    var ncells = cells.length;\n    for (var i = 0; i < ncells; i++) {\n        var cell = cells[i];\n        if (cell.cell_type === 'code') {\n            for (var j = 0; j < cell.output_area.outputs.length; j++) {\n                var data = cell.output_area.outputs[j];\n                if (data.data) {\n                    // IPython >= 3 moved mimebundle to data attribute of output\n                    data = data.data;\n                }\n                if (data['text/html'] === html_output) {\n                    return [cell, data, j];\n                }\n            }\n        }\n    }\n};\n\n// Register the function which deals with the matplotlib target/channel.\n// The kernel may be null if the page has been refreshed.\nif (IPython.notebook.kernel !== null) {\n    IPython.notebook.kernel.comm_manager.register_target(\n        'matplotlib',\n        mpl.mpl_figure_comm\n    );\n}\n"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": "<IPython.core.display.HTML object>",
+      "text/html": "<div id='33bff1e4-abde-4976-b503-0126a0cd70ce'></div>"
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "True positive: 6010\n",
+      "False negative: 2082\n",
+      "False positive: 1015\n",
+      "True negative: Not defined\n"
+     ]
+    }
+   ],
+   "source": [
+    "conf_mat_cnn = np.array([[tp/n_total, fn/n_total], [fp/n_total, tn/n_total]])\n",
+    "disp = ConfusionMatrixDisplay(conf_mat_cnn)\n",
+    "disp.plot()\n",
+    "plt.grid(False)\n",
+    "plt.show()\n",
+    "\n",
+    "print(f'True positive: {tp}')\n",
+    "print(f'False negative: {fn}')\n",
+    "print(f'False positive: {fp}')\n",
+    "print(f'True negative: Not defined')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.722610Z",
+     "start_time": "2024-04-12T16:34:14.648309Z"
+    }
+   }
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 32,
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Recall: 0.7427088482451805\n",
+      "Precision: 0.8555160142348754\n",
+      "F1-score: 0.7951313091221803\n"
+     ]
+    }
+   ],
+   "source": [
+    "precision = tp/(tp+fp)\n",
+    "recall = tp/(tp+fn)\n",
+    "print(f'Recall: {recall}')\n",
+    "print(f'Precision: {precision}')\n",
+    "print(f'F1-score: {2*precision*recall/(precision+recall)}')"
+   ],
+   "metadata": {
+    "collapsed": false,
+    "ExecuteTime": {
+     "end_time": "2024-04-12T16:34:14.738589Z",
+     "start_time": "2024-04-12T16:34:14.724590Z"
+    }
+   }
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/scripts/benchmark/benchmark_utils.py b/scripts/benchmark/benchmark_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c05ca05e026b10d9724757de827f4f28672d1b5
--- /dev/null
+++ b/scripts/benchmark/benchmark_utils.py
@@ -0,0 +1,93 @@
+import pandas as pd
+import numpy as np
+from pathlib import Path
+from typing import Callable
+from numpy.typing import ArrayLike
+
+
+from G_SPC.nn.preproc import ramp_1d_score
+
+
+def prepare_benchmark_data(
+    pth_observation: str | Path,
+    pth_inspect: str | Path,
+    pth_phase: str | Path | None,
+    load_fn: Callable[[str | Path], np.ndarray],
+    observation_length: int,
+    ramp_length: int,
+    anomaly_scale: float,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
+    """Prepare data with for benchmark
+
+    Parameters
+    ----------
+    pth_observation: str | Path,
+        path to observation data: real physical measurements, which shall be monitored/ controlled.
+        loaded data should have shape=(n_steps, n_vars)
+    pth_inspect: str | Path,
+        path to inspection data: real or synthetic QC-Data, should be bool np.ndarray with shape=(n_steps,),
+        where OK=True and NOK=False
+    pth_phase: str | Path,
+        path to phase data (in case of synthetic data): bool np.ndarray with shape=(n_steps,),
+        where OK-Phase=True and NOK-Phase=False
+    load_fn
+    observation_length
+    ramp_length
+    anomaly_scale: float
+        max anomaly score value (in case of NOK)
+
+    Returns
+    -------
+    tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
+        data_train, data_test, target_train, target_test
+    """
+    obs = load_fn(pth_observation)
+    insp = load_fn(pth_inspect)
+    phase = load_fn(pth_phase) if pth_phase else None
+
+    target = ramp_1d_score(insp, ramp_length, anomaly_scale)
+
+    return blockify_benchmark(obs, target, phase, observation_length)
+
+
+def blockify_benchmark(
+    inp: np.ndarray, target: np.ndarray, phase: np.ndarray | None, window_length: int
+) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
+    # inp_ret --> observations: either 1D or multidimensional
+    # target --> anomaly scores: always 1D
+    # phase --> phase information (OK/NOK): always 1D
+
+    if len(target.shape) > 1:
+        raise IndexError(f"`target` must be 1D. Got: {target.shape}")
+
+    if type(phase) == np.ndarray and len(phase.shape) > 1:
+        raise IndexError(f"`phase` must be 1D. Got: {phase.shape}")
+
+    inp_ret = []
+    target_ret = []
+    phase_ret = [] if type(phase) == np.ndarray else None
+
+    is_1d = len(inp.shape) == 1
+
+    for i in range(len(target) - window_length):
+
+        if is_1d:
+            inp_ret.append(inp[i : i + window_length])
+        else:
+            inp_ret.append(inp[i : i + window_length, :])
+
+        target_ret.append(target[i : i + window_length])
+        if type(phase) == np.ndarray:
+            phase_ret.append(np.all(phase[i : i + window_length]))
+
+    if is_1d:
+        inp_ret = np.expand_dims(inp_ret, axis=-1)
+
+    target_ret = np.array(target_ret)
+    if len(target_ret.shape) < 3:
+        target_ret = np.expand_dims(target_ret, -1)
+
+    # inp_ret: (blocks, window_length, 1) or (blocks, window_lengths, num_var)
+    # target_ret: (blocks, window_length, 1)
+    # phase_ret: (blocks)
+    return np.array(inp_ret), target_ret, np.array(phase_ret)
diff --git a/scripts/data_generation/test_data.py b/scripts/data_generation/test_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dc0f5afdb58e7b1a31c9afdae38ff25119c5c0c
--- /dev/null
+++ b/scripts/data_generation/test_data.py
@@ -0,0 +1,85 @@
+import json
+import pickle
+import random
+
+import numpy as np
+
+from scripts.data_generation.generate_k_initial import generate_k_initial
+from G_SPC.mixture.generator import generate_cycles, RampGenerator
+from G_SPC.mixture.distribution.impl import (
+    SciPyLogNorm,
+    SciPyUniform,
+    SciPyPareto,
+    SciPyCauchy,
+    SciPyLogLogistic,
+    NpMixture,
+    SciPyNormal, SciPyLogistic, SciPyLaplace, SciPyWeibull,
+)
+# define kinds of OK distributions
+ok_distribs_list = [[
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyLaplace(None, None, True),
+        SciPyLaplace(None, None, False),
+        SciPyLaplace(None, None, False),
+        SciPyCauchy(None, None, True),
+        SciPyCauchy(None, None, False),
+        SciPyCauchy(None, None, False),
+        SciPyUniform(None, None, True),
+        SciPyUniform(None, None, False),
+        SciPyUniform(None, None, False)]] * 11
+
+# define NOK distributions, make sure the nok distributions differ significantly from ok distributions
+nok_distribs_list = [[SciPyNormal.make_nok(-0.25, 0.5, 1.25, 3, 100)] for _ in range(10)]
+t = 100
+# generate random initial weights
+k_initial_list = [generate_k_initial(t, [0, 3, 6, 9, 12, 15], [1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17],
+                                     n_centered_dists=random.randint(0, 2), n_timeframes=10) for _ in range(20)]
+# define the ramp up / ramp down points of NOK distributions
+t_inflection_list = [random.randint(1, 25) if i % 2 == 1 else random.randint(975, 998) for i in range(20)]
+# define number of time steps (i.e. products) in each cycle
+nb_steps_list = [1000] * 20
+
+# define data generator object for each cycle
+generators_list = [
+    RampGenerator(
+        lower_limit=0,
+        upper_limit=1,
+        median=0.5,
+        sigma=3,
+        ok_distribs=ok_distribs_list[i // 2 if i % 2 == 0 else i // 2 + 1],
+        nok_distribs=nok_distribs_list[i // 2],
+        max_iter_parametrize=100,
+        mixture=NpMixture(),
+        revert=i % 2 != 0,
+        nb_cores=-1,
+    )
+    for i in range(len(k_initial_list))
+]
+# generate the dataset
+x, y, kmat, distrib_params, distribs = generate_cycles(
+    generators=generators_list,
+    k_initial_list=k_initial_list,
+    t_inflection_list=t_inflection_list,
+    nb_steps_list=nb_steps_list,
+)
+
+# save the results to files
+np.savetxt("../../data/test/x.csv", x, delimiter=",")
+
+np.savetxt("../../data/test/y.csv", y, delimiter=",")
+
+np.savetxt("../../data/test/k_matrix.csv", kmat, delimiter=",")
+
+with open("../../data/test/dist_parameters.json", "w") as f:
+    json.dump(distrib_params, f)
+
+with open("../../data/test/distribs", "wb") as f:
+    pickle.dump(distribs, f)
diff --git a/scripts/data_generation/train_data.py b/scripts/data_generation/train_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab147a2c0ff8295924ee91b3dbd4a6c8285c15c4
--- /dev/null
+++ b/scripts/data_generation/train_data.py
@@ -0,0 +1,98 @@
+import json
+import pickle
+import random
+
+import numpy as np
+
+from scripts.data_generation.generate_k_initial import generate_k_initial
+from G_SPC.mixture.generator import generate_cycles, RampGenerator
+from G_SPC.mixture.distribution.impl import (
+    SciPyLogNorm,
+    SciPyUniform,
+    SciPyPareto,
+    SciPyCauchy,
+    SciPyLogLogistic,
+    NpMixture,
+    SciPyNormal, SciPyLogistic, SciPyLaplace, SciPyWeibull,
+)
+# define kinds of OK distributions
+ok_distribs_list = [[
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, True),
+        SciPyNormal(None, None, False),
+        SciPyNormal(None, None, False)]] * 5001
+
+
+# define NOK distributions, make sure the nok distributions differ significantly from ok distributions
+nok_distribs_list = [[SciPyNormal.make_nok(0,0.5,1,3,100)] for _ in range(5000)]
+t = 100
+
+# generate random initial weights
+k_initial_list = [generate_k_initial(t, [0,3,6,9,12,15,18,21,24,27], [1,2,4,5,7,8,10,11,13,14,16,17,19,20,22,23,25,26,28],
+                                     n_centered_dists=random.randint(0,2), n_timeframes=4) for _ in range(10000)]
+
+
+# define the ramp up / ramp down points of NOK distributions
+t_inflection_list = [random.randint(1,20) if i % 2 == 1 else random.randint(80,98) for i in range(10000)]
+# define number of time steps (i.e. products) in each cycle
+nb_steps_list = [100]*10000
+
+# define data generator object for each cycle
+generators_list = [
+    RampGenerator(
+        lower_limit=0,
+        upper_limit=1,
+        median=0.5,
+        sigma=3,
+        ok_distribs=ok_distribs_list[i // 2 if i % 2 == 0 else i // 2 + 1],
+        nok_distribs=nok_distribs_list[i // 2],
+        max_iter_parametrize=1000,
+        mixture=NpMixture(),
+        revert=i % 2 != 0,
+        nb_cores=-1,
+    )
+    for i in range(len(k_initial_list))
+]
+# generate the dataset
+x, y, kmat, distrib_params, distribs = generate_cycles(
+    generators=generators_list,
+    k_initial_list=k_initial_list,
+    t_inflection_list=t_inflection_list,
+    nb_steps_list=nb_steps_list,
+)
+
+# save the results to files
+np.savetxt("../../data/training/x.csv", x, delimiter=",")
+
+np.savetxt("../../data/training/y.csv", y, delimiter=",")
+
+np.savetxt("../../data/training/k_matrix.csv", kmat, delimiter=",")
+
+with open("../../data/training/dist_parameters.json", "w") as f:
+    json.dump(distrib_params, f)
+
+with open("../../data/training/distribs", "wb") as f:
+    pickle.dump(distribs, f)
diff --git a/scripts/model_training/ResNet/__init__.py b/scripts/model_training/ResNet/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scripts/model_training/ResNet/model/best_model.h5 b/scripts/model_training/ResNet/model/best_model.h5
new file mode 100644
index 0000000000000000000000000000000000000000..4d912502a7ad0d40e38a8859f6188a23589886ed
Binary files /dev/null and b/scripts/model_training/ResNet/model/best_model.h5 differ
diff --git a/scripts/model_training/ResNet/model/hist.pkl b/scripts/model_training/ResNet/model/hist.pkl
new file mode 100644
index 0000000000000000000000000000000000000000..24f73cea014becc9833712599bcefbb9fe012132
Binary files /dev/null and b/scripts/model_training/ResNet/model/hist.pkl differ
diff --git a/scripts/model_training/ResNet/train.py b/scripts/model_training/ResNet/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..a97816f0698f5f26c5f3c2ae965f584c42222c85
--- /dev/null
+++ b/scripts/model_training/ResNet/train.py
@@ -0,0 +1,106 @@
+from tensorflow import keras
+import pandas as pd
+import pickle
+
+from G_SPC.nn.model import make_resnet_spc, rebalanced_nok_mse
+from G_SPC.nn.preproc_pipe import prepare_pipe_ramp
+
+
+PTH_OBS = "../../../data/training/x.csv"
+PTH_INSP = "../../../data/training/y.csv"
+
+OBS_WIN_RANGE = list(range(10, 31))
+RAMP_LEN_RANGE = list(range(3, 16))
+ANOM_SCALE_RANGE = [0.5, 1, 1.5, 2, 10]
+
+EPOCHS = 100_000
+BATCH_SIZE = 128
+OPTIMIZER = "adam"
+LOSS = rebalanced_nok_mse
+METRICS = ["mean_absolute_error", "mean_squared_error"]
+
+
+def load_fn(x):
+    return pd.read_csv(x, header=None).to_numpy().flatten()
+
+
+class HistorySaver:
+    def __init__(self, history, epoch, params):
+        self.history = history
+        self.epoch = epoch
+        self.params = params
+
+
+def prepare_training_data(obs_win, ramp_len, anom_scale):
+    x_train, x_test, y_train, y_test = prepare_pipe_ramp(
+        pth_observation=PTH_OBS,
+        pth_inspect=PTH_INSP,
+        load_fn=load_fn,
+        observation_length=obs_win,
+        ramp_length=ramp_len,
+        anomaly_scale=anom_scale,
+        shuffle=True,
+        test_split=0.2,
+        random_state=42,
+    )
+    y_train = y_train[:, -1]
+    y_test = y_test[:, -1]
+    return x_train, x_test, y_train, y_test
+
+
+def prepare_callbacks():
+    callbacks = [
+        keras.callbacks.ModelCheckpoint(
+            "model/best_model.h5", save_best_only=True, monitor="val_loss"
+        ),
+        keras.callbacks.ReduceLROnPlateau(
+            monitor="val_loss", factor=0.5, patience=20, min_lr=0.0001
+        ),
+        keras.callbacks.EarlyStopping(monitor="val_loss", patience=50, verbose=1),
+    ]
+    return callbacks
+
+
+def training_run(fn_make_model, obs_win, callbacks, x_train, x_test, y_train, y_test):
+    model = fn_make_model((obs_win, 1))
+
+    model.compile(
+        optimizer=OPTIMIZER,
+        loss=LOSS,
+        metrics=METRICS,
+    )
+
+    history = model.fit(
+        x_train,
+        y_train,
+        batch_size=BATCH_SIZE,
+        epochs=EPOCHS,
+        callbacks=callbacks,
+        verbose=1,
+        validation_data=(x_test, y_test),
+    )
+    return history, model
+
+
+def save_history(history):
+    with open("model/hist.pkl", "wb") as f:
+        hist = HistorySaver(history.history, history.epoch, history.params)
+        pickle.dump(hist, f, pickle.HIGHEST_PROTOCOL)
+
+
+def main():
+    # settings taken from cnn... (obs_win, ramp_len, anomaly_scale) -> (18, 3, 0.5)
+
+    x_train, x_test, y_train, y_test = prepare_training_data(18, 3, 0.5)
+
+    callbacks = prepare_callbacks()
+
+    history, _ = training_run(
+        make_resnet_spc, 18, callbacks, x_train, x_test, y_train, y_test
+    )
+
+    save_history(history)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/model_training/cnn/__init__.py b/scripts/model_training/cnn/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scripts/model_training/cnn/grid_search.py b/scripts/model_training/cnn/grid_search.py
new file mode 100644
index 0000000000000000000000000000000000000000..41ada89f8a34b045a37a95ca2187896d84d1a362
--- /dev/null
+++ b/scripts/model_training/cnn/grid_search.py
@@ -0,0 +1,96 @@
+import itertools
+from typing import Callable
+
+import numpy as np
+
+from G_SPC.nn.preproc import ramp_1d_score
+from G_SPC.nn.preproc_pipe import _pipe
+
+
+def _prepare_data(obs_data, insp_data, obs_win_i, rampl_len_i, anomaly_scale_i):
+    target = ramp_1d_score(insp_data, rampl_len_i, anomaly_scale_i)
+    x_train, x_test, y_train, y_test = _pipe(
+        obs_data, target, obs_win_i, True, 0.2, random_state=42
+    )
+    y_train = y_train[:, -1]
+    y_test = y_test[:, -1]
+    return x_train, x_test, y_train, y_test
+
+
+def grid_search(
+    fn_make_model: Callable,
+    optimizer,
+    loss,
+    metrics,
+    batch_size: int,
+    epochs: int,
+    pth_obs,
+    pth_insp,
+    load_fn: Callable,
+    data_len: int,
+    obs_win: list[int],
+    ramp_len: list[int],
+    anomaly_scale: list[float],
+) -> tuple[int, int, float]:
+
+    obs = load_fn(pth_obs)[:data_len]
+    insp = load_fn(pth_insp)[:data_len]
+
+    val_losses = []
+    settings = []
+
+    nb_total = len(obs_win) * len(ramp_len) * len(anomaly_scale)
+
+    for i, (obs_win_i, rampl_len_i, anomaly_scale_i) in enumerate(
+        itertools.product(obs_win, ramp_len, anomaly_scale)
+    ):
+        print(f"{i+1} / {nb_total}")
+        print(f"obs_win: {obs_win_i}")
+        print(f"ramp_len: {rampl_len_i}")
+        print(f"anomaly_scale: {anomaly_scale_i}")
+
+        if rampl_len_i >= obs_win_i:
+            print(f"Skipping obs_win_i={obs_win_i} and ramp_len_i={rampl_len_i}")
+            continue
+
+        x_train, x_test, y_train, y_test = _prepare_data(
+            obs, insp, obs_win_i, rampl_len_i, anomaly_scale_i
+        )
+
+        model = fn_make_model((obs_win_i, 1))
+
+        model.compile(
+            optimizer=optimizer,
+            loss=loss,
+            metrics=metrics,
+        )
+
+        history = model.fit(
+            x_train,
+            y_train,
+            batch_size=batch_size,
+            epochs=epochs,
+            verbose=1,
+            validation_data=(x_test, y_test),
+        )
+
+        min_val_loss = np.min(history.history["val_loss"])
+
+        settings.append((obs_win_i, rampl_len_i, anomaly_scale_i))
+        val_losses.append(min_val_loss)
+
+        print(f"Min val_loss: {min_val_loss}")
+
+    min_loss = np.argmin(val_losses)
+
+    best_settings = settings[min_loss]
+
+    print("---------------------------------------------")
+    print("Best settings:")
+    print(f"Observation window: {best_settings[0]}")
+    print(f"Ramp up length: {best_settings[1]}")
+    print(f"Anomaly scale: {best_settings[2]}")
+    print(f"Validation loss: {val_losses[min_loss]}")
+    print("---------------------------------------------")
+
+    return best_settings
diff --git a/scripts/model_training/cnn/model/best_model.h5 b/scripts/model_training/cnn/model/best_model.h5
new file mode 100644
index 0000000000000000000000000000000000000000..fdefdef0feeddd2d8cf2fe01a2c2390b08184cf6
Binary files /dev/null and b/scripts/model_training/cnn/model/best_model.h5 differ
diff --git a/scripts/model_training/cnn/model/hist.pkl b/scripts/model_training/cnn/model/hist.pkl
new file mode 100644
index 0000000000000000000000000000000000000000..9ccfd72cde7e3aa332dbbfbb11b14827b51c3dd6
Binary files /dev/null and b/scripts/model_training/cnn/model/hist.pkl differ
diff --git a/scripts/model_training/cnn/settings.pkl b/scripts/model_training/cnn/settings.pkl
new file mode 100644
index 0000000000000000000000000000000000000000..89d351762cde714f2401dcd543fb23f5b9a2a333
Binary files /dev/null and b/scripts/model_training/cnn/settings.pkl differ
diff --git a/scripts/model_training/cnn/train.py b/scripts/model_training/cnn/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..d95794ac4a44b6bab1ef6dd26bf1b8001aad5ff7
--- /dev/null
+++ b/scripts/model_training/cnn/train.py
@@ -0,0 +1,129 @@
+from tensorflow import keras
+import pandas as pd
+import pickle
+
+from G_SPC.nn.model import make_conv_spc
+from G_SPC.nn.preproc_pipe import prepare_pipe_ramp
+
+from scripts.training_conv_spc.grid_search import grid_search
+
+
+PTH_OBS = "../../../data/training/x.csv"
+PTH_INSP = "../../../data/training/y.csv"
+
+OBS_WIN_RANGE = list(range(10, 31))
+RAMP_LEN_RANGE = list(range(3, 16))
+ANOM_SCALE_RANGE = [0.5, 1, 1.5, 2, 10]
+
+EPOCHS = 100_000
+BATCH_SIZE = 32
+OPTIMIZER = "adam"
+LOSS = keras.losses.MeanSquaredError()
+METRICS = ["mean_absolute_error", "mean_squared_error"]
+
+
+def load_fn(x):
+    return pd.read_csv(x, header=None).to_numpy().flatten()
+
+
+class HistorySaver:
+    def __init__(self, history, epoch, params):
+        self.history = history
+        self.epoch = epoch
+        self.params = params
+
+
+def grid_search_run():
+    return grid_search(
+        make_conv_spc,
+        OPTIMIZER,
+        LOSS,
+        METRICS,
+        BATCH_SIZE,
+        5,
+        PTH_OBS,
+        PTH_INSP,
+        load_fn,
+        5_000,
+        OBS_WIN_RANGE,
+        RAMP_LEN_RANGE,
+        ANOM_SCALE_RANGE,
+    )
+
+
+def prepare_training_data(obs_win, ramp_len, anom_scale):
+    x_train, x_test, y_train, y_test = prepare_pipe_ramp(
+        pth_observation=PTH_OBS,
+        pth_inspect=PTH_INSP,
+        load_fn=load_fn,
+        observation_length=obs_win,
+        ramp_length=ramp_len,
+        anomaly_scale=anom_scale,
+        shuffle=True,
+        test_split=0.2,
+        random_state=42,
+    )
+    y_train = y_train[:, -1]
+    y_test = y_test[:, -1]
+    return x_train, x_test, y_train, y_test
+
+
+def prepare_callbacks():
+    callbacks = [
+        keras.callbacks.ModelCheckpoint(
+            "model/best_model.h5", save_best_only=True, monitor="val_loss"
+        ),
+        keras.callbacks.ReduceLROnPlateau(
+            monitor="val_loss", factor=0.5, patience=20, min_lr=0.0001
+        ),
+        keras.callbacks.EarlyStopping(monitor="val_loss", patience=50, verbose=1),
+    ]
+    return callbacks
+
+
+def training_run(fn_make_model, obs_win, callbacks, x_train, x_test, y_train, y_test):
+    model = fn_make_model((obs_win, 1))
+
+    model.compile(
+        optimizer=OPTIMIZER,
+        loss=LOSS,
+        metrics=METRICS,
+    )
+
+    history = model.fit(
+        x_train,
+        y_train,
+        batch_size=BATCH_SIZE,
+        epochs=EPOCHS,
+        callbacks=callbacks,
+        verbose=1,
+        validation_data=(x_test, y_test),
+    )
+    return history, model
+
+
+def save_history(history):
+    with open("model/hist.pkl", "wb") as f:
+        hist = HistorySaver(history.history, history.epoch, history.params)
+        pickle.dump(hist, f, pickle.HIGHEST_PROTOCOL)
+
+
+def main():
+    settings = grid_search_run()  # 1365 options to search through
+
+    with open("settings.pkl", "wb") as f:
+        pickle.dump(settings, f, pickle.HIGHEST_PROTOCOL)
+
+    x_train, x_test, y_train, y_test = prepare_training_data(*settings)
+
+    callbacks = prepare_callbacks()
+
+    history, _ = training_run(
+        make_conv_spc, settings[0], callbacks, x_train, x_test, y_train, y_test
+    )
+
+    save_history(history)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..da98a68caf26e5260366c473fd48e0c6ca218f95
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,7 @@
+from setuptools import setup, find_packages
+
+
+setup(
+    name="G_SPC",
+    packages=find_packages(exclude=["data"]),
+)